Compare commits

..

158 Commits

Author SHA1 Message Date
Romuald Juchnowicz-Bierbasz
630d878a3c Use constants 2019-07-12 11:46:48 +02:00
Romuald Juchnowicz-Bierbasz
cc63c24bde Increment version 2019-07-12 11:46:48 +02:00
Romuald Juchnowicz-Bierbasz
0d0f657240 SDK-2930: Refactor http module 2019-07-12 11:46:48 +02:00
Steven M. Vascellaro
33c630225d README.md cleanup (#17) 2019-07-12 10:39:37 +02:00
apaulouski
f4bd18a8ab Merge pull request #2 from rogersachan/rogersachan-patch-1
Fix spelling errors in the README
2019-07-10 12:12:07 +02:00
Roger
fa4541434f Merge branch 'master' into rogersachan-patch-1 2019-07-09 13:30:35 -04:00
rbierbasz-gog
c083a3089a Create .travis.yml 2019-07-08 15:43:12 +02:00
Romuald Juchnowicz-Bierbasz
f6b5a12b24 SDK-2932: Remove github deployment (use mirroring) 2019-07-03 13:42:06 +02:00
Romuald Juchnowicz-Bierbasz
8a67747df5 Merge remote-tracking branch 'github/master'
* github/master:
  version 0.38
  version 0.35.2
  version 0.35.1
  version 0.34
  version 0.33.1
  version 0.33
  version 0.32.1
  version 0.32.0
  version 0.31.3
  version 0.31.2
  version 0.31.1
  Initial commit
2019-07-03 13:35:34 +02:00
Romuald Juchnowicz-Bierbasz
2db9d0f383 Increment version 2019-07-01 14:35:05 +02:00
Mieszko Banczerowski
9d93762867 Workaround for removing creds on push_cache 2019-07-01 14:32:23 +02:00
Romuald Juchnowicz-Bierbasz
c364b716f4 Increment version 2019-07-01 13:16:08 +02:00
Romuald Juchnowicz-Bierbasz
48e1782484 SDK-2893: Optional game time and last played 2019-07-01 13:14:07 +02:00
Romuald Juchnowicz-Bierbasz
ff30675a25 Do not invoke tick before handshake 2019-07-01 12:26:05 +02:00
Aliaksei Paulouski
7b3965ff4b Add poe platform 2019-06-28 15:09:46 +02:00
Piotr Marzec
2ebdfabd9b Path of Exile added 2019-06-28 14:49:56 +02:00
Romuald Juchnowicz-Bierbasz
4e1ea8056d Add StreamLineReader with unit tests 2019-06-28 14:00:44 +02:00
Romuald Juchnowicz-Bierbasz
67e8681de6 Increment version 2019-06-28 11:59:01 +02:00
Romuald Juchnowicz-Bierbasz
77d742ce18 SDK-2910: Fix readline 2019-06-28 11:58:32 +02:00
GOG Galaxy SDK Team
f1fd00fcd3 version 0.38 2019-06-26 12:46:23 +02:00
Romuald Juchnowicz-Bierbasz
692bdbf370 Increment version 2019-06-26 12:07:28 +02:00
Romuald Juchnowicz-Bierbasz
207b1e1313 SDK-2880: Fix readline 2019-06-26 12:02:25 +02:00
Romuald Juchnowicz-Bierbasz
05042fe430 Increment version 2019-06-25 17:53:35 +02:00
Romuald Juchnowicz-Bierbasz
58b17d94fa SDK-2880: Read in chunks 2019-06-25 17:53:16 +02:00
Romuald Juchnowicz-Bierbasz
be03c83d45 SDK-2873: Add typing to API 2019-06-24 10:20:04 +02:00
Roger
1edf4ff5ba Fix spelling errors 2019-06-19 15:39:22 -04:00
Rafal Makagon
c07c7a2c2a Platform list 2019-06-18 15:56:45 +02:00
GOG Galaxy SDK Team
9d5d48032e version 0.35.2 2019-06-17 18:11:24 +02:00
Pawel Czoppa
cb1a5fa5e4 fix for merge request 2019-06-17 18:01:09 +02:00
Pawel Czoppa
4790238638 Update setup.py 2019-06-17 17:38:43 +02:00
Aliaksei Paulouski
5d90ba0c09 Increment version 2019-06-17 10:39:36 +02:00
Aliaksei Paulouski
d74ed3a4b5 Hide persistent cache data 2019-06-17 10:39:30 +02:00
Pawel Czoppa
d6f2d00fb9 typo 2019-06-15 15:11:05 +02:00
Piotr Marzec
ce9f33f5d0 platform names clieanup 2019-06-15 15:05:12 +02:00
Pawel Czoppa
b28fc60088 include Platform id's in sphinx generated documentation + some fixed 2019-06-15 14:57:20 +02:00
Piotr Marzec
be3d3bb7e5 text cleanup 2019-06-15 10:16:04 +02:00
Piotr Marzec
6dec4a99d3 add platforms list 2019-06-14 19:30:40 +02:00
Piotr Marzec
69ffef2fde create platform id list 2019-06-14 19:25:08 +02:00
Rafal Makagon
da59670d8e Increment version 2019-06-14 16:35:20 +02:00
Rafal Makagon
ed1049b543 Write down unexpected responses from http client 2019-06-14 16:32:07 +02:00
Rafal Makagon
9e8748b032 Increment version 2019-06-14 14:41:34 +02:00
Mieszko Banczerowski
bb482d4ed6 SDK-2872 update documentation about plugin deployment 2019-06-14 11:51:30 +02:00
Aliaksei Paulouski
909cc10762 Increment version 2019-06-13 12:26:32 +02:00
Rafal Makagon
9b4537c54f Fix anonimize params method 2019-06-13 12:25:57 +02:00
Rafal Makagon
2e90c66390 Remove not used errors 2019-06-13 12:21:34 +02:00
Aliaksei Paulouski
8647b06ca2 Add TooManyRequests error 2019-06-13 08:52:40 +02:00
Aliaksei Paulouski
f6522be74d SDK-2874: Persistent cache 2019-06-12 17:13:41 +02:00
Mieszko Banczerowski
5ca9254d2a GPI-438: Add link to docs in readme.md; fix make.py 2019-06-10 18:53:02 +02:00
Mieszko Banczerowski
79808e49f7 Bump version after adding documentation 2019-06-07 14:02:23 +02:00
Mateusz Silaczewski
7099cf3195 Merge branch 'documentation' of https://gitlab.gog.com/galaxy-client/galaxy-plugin-api into documentation 2019-06-06 15:56:01 +02:00
Rafal Makagon
ed91fd582c Deploy setup py 2019-05-31 12:08:49 +02:00
Rafal Makagon
bd393a96f0 Increment version 2019-05-31 11:50:53 +02:00
Piotr Marzec
c53aab1abb Legal Notice added 2019-05-31 11:29:05 +02:00
Romuald Juchnowicz-Bierbasz
80f40b1971 Increment version 2019-05-29 13:04:35 +02:00
Romuald Juchnowicz-Bierbasz
0da0296154 Release on gogcom 2019-05-28 17:59:11 +02:00
Romuald Juchnowicz-Bierbasz
9a115557b3 Fix username 2019-05-28 12:03:56 +02:00
Romuald Juchnowicz-Bierbasz
14c2d7d9e8 Update README 2019-05-28 11:28:32 +02:00
Romuald Juchnowicz-Bierbasz
4a7a759cea Update release scripts 2019-05-28 11:22:38 +02:00
Romuald Juchnowicz-Bierbasz
da8da24b01 Remove changelog 2019-05-28 11:19:48 +02:00
Rafal Makagon
ccbb13e685 Update README.md 2019-05-27 17:48:42 +02:00
Piotr Marzec
a3ca815975 cosmetic changes to readme.md 2019-05-27 17:41:28 +02:00
Romuald Juchnowicz-Bierbasz
f2d4127a31 Increment version 2019-05-24 14:04:17 +02:00
Romuald Juchnowicz-Bierbasz
07b6edce12 SDK-2840: Refactor import methods 2019-05-24 14:04:05 +02:00
Romuald Bierbasz
ef7f9ccca1 Fix pipeline 2019-05-21 14:27:02 +02:00
Romuald Juchnowicz-Bierbasz
3b296cbcc9 Fix pipeline 2019-05-21 14:18:45 +02:00
Romuald Juchnowicz-Bierbasz
f5361cd5ab Fix pipeline 2019-05-21 14:09:57 +02:00
Romuald Juchnowicz-Bierbasz
758909efba Fix release.py 2019-05-21 11:20:38 +02:00
Romuald Bierbasz
0bc8000f14 Fix release pipeline 2019-05-21 11:10:52 +02:00
Romuald Juchnowicz-Bierbasz
e62e7e0e6e Add pipeline for releasing to github 2019-05-20 15:52:35 +02:00
Romuald Juchnowicz-Bierbasz
be6c0eb03e Increment version 2019-05-20 15:38:24 +02:00
Aliaksei Paulouski
0ee56193de Fix aiohttp exception hierarchy 2019-05-20 11:20:27 +02:00
Romuald Juchnowicz-Bierbasz
6bc91a12fa Register new requests 2019-05-17 16:42:13 +02:00
Romuald Juchnowicz-Bierbasz
6d513d86bf Increment version 2019-05-17 16:42:13 +02:00
Romuald Juchnowicz-Bierbasz
bdd2225262 Add new interface methods for game time and achievements 2019-05-17 16:42:12 +02:00
Rafal Makagon
68fdc4d188 Printing own port 2019-05-17 15:32:24 +02:00
Romuald Juchnowicz-Bierbasz
f283c10a95 Anonymise params in pass_login_credentials 2019-05-15 19:49:50 +02:00
Romuald Juchnowicz-Bierbasz
453734cefe Increment version 2019-05-10 17:21:31 +02:00
Romuald Juchnowicz-Bierbasz
85f1d83c28 Fix parameters 2019-05-10 17:21:05 +02:00
Romuald Juchnowicz-Bierbasz
701d3cf522 Increment version 2019-05-10 17:06:01 +02:00
Romuald Juchnowicz-Bierbasz
c8083b9006 Add cookie_jar param to HttpClient 2019-05-10 17:05:40 +02:00
Romuald Juchnowicz-Bierbasz
0608ade6d3 Fix name 2019-05-10 14:14:33 +02:00
Romuald Juchnowicz-Bierbasz
c349a3df8e Add timeout 2019-05-10 13:56:53 +02:00
Romuald Juchnowicz-Bierbasz
1fd959a665 Handle ServerDisconnectedError 2019-05-10 13:50:46 +02:00
Romuald Juchnowicz-Bierbasz
234a21d085 Add HttpClient 2019-05-10 13:36:32 +02:00
Romuald Juchnowicz-Bierbasz
90835ece58 Change project layout 2019-05-10 13:16:28 +02:00
Aliaksei Paulouski
9e1c8cfddd Add JS to NextStep params 2019-05-03 15:56:40 +02:00
Aliaksei Paulouski
f7f170b9ca Increment version 2019-05-03 15:56:40 +02:00
Romuald Juchnowicz-Bierbasz
8ad5ed76b7 Increment version 2019-04-30 16:59:13 +02:00
Romuald Juchnowicz-Bierbasz
7727098c6f SDK-2762: Fix error handling 2019-04-30 16:58:54 +02:00
Romuald Juchnowicz-Bierbasz
e53dc8f2c6 Merge branch 'master' into parameter-checking
* master:
  Add friends features
2019-04-30 14:50:23 +02:00
Romuald Juchnowicz-Bierbasz
527fd034bf Increment version 2019-04-29 15:45:18 +02:00
Romuald Juchnowicz-Bierbasz
6e251c6eb9 SDK-2762: Bind method params before calling 2019-04-29 15:45:03 +02:00
Romuald Juchnowicz-Bierbasz
dc9fc2cc5d SDK-2762: Standarize parameter binding 2019-04-29 14:51:12 +02:00
Aliaksei Paulouski
1fb79eb21a Add friends features 2019-04-26 11:08:49 +02:00
Romuald Juchnowicz-Bierbasz
7b9bcf86a1 Increment version 2019-04-16 14:53:57 +02:00
Romuald Juchnowicz-Bierbasz
30b3533e1d Old style namespace package 2019-04-16 14:53:28 +02:00
Romuald Juchnowicz-Bierbasz
92b1d8e4df SDK-2760: Fix paths 2019-04-16 11:02:56 +02:00
Romuald Juchnowicz-Bierbasz
4adef2dace SDK-2760: Move modules 2019-04-16 10:38:45 +02:00
Romuald Juchnowicz-Bierbasz
1430fe39d7 SDK-2760: Make galaxy namespace package 2019-04-16 10:33:05 +02:00
Romuald Juchnowicz-Bierbasz
c591efc493 Increment version 2019-04-16 10:27:07 +02:00
Romuald Juchnowicz-Bierbasz
7c4f3fba5b SDK-2760: Add mock module 2019-04-16 10:26:37 +02:00
Romuald Juchnowicz-Bierbasz
f2e2e41d04 SDK-2760: Add tools module 2019-04-16 10:24:32 +02:00
Romuald Juchnowicz-Bierbasz
25b850d8bb SDK-2760: Dlc list optional 2019-04-16 10:21:31 +02:00
Romuald Juchnowicz-Bierbasz
403736612a Increment version, add changelog 2019-04-12 13:52:47 +02:00
Romuald Juchnowicz-Bierbasz
3071c2e771 SDK-2760: Add Epic platform 2019-04-12 13:51:41 +02:00
Aliaksei Paulouski
23ef34bed5 Increment version 2019-04-06 15:02:06 +02:00
Aliaksei Paulouski
a4b08f8105 Add Cookies to NextStep 2019-04-06 15:02:01 +02:00
Romuald Bierbasz
4d62b8ccb8 SDK-2743: Remove logging setup 2019-04-05 14:24:14 +02:00
Aliaksei Paulouski
d759b4aa85 Increment version 2019-03-28 14:37:30 +01:00
Romuald Juchnowicz-Bierbasz
9b33397827 Add NextStep and pass_login_credentials 2019-03-28 10:20:16 +01:00
Romuald Juchnowicz-Bierbasz
e09e443064 Increment version 2019-03-27 15:15:25 +01:00
Romuald Juchnowicz-Bierbasz
00ed52384a Exclude tests from package 2019-03-27 15:14:29 +01:00
Aliaksei Paulouski
958d9bc0e6 Fix send_message message param name 2019-03-25 11:46:30 +01:00
Pawel Kierski
d73d048ff7 Increment version to 0.17 2019-03-12 16:10:23 +01:00
Aliaksei Paulouski
e06e40f845 Fix duplicated error code 2019-03-12 15:53:42 +01:00
Paweł Kierski
833e6999d7 Return JSON-RPC reponse on generic Exception 2019-03-12 15:38:22 +01:00
Paweł Kierski
ca778e2cdb SDK-2647 Serializing local game state as integer 2019-03-11 13:47:12 +01:00
Romuald Juchnowicz-Bierbasz
9a06428fc0 Remove log 2019-03-11 12:19:45 +01:00
Romuald Juchnowicz-Bierbasz
f9eaeaf726 Increment version, add changelog 2019-03-11 11:08:28 +01:00
Romuald Juchnowicz-Bierbasz
f09171672f SDK-2636: Do not log sensitive data 2019-03-11 11:07:31 +01:00
Romuald Juchnowicz-Bierbasz
ca8d0dfaf4 Increment version, add changelog 2019-03-08 10:19:43 +01:00
Romuald Juchnowicz-Bierbasz
73bc9aa8ec SDK-2623: Call shutdown on socket close 2019-03-08 10:16:57 +01:00
Romuald Juchnowicz-Bierbasz
52273e2f8c Increment veresion, add changelog 2019-03-07 13:19:37 +01:00
Romuald Juchnowicz-Bierbasz
bda867473c SDK-2627: Add version param to Plugin 2019-03-07 13:18:26 +01:00
Pawel Kierski
6885cdc439 Increment version 2019-03-06 14:13:45 +01:00
Pawel Kierski
88e25a93be Ensure log folder exists 2019-03-06 11:15:33 +01:00
Pawel Kierski
67e7a4c0b2 Don't create log file if not specified 2019-03-05 12:19:48 +01:00
Paweł Kierski
788d2550e6 SDK-2552 optional achievement id or name 2019-03-05 12:18:33 +01:00
Rafal Makagon
059a1ea343 update logging facility in plugin API 2019-03-05 09:36:44 +01:00
Pawel Kierski
300ade5d43 Fix handling unknown notification 2019-03-04 11:52:08 +01:00
Paweł Kierski
43556a0470 SDK-2586 Return "None" instead of "Unknown" state for local game for Origin 2019-03-01 14:10:48 +01:00
Romuald Bierbasz
e244d3bb44 SDK-2577: Add UnknownBackendResponse 2019-02-28 15:20:03 +01:00
Romuald Bierbasz
d6e6efc633 SDK-2571: Refactor logging 2019-02-28 10:31:12 +01:00
Paweł Kierski
a114c9721c Add Unknown for all enums 2019-02-22 11:26:17 +01:00
Romuald Juchnowicz-Bierbasz
6c0389834b Increment version 2019-02-21 15:29:39 +01:00
Romuald Juchnowicz-Bierbasz
bc7d1c2914 SDK-2538: Use Optional 2019-02-21 15:17:38 +01:00
Romuald Juchnowicz-Bierbasz
d69e1aaa08 SDK-2538: Add LicenseType enum 2019-02-21 15:11:49 +01:00
Romuald Juchnowicz-Bierbasz
c2a0534162 Deploy only from master 2019-02-20 16:44:53 +01:00
Paweł Kierski
1614fd6eb2 Fix end of stream detecting 2019-02-20 16:41:44 +01:00
Paweł Kierski
48e54a8460 Revert "Make galaxy namespace package" 2019-02-20 14:09:34 +01:00
Paweł Kierski
70a1d5cd1f SDK-2521 switch plugin transport to sockets 2019-02-20 11:30:06 +01:00
Romuald Juchnowicz-Bierbasz
853ecf1d3b Make galaxy namespace package 2019-02-19 16:53:42 +01:00
Romuald Juchnowicz-Bierbasz
f025d9f93c SDK-2525: Increment version 2019-02-15 10:16:38 +01:00
Romuald Juchnowicz-Bierbasz
9f3df6aee3 SDK-2525: Add AuthenticationRequired error, change codes 2019-02-15 10:16:26 +01:00
Romuald Juchnowicz-Bierbasz
c6d5c55dfd SDK-2525: Refactor errors 2019-02-13 12:53:25 +01:00
Rafal Makagon
d78c08ae4b add authentication lost notification 2019-02-13 10:40:08 +01:00
Romuald Bierbasz
4cec6c09b2 SDK-2524: Fix achievement notification 2019-02-13 10:22:36 +01:00
Romuald Juchnowicz-Bierbasz
3e34edf5e7 Increment version 2019-02-13 08:23:07 +01:00
Romuald Juchnowicz-Bierbasz
0d52b3dda6 SDK-2526: Refactor authentication 2019-02-12 17:55:28 +01:00
Romuald Juchnowicz-Bierbasz
00fe3dd553 SDK-2520: Skipt deployment for tags 2019-02-11 15:12:57 +01:00
Romuald Juchnowicz-Bierbasz
20143e3b4f SDK-2520: Fix typo 2019-02-11 15:11:18 +01:00
Romuald Juchnowicz-Bierbasz
0b9b2dc8d3 SDK-2520: Add docs 2019-02-11 15:09:23 +01:00
Romuald Juchnowicz-Bierbasz
94b8c8d1a0 SDK-2520: Fix multiline command 2019-02-11 14:50:28 +01:00
Romuald Juchnowicz-Bierbasz
b7b759d483 SDK-2520: Configure deployment 2019-02-11 14:30:08 +01:00
Romuald Bierbasz
da91ff911f Merge branch 'ci-setup' into 'master'
SDK-2520: Configure CI

See merge request galaxy-client/galaxy-plugin-api!1
2019-02-11 12:02:02 +01:00
Romuald Bierbasz
68025644ff SDK-2520: Configure CI 2019-02-11 12:02:02 +01:00
Romuald Juchnowicz-Bierbasz
3e9276e419 SDK-2520: Move files from desktop-galaxy-client 2019-02-11 11:07:57 +01:00
Romuald Bierbasz
11a6416702 Initial commit 2019-02-11 10:30:41 +01:00
31 changed files with 517 additions and 174 deletions

7
.gitignore vendored
View File

@@ -1,2 +1,9 @@
# pytest
__pycache__/
.vscode/
.venv/
src/galaxy.plugin.api.egg-info/
docs/build/
Pipfile
.idea
docs/source/_build

26
.gitlab-ci.yml Normal file
View File

@@ -0,0 +1,26 @@
image: registry-gitlab.gog.com/galaxy-client/gitlab-ci-tools:latest
stages:
- test
- deploy
test_package:
stage: test
script:
- pip install -r requirements.txt
- pytest
except:
- tags
deploy_package:
stage: deploy
script:
- export VERSION=$(python setup.py --version)
- python setup.py sdist --formats=gztar upload -r gog-pypi
- 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}"
when: manual
only:
- master
except:
- tags

8
.travis.yml Normal file
View 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

View File

@@ -79,4 +79,4 @@ Platform ID list for GOG Galaxy 2.0 Integrations
| psvita | Playstation Vita |
| nds | Nintendo DS |
| 3ds | Nintendo 3DS |
| pathofexile | Path of Exile |

View File

@@ -1,35 +1,35 @@
# 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>
## 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:
- 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:
- importing owned and detecting installed games
- installing and launching games
- importing achievements and game time
- importing friends lists and statuses
- importing friends recomendations list
- receiving and sending chat messages
- importing owned and detecting installed games
- installing and launching games
- importing achievements and game time
- importing friends lists and statuses
- importing friends recommendations list
- receiving and sending chat messages
- cache storage
## Platform Id's
Each integration can implement only one platform. Each integration must declare which platform it's integrating.
[List of possible Platofrm IDs](PLATFORM_IDs.md)
[List of possible Platform IDs](PLATFORM_IDs.md)
## 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 of those method can raise exceptions inherited from the :exc:`~galaxy.api.jsonrpc.ApplicationError`.
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 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`.
```python
@@ -61,11 +61,13 @@ if __name__ == "__main__":
## Deployment
The client has a built-in Python 3.7 interpreter, so the integrations are delivered as python modules.
In order to be found by GOG Galaxy 2.0 an integration folder should be placed in [lookup directory](#deploy-location). Beside all the python files, the integration folder has to contain [manifest.json](#deploy-manifest) and all third-party dependencies. See an [examplary structure](#deploy-structure-example).
The client has a built-in Python 3.7 interpreter, so integrations are delivered as Python modules.
In order to be found by GOG Galaxy 2.0 an integration folder should be placed in [lookup directory](#deploy-location). Beside all the Python files, the integration folder must contain [manifest.json](#deploy-manifest) and all third-party dependencies. See an [exemplary structure](#deploy-structure-example).
### Lookup directory
<a name="deploy-location"></a>
- Windows:
`%localappdata%\GOG.com\Galaxy\plugins\installed`
@@ -75,8 +77,9 @@ In order to be found by GOG Galaxy 2.0 an integration folder should be placed in
`~/Library/Application Support/GOG.com/Galaxy/plugins/installed`
### Manifest
<a name="deploy-manifest"></a>
Obligatory JSON file to be placed in a integration folder.
<a name="deploy-manifest"></a>
Obligatory JSON file to be placed in an integration folder.
```json
{
@@ -91,6 +94,7 @@ Obligatory JSON file to be placed in a integration folder.
"script": "plugin.py"
}
```
| property | description |
|---------------|---|
| `guid` | |
@@ -99,13 +103,15 @@ Obligatory JSON file to be placed in a integration folder.
| `script` | path of the entry point module, relative to the integration folder |
### Dependencies
All third-party packages (packages not included in Python 3.7 standard library) should be deployed along with plugin files. Use the folowing command structure:
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 plugin that uses *requests* has structure as follows:
For example, a plugin that uses *requests* could have the following structure:
<a name="deploy-structure-example"></a>
```bash
installed
└── my_integration

View File

@@ -9,7 +9,7 @@
.. excluding Platforms Id's link
:ref:`platforms-link`.
:ref:`platforms-link`
.. mdinclude:: ../../README.md
:start-line: 28

View File

@@ -0,0 +1,2 @@
.. _platforms-link:
.. mdinclude:: ../../PLATFORM_IDs.md

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="galaxy.plugin.api",
version="0.35.1",
version="0.41",
description="GOG Galaxy Integrations Python API",
author='Galaxy team',
author_email='galaxy@gog.com',

View File

@@ -13,7 +13,74 @@ class Platform(Enum):
Uplay = "uplay"
Battlenet = "battlenet"
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"
class Feature(Enum):
"""Possible features that can be implemented by an integration.

View File

@@ -5,6 +5,8 @@ import logging
import inspect
import json
from galaxy.reader import StreamLineReader
class JsonRpcError(Exception):
def __init__(self, code, message, data=None):
self.code = code
@@ -67,7 +69,7 @@ def anonymise_sensitive_params(params, sensitive_params):
class Server():
def __init__(self, reader, writer, encoder=json.JSONEncoder()):
self._active = True
self._reader = reader
self._reader = StreamLineReader(reader)
self._writer = writer
self._encoder = encoder
self._methods = {}
@@ -114,6 +116,7 @@ class Server():
data = data.strip()
logging.debug("Received %d bytes of data", len(data))
self._handle_input(data)
await asyncio.sleep(0) # To not starve task queue
def stop(self):
self._active = False

View File

@@ -7,13 +7,14 @@ from enum import Enum
from collections import OrderedDict
import sys
from typing import List, Dict
from typing import Any, List, Dict, Optional, Union
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.errors import UnknownError, ImportInProgress
from galaxy.api.types import Authentication, NextStep, Message
class JSONEncoder(json.JSONEncoder):
@@ -37,6 +38,7 @@ class Plugin:
self._feature_methods = OrderedDict()
self._active = True
self._pass_control_task = None
self._reader, self._writer = reader, writer
self._handshake_token = handshake_token
@@ -209,15 +211,17 @@ class Plugin:
async def run(self):
"""Plugin's main coroutine."""
async def pass_control():
while self._active:
try:
self.tick()
except Exception:
logging.exception("Unexpected exception raised in plugin tick")
await asyncio.sleep(1)
await self._server.run()
if self._pass_control_task is not None:
await self._pass_control_task
await asyncio.gather(pass_control(), self._server.run())
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)
def _shutdown(self):
logging.info("Shutting down")
@@ -235,13 +239,14 @@ class Plugin:
def _initialize_cache(self, data: Dict):
self._persistent_cache = data
self.handshake_complete()
self._pass_control_task = asyncio.create_task(self._pass_control())
@staticmethod
def _ping():
pass
# notifications
def store_credentials(self, credentials: dict):
def store_credentials(self, credentials: Dict[str, Any]) -> None:
"""Notify the client to store authentication credentials.
Credentials are passed on the next authenticate call.
@@ -263,9 +268,10 @@ class Plugin:
return Authentication(user_data['userId'], user_data['username'])
"""
self.persistent_cache['credentials'] = credentials
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
of the currently authenticated user.
@@ -287,7 +293,7 @@ class Plugin:
params = {"owned_game": game}
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
of the currently authenticated user.
@@ -309,7 +315,7 @@ class Plugin:
params = {"game_id": game_id}
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
owned by the currently authenticated user.
@@ -318,7 +324,7 @@ class Plugin:
params = {"owned_game": game}
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.
:param game_id: game_id of the game for which to unlock an achievement.
@@ -330,7 +336,7 @@ class Plugin:
}
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.
@@ -343,7 +349,7 @@ class Plugin:
}
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.
@@ -359,12 +365,12 @@ class Plugin:
}
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)
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.
:param local_game: the LocalGame to update
@@ -390,7 +396,7 @@ class Plugin:
params = {"local_game": local_game}
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.
:param user: FriendInfo of a user that the client will add to friends list
@@ -398,7 +404,7 @@ class Plugin:
params = {"friend_info": user}
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.
:param user_id: id of the user to remove from friends list
@@ -406,7 +412,12 @@ class Plugin:
params = {"user_id": user_id}
self._notification_client.notify("friend_removed", params)
def update_room(self, room_id: str, unread_message_count=None, new_messages=None):
def update_room(
self,
room_id: str,
unread_message_count: Optional[int]=None,
new_messages: Optional[List[Message]]=None
) -> None:
"""WIP, Notify the client to update the information regarding
a chat room that the currently authenticated user is in.
@@ -421,7 +432,7 @@ class Plugin:
params["messages"] = new_messages
self._notification_client.notify("chat_room_updated", params)
def update_game_time(self, game_time: GameTime):
def update_game_time(self, game_time: GameTime) -> None:
"""Notify the client to update game time for a game.
:param game_time: game time to update
@@ -429,7 +440,7 @@ class Plugin:
params = {"game_time": game_time}
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.
@@ -438,7 +449,7 @@ class Plugin:
params = {"game_time": game_time}
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.
@@ -454,19 +465,19 @@ class Plugin:
}
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)
def lost_authentication(self):
def lost_authentication(self) -> None:
"""Notify the client that integration has lost authentication for the
current user and is unable to perform actions which would require it.
"""
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.
"""
self._notification_client.notify(
@@ -476,14 +487,14 @@ class Plugin:
)
# 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
before any other operations are called by the GOG Galaxy Client.
Persistent cache is available when this method is called.
Override it if you need to do additional plugin initializations.
This method is called internally."""
def tick(self):
def tick(self) -> None:
"""This method is called periodically.
Override it to implement periodical non-blocking tasks.
This method is called internally.
@@ -503,13 +514,13 @@ class Plugin:
"""
def shutdown(self):
def shutdown(self) -> None:
"""This method is called on integration shutdown.
Override it to implement tear down.
This method is called by the GOG Galaxy Client."""
# 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.
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.
@@ -537,7 +548,8 @@ class Plugin:
"""
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'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
@@ -592,7 +604,7 @@ class Plugin:
"""
raise NotImplementedError()
async def start_achievements_import(self, game_ids: List[str]):
async def start_achievements_import(self, game_ids: List[str]) -> None:
"""Starts the task of importing achievements.
This method is called by the GOG Galaxy Client.
@@ -611,7 +623,7 @@ class Plugin:
asyncio.create_task(import_games_achievements_task(game_ids))
self._achievements_import_in_progress = True
async def import_games_achievements(self, game_ids: List[str]):
async def import_games_achievements(self, game_ids: List[str]) -> None:
"""
Override this method to return the unlocked achievements
of the user that is currently logged in to the plugin.
@@ -652,7 +664,7 @@ class Plugin:
"""
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
identified by the provided game_id.
This method is called by the GOG Galaxy Client.
@@ -670,7 +682,7 @@ class Plugin:
"""
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
identified by the provided game_id.
This method is called by the GOG Galaxy Client.
@@ -688,7 +700,7 @@ class Plugin:
"""
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
identified by the provided game_id.
This method is called by the GOG Galaxy Client.
@@ -734,7 +746,7 @@ class Plugin:
"""
raise NotImplementedError()
async def send_message(self, room_id: str, message_text: str):
async def send_message(self, room_id: str, message_text: str) -> None:
"""WIP, Override this method to send message to a chat room.
This method is called by the GOG Galaxy Client.
@@ -743,7 +755,7 @@ class Plugin:
"""
raise NotImplementedError()
async def mark_as_read(self, room_id: str, last_message_id: str):
async def mark_as_read(self, room_id: str, last_message_id: str) -> None:
"""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.
@@ -759,7 +771,7 @@ class Plugin:
"""
raise NotImplementedError()
async def get_room_history_from_message(self, room_id: str, message_id: str):
async def get_room_history_from_message(self, room_id: str, message_id: str) -> List[Message]:
"""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.
@@ -768,7 +780,7 @@ class Plugin:
"""
raise NotImplementedError()
async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int):
async def get_room_history_from_timestamp(self, room_id: str, from_timestamp: int) -> List[Message]:
"""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.
@@ -784,7 +796,7 @@ class Plugin:
"""
raise NotImplementedError()
async def start_game_times_import(self, game_ids: List[str]):
async def start_game_times_import(self, game_ids: List[str]) -> None:
"""Starts the task of importing game times
This method is called by the GOG Galaxy Client.
@@ -803,7 +815,7 @@ class Plugin:
asyncio.create_task(import_game_times_task(game_ids))
self._game_times_import_in_progress = True
async def import_game_times(self, game_ids: List[str]):
async def import_game_times(self, game_ids: List[str]) -> None:
"""
Override this method to return game times for
games owned by the currently authenticated user.

View File

@@ -204,5 +204,5 @@ class GameTime():
:param last_time_played: last time the game was played (**unix timestamp**)
"""
game_id: str
time_played: int
last_played_time: int
time_played: Optional[int]
last_played_time: Optional[int]

View File

@@ -1,5 +1,6 @@
import asyncio
import ssl
from contextlib import contextmanager
from http import HTTPStatus
import aiohttp
@@ -12,44 +13,69 @@ from galaxy.api.errors import (
)
DEFAULT_LIMIT = 20
DEFAULT_TIMEOUT = 60 # seconds
class HttpClient:
def __init__(self, limit=20, timeout=aiohttp.ClientTimeout(total=60), cookie_jar=None):
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_context.load_verify_locations(certifi.where())
connector = aiohttp.TCPConnector(limit=limit, ssl=ssl_context)
self._session = aiohttp.ClientSession(connector=connector, timeout=timeout, cookie_jar=cookie_jar)
"""Deprecated"""
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):
await self._session.close()
async def request(self, method, url, *args, **kwargs):
try:
response = 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:
logging.exception(
"Caught exception while running {} request for {}".format(method, url))
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:
logging.warning(
"Got status {} while running {} request for {}".format(response.status, method, url))
raise UnknownError()
with handle_exception():
return await self._session.request(method, url, *args, **kwargs)
def create_tcp_connector(*args, **kwargs):
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)
def create_client_session(*args, **kwargs):
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)
@contextmanager
def handle_exception():
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()
return response

28
src/galaxy/reader.py Normal file
View 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

View File

@@ -11,7 +11,7 @@ from galaxy.unittest.mock import AsyncMock, coroutine_mock
@pytest.fixture()
def reader():
stream = MagicMock(name="stream_reader")
stream.readline = AsyncMock()
stream.read = AsyncMock()
yield stream
@pytest.fixture()
@@ -22,8 +22,8 @@ def writer():
yield stream
@pytest.fixture()
def readline(reader):
yield reader.readline
def read(reader):
yield reader.read
@pytest.fixture()
def write(writer):

View File

@@ -16,7 +16,7 @@ def test_initialization_no_id_nor_name():
with raises(AssertionError):
Achievement(unlock_time=1234567890)
def test_success(plugin, readline, write):
def test_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
@@ -25,7 +25,7 @@ def test_success(plugin, readline, write):
"game_id": "14"
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_unlocked_achievements.coro.return_value = [
Achievement(achievement_id="lvl10", unlock_time=1548421241),
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
@@ -57,7 +57,7 @@ def test_success(plugin, readline, write):
}
}
def test_failure(plugin, readline, write):
def test_failure(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
@@ -67,7 +67,7 @@ def test_failure(plugin, readline, write):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_unlocked_achievements.coro.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_unlocked_achievements.assert_called()

View File

@@ -9,14 +9,14 @@ from galaxy.api.errors import (
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied
)
def test_success(plugin, readline, write):
def test_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "init_authentication"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
asyncio.run(plugin.run())
plugin.authenticate.assert_called_with()
@@ -44,14 +44,14 @@ def test_success(plugin, readline, write):
pytest.param(Banned, 105, "Banned", id="banned"),
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
])
def test_failure(plugin, readline, write, error, code, message):
def test_failure(plugin, read, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "init_authentication"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.authenticate.coro.side_effect = error()
asyncio.run(plugin.run())
plugin.authenticate.assert_called_with()
@@ -66,7 +66,7 @@ def test_failure(plugin, readline, write, error, code, message):
}
}
def test_stored_credentials(plugin, readline, write):
def test_stored_credentials(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
@@ -77,7 +77,7 @@ def test_stored_credentials(plugin, readline, write):
}
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
asyncio.run(plugin.run())
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
@@ -100,7 +100,7 @@ def test_store_credentials(plugin, write):
"params": credentials
}
def test_lost_authentication(plugin, readline, write):
def test_lost_authentication(plugin, write):
async def couritine():
plugin.lost_authentication()

View File

@@ -9,7 +9,7 @@ from galaxy.api.errors import (
TooManyMessagesSent, IncoherentLastMessage, MessageNotFound
)
def test_send_message_success(plugin, readline, write):
def test_send_message_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
@@ -20,7 +20,7 @@ def test_send_message_success(plugin, readline, write):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.send_message.coro.return_value = None
asyncio.run(plugin.run())
plugin.send_message.assert_called_with(room_id="14", message="Hello!")
@@ -40,7 +40,7 @@ def test_send_message_success(plugin, readline, write):
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):
def test_send_message_failure(plugin, read, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "6",
@@ -51,7 +51,7 @@ def test_send_message_failure(plugin, readline, write, error, code, message):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.send_message.coro.side_effect = error()
asyncio.run(plugin.run())
plugin.send_message.assert_called_with(room_id="15", message="Bye")
@@ -66,7 +66,7 @@ def test_send_message_failure(plugin, readline, write, error, code, message):
}
}
def test_mark_as_read_success(plugin, readline, write):
def test_mark_as_read_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "7",
@@ -77,7 +77,7 @@ def test_mark_as_read_success(plugin, readline, write):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
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")
@@ -102,7 +102,7 @@ def test_mark_as_read_success(plugin, readline, write):
id="incoherent_last_message"
)
])
def test_mark_as_read_failure(plugin, readline, write, error, code, message):
def test_mark_as_read_failure(plugin, read, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "4",
@@ -113,7 +113,7 @@ def test_mark_as_read_failure(plugin, readline, write, error, code, message):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
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")
@@ -128,14 +128,14 @@ def test_mark_as_read_failure(plugin, readline, write, error, code, message):
}
}
def test_get_rooms_success(plugin, readline, write):
def test_get_rooms_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "2",
"method": "import_rooms"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_rooms.coro.return_value = [
Room("13", 0, None),
Room("15", 34, "8")
@@ -162,14 +162,14 @@ def test_get_rooms_success(plugin, readline, write):
}
}
def test_get_rooms_failure(plugin, readline, write):
def test_get_rooms_failure(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "9",
"method": "import_rooms"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_rooms.coro.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_rooms.assert_called_with()
@@ -184,7 +184,7 @@ def test_get_rooms_failure(plugin, readline, write):
}
}
def test_get_room_history_from_message_success(plugin, readline, write):
def test_get_room_history_from_message_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "2",
@@ -195,7 +195,7 @@ def test_get_room_history_from_message_success(plugin, readline, write):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_room_history_from_message.coro.return_value = [
Message("13", "149", 1549454837, "Hello"),
Message("14", "812", 1549454899, "Hi")
@@ -233,7 +233,7 @@ def test_get_room_history_from_message_success(plugin, readline, write):
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):
def test_get_room_history_from_message_failure(plugin, read, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "7",
@@ -244,7 +244,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write, error, c
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
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")
@@ -259,7 +259,7 @@ def test_get_room_history_from_message_failure(plugin, readline, write, error, c
}
}
def test_get_room_history_from_timestamp_success(plugin, readline, write):
def test_get_room_history_from_timestamp_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "7",
@@ -270,7 +270,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_room_history_from_timestamp.coro.return_value = [
Message("12", "155", 1549454836, "Bye")
]
@@ -296,7 +296,7 @@ def test_get_room_history_from_timestamp_success(plugin, readline, write):
}
}
def test_get_room_history_from_timestamp_failure(plugin, readline, write):
def test_get_room_history_from_timestamp_failure(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
@@ -307,7 +307,7 @@ def test_get_room_history_from_timestamp_failure(plugin, readline, write):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_room_history_from_timestamp.coro.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_room_history_from_timestamp.assert_called_with(

View File

@@ -0,0 +1,54 @@
import asyncio
import json
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 = [message[:5], message[5:], b""]
asyncio.run(plugin.run())
plugin.install_game.assert_called_with(game_id="3")
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 = [data, b""]
asyncio.run(plugin.run())
plugin.install_game.assert_called_with(game_id="3")
plugin.launch_game.assert_called_with(game_id="3")
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 = [message, b""]
asyncio.run(plugin.run())
plugin.install_game.assert_not_called()

View File

@@ -5,14 +5,14 @@ from galaxy.api.types import FriendInfo
from galaxy.api.errors import UnknownError
def test_get_friends_success(plugin, readline, write):
def test_get_friends_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_friends"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_friends.coro.return_value = [
FriendInfo("3", "Jan"),
FriendInfo("5", "Ola")
@@ -33,14 +33,14 @@ def test_get_friends_success(plugin, readline, write):
}
def test_get_friends_failure(plugin, readline, write):
def test_get_friends_failure(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_friends"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_friends.coro.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_friends.assert_called_with()

View File

@@ -6,17 +6,18 @@ import pytest
from galaxy.api.types import GameTime
from galaxy.api.errors import UnknownError, ImportInProgress, BackendError
def test_success(plugin, readline, write):
def test_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_game_times"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_game_times.coro.return_value = [
GameTime("3", 60, 1549550504),
GameTime("5", 10, 1549550502)
GameTime("5", 10, None),
GameTime("7", None, 1549550502),
]
asyncio.run(plugin.run())
plugin.get_game_times.assert_called_with()
@@ -35,20 +36,23 @@ def test_success(plugin, readline, write):
{
"game_id": "5",
"time_played": 10,
"last_played_time": 1549550502
},
{
"game_id": "7",
"last_played_time": 1549550502
}
]
}
}
def test_failure(plugin, readline, write):
def test_failure(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_game_times"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_game_times.coro.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_game_times.assert_called_with()

37
tests/test_http.py Normal file
View File

@@ -0,0 +1,37 @@
import asyncio
from http import HTTPStatus
import aiohttp
import pytest
from galaxy.api.errors import (
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
TooManyRequests, UnknownBackendResponse, UnknownError
)
from galaxy.http import handle_exception
request_info = aiohttp.RequestInfo("http://o.pl", "GET", {})
@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

View File

@@ -1,7 +1,7 @@
import asyncio
import json
def test_success(plugin, readline):
def test_success(plugin, read):
request = {
"jsonrpc": "2.0",
"method": "install_game",
@@ -10,7 +10,7 @@ def test_success(plugin, readline):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_owned_games.return_value = None
asyncio.run(plugin.run())
plugin.install_game.assert_called_with(game_id="3")

View File

@@ -4,7 +4,7 @@ import json
from galaxy.api.plugin import Plugin
from galaxy.api.consts import Platform
def test_get_capabilites(reader, writer, readline, write):
def test_get_capabilites(reader, writer, read, write):
class PluginImpl(Plugin): #pylint: disable=abstract-method
async def get_owned_games(self):
pass
@@ -16,7 +16,7 @@ def test_get_capabilites(reader, writer, readline, write):
}
token = "token"
plugin = PluginImpl(Platform.Generic, "0.1", reader, writer, token)
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
asyncio.run(plugin.run())
response = json.loads(write.call_args[0][0])
assert response == {
@@ -31,13 +31,13 @@ def test_get_capabilites(reader, writer, readline, write):
}
}
def test_shutdown(plugin, readline, write):
def test_shutdown(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "5",
"method": "shutdown"
}
readline.side_effect = [json.dumps(request)]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
asyncio.run(plugin.run())
plugin.shutdown.assert_called_with()
response = json.loads(write.call_args[0][0])
@@ -47,13 +47,13 @@ def test_shutdown(plugin, readline, write):
"result": None
}
def test_ping(plugin, readline, write):
def test_ping(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "7",
"method": "ping"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
asyncio.run(plugin.run())
response = json.loads(write.call_args[0][0])
assert response == {
@@ -62,7 +62,18 @@ def test_ping(plugin, readline, write):
"result": None
}
def test_tick(plugin, readline):
readline.side_effect = [""]
def test_tick_before_handshake(plugin, read):
read.side_effect = [b""]
asyncio.run(plugin.run())
plugin.tick.assert_not_called()
def test_tick_after_handshake(plugin, read):
request = {
"jsonrpc": "2.0",
"id": "6",
"method": "initialize_cache",
"params": {"data": {}}
}
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
asyncio.run(plugin.run())
plugin.tick.assert_called_with()

View File

@@ -1,7 +1,7 @@
import asyncio
import json
def test_success(plugin, readline):
def test_success(plugin, read):
request = {
"jsonrpc": "2.0",
"method": "launch_game",
@@ -10,7 +10,7 @@ def test_success(plugin, readline):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_owned_games.return_value = None
asyncio.run(plugin.run())
plugin.launch_game.assert_called_with(game_id="3")

View File

@@ -7,14 +7,14 @@ from galaxy.api.types import LocalGame
from galaxy.api.consts import LocalGameState
from galaxy.api.errors import UnknownError, FailedParsingManifest
def test_success(plugin, readline, write):
def test_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_local_games"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_local_games.coro.return_value = [
LocalGame("1", LocalGameState.Running),
@@ -53,14 +53,14 @@ def test_success(plugin, readline, write):
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
],
)
def test_failure(plugin, readline, write, error, code, message):
def test_failure(plugin, read, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_local_games"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_local_games.coro.side_effect = error()
asyncio.run(plugin.run())
plugin.get_local_games.assert_called_with()

View File

@@ -5,14 +5,14 @@ from galaxy.api.types import Game, Dlc, LicenseInfo
from galaxy.api.consts import LicenseType
from galaxy.api.errors import UnknownError
def test_success(plugin, readline, write):
def test_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_owned_games"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_owned_games.coro.return_value = [
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
Game(
@@ -67,14 +67,14 @@ def test_success(plugin, readline, write):
}
}
def test_failure(plugin, readline, write):
def test_failure(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_owned_games"
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_owned_games.coro.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_owned_games.assert_called_with()

View File

@@ -28,7 +28,7 @@ def cache_data():
}
def test_initialize_cache(plugin, readline, write, cache_data):
def test_initialize_cache(plugin, read, write, cache_data):
request_id = 3
request = {
"jsonrpc": "2.0",
@@ -36,7 +36,7 @@ def test_initialize_cache(plugin, readline, write, cache_data):
"method": "initialize_cache",
"params": {"data": cache_data}
}
readline.side_effect = [json.dumps(request)]
read.side_effect = [json.dumps(request).encode() + b"\n"]
assert {} == plugin.persistent_cache
asyncio.run(plugin.run())

View File

@@ -0,0 +1,52 @@
from unittest.mock import MagicMock
import pytest
from galaxy.reader import StreamLineReader
from galaxy.unittest.mock import AsyncMock
@pytest.fixture()
def stream_reader():
reader = MagicMock()
reader.read = AsyncMock()
return reader
@pytest.fixture()
def read(stream_reader):
return stream_reader.read
@pytest.fixture()
def reader(stream_reader):
return StreamLineReader(stream_reader)
@pytest.mark.asyncio
async def test_message(reader, read):
read.return_value = b"a\n"
assert await reader.readline() == b"a"
read.assert_called_once()
@pytest.mark.asyncio
async def test_separate_messages(reader, read):
read.side_effect = [b"a\n", b"b\n"]
assert await reader.readline() == b"a"
assert await reader.readline() == b"b"
assert read.call_count == 2
@pytest.mark.asyncio
async def test_connected_messages(reader, read):
read.return_value = b"a\nb\n"
assert await reader.readline() == b"a"
assert await reader.readline() == b"b"
read.assert_called_once()
@pytest.mark.asyncio
async def test_cut_message(reader, read):
read.side_effect = [b"a", b"b\n"]
assert await reader.readline() == b"ab"
assert read.call_count == 2
@pytest.mark.asyncio
async def test_half_message(reader, read):
read.side_effect = [b"a", b""]
assert await reader.readline() == b""
assert read.call_count == 2

View File

@@ -1,7 +1,7 @@
import asyncio
import json
def test_success(plugin, readline):
def test_success(plugin, read):
request = {
"jsonrpc": "2.0",
"method": "uninstall_game",
@@ -10,7 +10,7 @@ def test_success(plugin, readline):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_owned_games.return_value = None
asyncio.run(plugin.run())
plugin.uninstall_game.assert_called_with(game_id="3")

View File

@@ -6,7 +6,7 @@ from galaxy.api.errors import UnknownError
from galaxy.api.consts import PresenceState
def test_get_users_success(plugin, readline, write):
def test_get_users_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "8",
@@ -16,7 +16,7 @@ def test_get_users_success(plugin, readline, write):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_users.coro.return_value = [
UserInfo("5", False, "Ula", "http://avatar.png", Presence(PresenceState.Offline))
]
@@ -43,7 +43,7 @@ def test_get_users_success(plugin, readline, write):
}
def test_get_users_failure(plugin, readline, write):
def test_get_users_failure(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "12",
@@ -53,7 +53,7 @@ def test_get_users_failure(plugin, readline, write):
}
}
readline.side_effect = [json.dumps(request), ""]
read.side_effect = [json.dumps(request).encode() + b"\n", b""]
plugin.get_users.coro.side_effect = UnknownError()
asyncio.run(plugin.run())
plugin.get_users.assert_called_with(user_id_list=["10", "11", "12"])