Compare commits

..
266 Commits
Author SHA1 Message Date
unknown 9062944d4f adhere to comments 2020-02-18 15:09:19 +01:00
unknown 2251747281 adhere to comments 2020-02-18 15:03:58 +01:00
unknown 0245e47a74 cleanup docs, up version 2020-02-11 09:53:38 +01:00
unknown 0c51ff2cc9 adhere to comments, move importers to seperate module 2020-02-10 11:37:01 +01:00
unknown cd452b881d include subscription_name in partial finished notification 2020-02-10 10:05:58 +01:00
unknown 19c9f14ca9 separate sub importer, notify partial finished per subscription 2020-02-10 09:26:48 +01:00
unknown f5683d222a adhere to comments 2020-02-07 09:20:33 +01:00
unknown 44ea89ef63 adhere to comments 2020-02-07 09:17:26 +01:00
unknown 325cf66c7d cleanup 2020-02-06 13:52:07 +01:00
unknown cd8aecac8f use python from galaxy 2020-02-06 13:49:23 +01:00
unknown 3aa37907fc use python from galaxy 2020-02-06 13:49:02 +01:00
unknown 01e844009b use python from galaxy 2020-02-06 13:45:24 +01:00
unknown 4a7febfa37 check remote tests fix 2020-02-06 12:20:30 +01:00
unknown f9eb9ab6cb dont change line in unittest/mock.py 2020-02-06 11:49:30 +01:00
unknown 134fbe2752 adjust tests, allow for None yield 2020-02-06 11:47:34 +01:00
unknown bd8e6703e0 async yield 2020-02-06 11:05:47 +01:00
unknown 74e3825f10 prepare interfaces for subscriptions 2020-02-05 16:28:15 +01:00
Mieszko Banczerowski 62206318bd GPI-1122 get_local_size docs clarification about unknown size and 0 value use-cases 2020-02-03 11:23:01 +01:00
Mieszko Banczerowski 083b9f869f GPI-1050 More detailed logging in http module 2020-01-28 15:03:21 +01:00
Mieszko Banczerowski 617dbdfee7 GPI-1109 Implement get_game_size 2020-01-28 10:35:54 +01:00
Denis LE 65f4334c03 Fix typo in galaxy.http doc 2019-12-25 12:42:56 +01:00
Aleksej Pawlowskij 26102dd832 Increment version 2019-12-17 15:56:37 +01:00
Aleksej Pawlowskij cdcebda529 SDK-3136: Relax install requirements 2019-12-17 15:43:47 +01:00
Romuald Bierbasz a83f348d7d Increment version 2019-12-10 16:02:40 +01:00
Romuald Bierbasz 1c196d60d5 SDK-3199: Log response json 2019-12-10 16:00:46 +01:00
Aleksej Pawlowskij deb125ec48 Add missing psutil setup requirement 2019-12-05 16:22:26 +01:00
Rafal Makagon 4cc0055119 Increment version 2019-12-05 13:58:04 +01:00
Romuald Bierbasz 00164fab67 Correctly set _import_in_progress 2019-12-05 11:39:09 +01:00
Romuald Juchnowicz-Bierbasz 453cd1cc70 Do not send notificaitons when import is cancelled 2019-12-03 14:06:55 +01:00
Romuald Juchnowicz-Bierbasz 1f55253fd7 Wait until writer is closed 2019-12-03 14:04:19 +01:00
Romuald Juchnowicz-Bierbasz 7aa3b01abd Add Importer class (reuse code for importers) 2019-12-03 14:03:53 +01:00
Rafal Makagon bd14d58bad Increment version 2019-11-28 14:37:46 +01:00
Romuald Juchnowicz-Bierbasz 274b9a2c18 Do not wait for drain 2019-11-28 13:10:58 +01:00
Rafal Makagon 75e5a66fbe Increment version 2019-11-27 13:14:11 +01:00
Mieszko Banczerowski 2a9ec3067d Fix sending Exceptions with custom data 2019-11-27 13:12:20 +01:00
Rafal Makagon 69532a5ba9 fix richpresence parameter name 2019-11-27 13:10:43 +01:00
Romuald Juchnowicz-Bierbasz f5d47b0167 Add timeout to shutdown 2019-11-22 13:11:08 +01:00
Romuald Juchnowicz-Bierbasz 02f4faa432 Do not use root logger 2019-11-22 13:07:33 +01:00
Romuald Juchnowicz-Bierbasz 3d3922c965 Add async_raise 2019-11-20 17:57:17 +01:00
Rafal Makagon b695cdfc78 Increment version 2019-11-20 16:23:23 +01:00
Rafal Makagon 66ab1809b8 Do not log data sent to socket 2019-11-20 15:48:00 +01:00
Rafal Makagon 8bf367d0f9 Increment vesion 2019-11-18 13:59:09 +01:00
Rafal Makagon 2cf83395fa fix parse sphinx parse error
+ other small imporvements in docs
2019-11-15 16:06:45 +01:00
Aliaksei Paulouski 4aa76b6e3d SDK-3137: friends and presence updates 2019-11-13 13:40:53 +01:00
Aleksej Pawlowskij c03465e8f2 SDK-3145: Add optional profile and avatar url 2019-11-13 08:49:38 +01:00
mezzode 810a87718d Fix incorrect field name in GameTime docstring 2019-11-08 11:25:58 +01:00
Rafal Makagon e32abe11b7 Increment version 2019-11-07 12:29:13 +01:00
FriendsOfGalaxy d79f183826 Fix RegistryMonitor.is_updated method 2019-11-04 14:26:26 +01:00
Rafal Makagon 78f1d5a4cc Add refresh_credentials method to plugin 2019-10-31 15:15:14 +01:00
Rafal Makagon 9041dbd98c Increase size that is to be read at once from reader's buffer 2019-10-30 15:42:03 +01:00
Aleksej Pawlowskij e57ecc489c SDK-3110: Deprecate FriendInfo and replace with UserInfo 2019-10-28 11:37:21 +01:00
Romuald Bierbasz 0a20629459 Use pytest 5.2.2 2019-10-28 11:35:59 +01:00
Romuald Bierbasz 1585bab203 Wait for drain before writing 2019-10-22 11:30:01 +02:00
Aleksej Pawlowskij 92caf682d8 Increment version 2019-10-21 16:13:09 +02:00
Aleksej Pawlowskij 062d6a9428 Add user presence import feature 2019-10-21 16:11:10 +02:00
Romuald Juchnowicz-Bierbasz c874bc1d6e Increment version 2019-10-21 14:54:23 +02:00
Romuald Bierbasz 2dc56571d6 Revert "Add more logs"
This reverts commit 21ab8bf33d3c2714b8c7af2da8036fe8baae20ea.
2019-10-21 14:50:33 +02:00
Romuald Bierbasz eb216a50a8 Fix mypy ignores 2019-10-21 14:20:21 +02:00
Rafal Makagon c9b1c8fcae Increment version 2019-10-15 12:58:48 +02:00
Aleksej Pawlowskij a19a6cf11f Add Rockstar platform 2019-10-07 14:12:44 +02:00
Aleksej Pawlowskij 98cff9cfb8 SDK-3069: add OS compatibility import 2019-10-02 15:41:16 +02:00
Rafal Makagon 2e2aa8c4a0 Increment version 2019-10-01 11:21:32 +02:00
Rafal Makagon f57e03db2d Add game library settings feature 2019-09-27 16:15:57 +02:00
Rafal Makagon 66085e2239 Revert "Add ignoring not having windll to mypy"
This reverts commit 55c7fcfd61e0391287e2717117da4fca03b77dec.
2019-09-27 15:37:21 +02:00
Romuald Bierbasz 4d3c9b78c4 Add RegistryMonitor 2019-09-25 11:46:18 +02:00
Romuald Juchnowicz-Bierbasz 392e4c5f68 Print error data in import notifications 2019-09-19 14:47:50 +02:00
Romuald Juchnowicz-Bierbasz 4d6d3b8eb2 Increment version 2019-09-18 12:16:34 +02:00
Romuald Juchnowicz-Bierbasz d5610221a9 Do not wait for external tasks in run 2019-09-18 12:15:18 +02:00
Aleksej Pawlowskij aa7b398d3b Increment version 2019-09-05 09:14:46 +02:00
Aleksej Pawlowskij 8d6ec500f9 SDK-3041: Add new platforms 2019-09-05 09:14:37 +02:00
Romuald Juchnowicz-Bierbasz bab0be9994 Increment version 2019-09-02 17:31:47 +02:00
Romuald Juchnowicz-Bierbasz 0294e2a1f1 SDK-3023: Introduce task managers 2019-09-02 17:28:13 +02:00
Mieszko Banczerowski 0ab00e4119 Fix typing in persistent cache 2019-08-23 17:48:42 +02:00
Mieszko Banczerowski b20fce057b GPI-661: Add docs to http module 2019-08-23 17:46:08 +02:00
Romuald Juchnowicz-Bierbasz dec59f47dd Add more detials to launch_platform_client method 2019-08-22 09:42:10 +02:00
Mieszko Banczerowski ca85b2428b GPI-517: Mark coroutines in docs 2019-08-20 14:35:54 +02:00
Pawel Czoppa d8a00d58a6 Merge branch 'master' of https://gitlab.gog.com/galaxy-client/galaxy-plugin-api into platform_id_update
# Conflicts:
#	PLATFORM_IDs.md
2019-08-20 14:22:36 +02:00
Romuald Juchnowicz-Bierbasz d4cd1cedfd Install wheel in deploy 2019-08-14 16:07:01 +02:00
Romuald Juchnowicz-Bierbasz 161122b94d SDK-2988: Upload to official pypi server 2019-08-14 13:48:35 +02:00
Romuald Juchnowicz-Bierbasz cec36695b6 Increment version 2019-08-13 16:08:59 +02:00
Romuald Juchnowicz-Bierbasz 4cc8be8f5d SDK-2997: Add launch_platform_client method 2019-08-13 15:23:20 +02:00
Vadim Suharnikov f5eb32aa19 Fix a comment typo 2019-08-04 01:06:22 +02:00
Romuald Bierbasz a76345ff6b Docs fixes 2019-08-02 17:23:56 +02:00
Romuald Bierbasz c3bbeee54d Turn on type checking 2019-08-02 15:15:29 +02:00
Romuald Juchnowicz-Bierbasz 13a3f7577b Increment version 2019-08-02 15:13:11 +02:00
Romuald Bierbasz f5b9adfbd5 Add achievements_import_complete and game_times_import_complete 2019-08-02 15:11:05 +02:00
Romuald Juchnowicz-Bierbasz e33dd09a8d Increment version 2019-08-02 12:57:13 +02:00
Aleksej Pawlowskij f4ea2af924 Make 'handshake_complete' safe 2019-08-02 12:29:48 +02:00
Romuald Bierbasz 3bcc674518 Use MagicMocks with async_return_value and pytest.mark.asyncio 2019-08-02 12:28:13 +02:00
Romuald Juchnowicz-Bierbasz b14595bef5 Fix types 2019-08-02 12:20:27 +02:00
Romuald Bierbasz a9acb7a0db SDK-2931: Refactor import methods 2019-08-02 10:10:58 +02:00
Mesco d95aacb9d8 Fix typo in docstring 2019-08-01 09:53:50 +02:00
Aleksej Pawlowskij 49ae2beab9 SDK-2983: Split entry methods from feature detection 2019-07-29 12:59:45 +02:00
Aleksej Pawlowskij c9e190772c Increment version 2019-07-26 16:28:47 +02:00
Aleksej Pawlowskij 789415d31b SDK-2974: add process info tools 2019-07-26 13:41:43 +02:00
Rafal Makagon 223adf6a38 Remove chat and users 2019-07-24 10:34:22 +02:00
Romuald Juchnowicz-Bierbasz bfb63a42bd SDK-2951: Add create_task method 2019-07-23 17:29:20 +02:00
Romuald Juchnowicz-Bierbasz 53b3062719 SDK-2951: Use WindowsProactorEventLoopPolicy on Windows 2019-07-23 17:08:48 +02:00
Steven M. Vascellaro 49eb10ac8a Add MIT LICENSE (#19)
Based on license from 'galaxy-csharp-demo-game'
https://github.com/gogcom/galaxy-csharp-demo-game/blob/da1b7f14536f5afc0373c9e000c543a69694ca50/LICENSE.md
2019-07-18 11:26:47 +02:00
Romuald Juchnowicz-Bierbasz 10ecef791f Increment version 2019-07-17 15:35:38 +02:00
Romuald Bierbasz ce193f39bc SDK-2933: Add shutdown_client notification 2019-07-17 15:27:27 +02:00
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
Piotr Marzec 8d210e7f3e platform name fixing 2019-06-19 20:43:25 +02: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
GOG Galaxy SDK Team 179fd147c1 version 0.35.1 2019-06-17 17:41:44 +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
GOG Galaxy SDK Team 7789927ed9 version 0.34 2019-06-14 16:54:11 +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
GOG Galaxy SDK Team e2f26271cb version 0.33.1 2019-06-14 14:49:25 +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
GOG Galaxy SDK Team 3bd0b71ab3 version 0.33 2019-06-13 12:30:12 +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
GOG Galaxy SDK Team 192d655d51 version 0.32.1 2019-06-10 19:04:08 +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
GOG Galaxy SDK Team 6c6dc42cd6 version 0.32.0 2019-06-07 15:08:47 +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
60 changed files with 4539 additions and 1349 deletions

No files matched your search

+9
View File
@@ -1,2 +1,11 @@
# pytest
__pycache__/
.vscode/
.venv/
src/galaxy.plugin.api.egg-info/
docs/build/
Pipfile
.idea
docs/source/_build
.mypy_cache
.pytest_cache
+32
View File
@@ -0,0 +1,32 @@
image: registry-gitlab.gog.com/docker/python:3.7.3
stages:
- test
- deploy
test_package:
stage: test
script:
- pip install -r requirements.txt
- pytest
except:
- tags
deploy_package:
stage: deploy
variables:
TWINE_USERNAME: $PYPI_USERNAME
TWINE_PASSWORD: $PYPI_PASSWORD
script:
- pip install twine wheel
- rm -rf dist
- export VERSION=$(python setup.py --version)
- python setup.py sdist --formats=gztar bdist_wheel
- twine upload dist/*
- 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
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
+21
View 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
View 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 |
+76 -15
View File
@@ -1,10 +1,36 @@
# GOG Galaxy - Community Integration - Python API
# GOG Galaxy Integrations Python API
This document is still work in progress.
This Python library allows developers to easily build community integrations for various gaming platforms with GOG Galaxy 2.0.
## Basic Usage
- refer to our <a href='https://galaxy-integrations-python-api.readthedocs.io'>documentation</a>
Basic implementation:
## Features
Each integration in GOG Galaxy 2.0 comes as a separate Python script and is launched as a separate process that needs to communicate with the main instance of GOG Galaxy 2.0.
The provided features are:
- 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 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 Platform IDs](PLATFORM_IDs.md)
## Basic usage
Each integration should inherit from the :class:`~galaxy.api.plugin.Plugin` class. Supported methods like :meth:`~galaxy.api.plugin.Plugin.get_owned_games` should be overwritten - they are called from the GOG Galaxy client at the appropriate times.
Each 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
import sys
@@ -33,7 +59,28 @@ if __name__ == "__main__":
main()
```
Plugin should be deployed with manifest:
## Deployment
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`
- 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
{
"name": "Example plugin",
@@ -48,20 +95,34 @@ Plugin should be deployed with manifest:
}
```
## Development
| property | description |
|---------------|---|
| `guid` | |
| `description` | |
| `url` | |
| `script` | path of the entry point module, relative to the integration folder |
Install required packages:
```bash
pip install -r requirements.txt
```
### 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>
Run tests:
```bash
pytest
installed
└── my_integration
   ├── galaxy
   │   └── api
   ├── requests
   │   └── ...
   ├── plugin.py
└── manifest.json
```
## Methods Documentation
TODO
## 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.
+16
View File
@@ -0,0 +1,16 @@
"""Builds documentation locally. Use for preview only"""
import pathlib
import subprocess
import webbrowser
source = pathlib.Path("docs", "source")
build = pathlib.Path("docs", "build")
master_doc = 'index.html'
subprocess.run(['sphinx-build', '-M', 'clean', str(source), str(build)])
subprocess.run(['sphinx-build', '-M', 'html', str(source), str(build)])
master_path = build / 'html' / master_doc
webbrowser.open(f'file://{master_path.resolve()}')
+5
View File
@@ -0,0 +1,5 @@
Sphinx==2.0.1
sphinx-rtd-theme==0.4.3
sphinx-autodoc-typehints==1.6.0
sphinxcontrib-asyncio==0.2.0
m2r==0.2.1
+75
View File
@@ -0,0 +1,75 @@
# Configuration file for the Sphinx documentation builder.
# Documentation:
# http://www.sphinx-doc.org/en/master/config
import os
import sys
import subprocess
# -- Path setup --------------------------------------------------------------
_ROOT = os.path.join('..', '..')
sys.path.append(os.path.abspath(os.path.join(_ROOT, 'src')))
# -- Project information -----------------------------------------------------
project = 'GOG Galaxy Integrations API'
copyright = '2019, GOG.com'
_author, _version = subprocess.check_output(
['python', os.path.join(_ROOT, 'setup.py'), '--author', '--version'],
universal_newlines=True).strip().split('\n')
author = _author
version = _version
release = _version
# -- General configuration ---------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.autodoc',
'sphinxcontrib.asyncio',
'sphinx_autodoc_typehints',
'm2r' # mdinclude directive for makrdown files
]
autodoc_member_order = 'bysource'
autodoc_inherit_docstrings = False
autodoc_mock_imports = ["aiohttp"]
set_type_checking_flag = True
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
# This pattern also affects html_static_path and html_extra_path.
exclude_patterns = [] # type: ignore
# -- Options for HTML output -------------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
#
html_theme = "sphinx_rtd_theme"
html_theme_options = {
# 'canonical_url': '', # main page to be serach in google with trailing slash
'display_version': True,
'style_external_links': True,
# Toc options
'collapse_navigation': False,
'sticky_navigation': True,
'navigation_depth': 4,
}
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
master_doc = 'index'
+40
View File
@@ -0,0 +1,40 @@
galaxy.api
=================
plugin
------------------------
.. automodule:: galaxy.api.plugin
:members:
:undoc-members:
:exclude-members: JSONEncoder, features, achievements_import_finished, game_times_import_finished, start_achievements_import, start_game_times_import, get_game_times, get_unlocked_achievements
types
-----------------------
.. automodule:: galaxy.api.types
:members:
:undoc-members:
consts
------------------------
.. automodule:: galaxy.api.consts
:members:
:undoc-members:
:show-inheritance:
:exclude-members: Feature
errors
------------------------
.. autoexception:: galaxy.api.jsonrpc.ApplicationError
:show-inheritance:
.. autoexception:: galaxy.api.jsonrpc.UnknownError
:show-inheritance:
.. automodule:: galaxy.api.errors
:members:
:undoc-members:
:show-inheritance:
+8
View File
@@ -0,0 +1,8 @@
galaxy.http
=================
.. automodule:: galaxy.http
:members:
:special-members: __init__
:undoc-members:
:show-inheritance:
+16
View File
@@ -0,0 +1,16 @@
GOG Galaxy Integrations Python API
=================================================
.. toctree::
:maxdepth: 2
:includehidden:
Overview <overview>
galaxy.api
galaxy.http
Platform ID's <platforms>
Index
-------------------
* :ref:`genindex`
+15
View File
@@ -0,0 +1,15 @@
.. mdinclude:: ../../README.md
:end-line: 4
.. excluding self-pointing documentation link
.. mdinclude:: ../../README.md
:start-line: 6
:end-line: 26
.. excluding Platforms Id's link
:ref:`platforms-link`
.. mdinclude:: ../../README.md
:start-line: 28
+2
View File
@@ -0,0 +1,2 @@
.. _platforms-link:
.. mdinclude:: ../../PLATFORM_IDs.md
+2
View File
@@ -0,0 +1,2 @@
[mypy]
ignore_missing_imports = True
+1 -1
View File
@@ -1,2 +1,2 @@
[pytest]
addopts = --flakes
addopts = --flakes --mypy
+3 -1
View File
@@ -1,8 +1,10 @@
-e .
pytest==4.2.0
pytest==5.2.2
pytest-asyncio==0.10.0
pytest-mock==1.10.3
pytest-mypy==0.4.1
pytest-flakes==4.0.0
# because of pip bug https://github.com/pypa/pip/issues/4780
aiohttp==3.5.4
certifi==2019.3.9
psutil==5.6.3; sys_platform == 'darwin'
+5 -4
View File
@@ -2,14 +2,15 @@ from setuptools import setup, find_packages
setup(
name="galaxy.plugin.api",
version="0.31.3",
description="Galaxy python plugin API",
version="0.64",
description="GOG Galaxy Integrations Python API",
author='Galaxy team',
author_email='galaxy@gog.com',
packages=find_packages("src"),
package_dir={'': 'src'},
install_requires=[
"aiohttp==3.5.4",
"certifi==2019.3.9"
"aiohttp>=3.5.4",
"certifi>=2019.3.9",
"psutil>=5.6.3; sys_platform == 'darwin'"
]
)
+12
View File
@@ -0,0 +1,12 @@
version: 2
sphinx:
configuration: docs/source/conf.py
formats: all
python:
version: 3.7
install:
- requirements: requirements.txt
- requirements: docs/requirements.txt
+1 -1
View File
@@ -1 +1 @@
__path__ = __import__('pkgutil').extend_path(__path__, __name__)
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
+111 -1
View File
@@ -1,6 +1,8 @@
from enum import Enum, Flag
class Platform(Enum):
"""Supported gaming platforms"""
Unknown = "unknown"
Gog = "gog"
Steam = "steam"
@@ -11,8 +13,90 @@ 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"
Twitch = "twitch"
Minecraft = "minecraft"
GameSessions = "gamesessions"
Nuuvem = "nuuvem"
FXStore = "fxstore"
IndieGala = "indiegala"
Playfire = "playfire"
Oculus = "oculus"
Test = "test"
Rockstar = "rockstar"
class Feature(Enum):
"""Possible features that can be implemented by an integration.
It does not have to support all or any specific features from the list.
"""
Unknown = "Unknown"
ImportInstalledGames = "ImportInstalledGames"
ImportOwnedGames = "ImportOwnedGames"
@@ -25,20 +109,46 @@ class Feature(Enum):
ImportUsers = "ImportUsers"
VerifyGame = "VerifyGame"
ImportFriends = "ImportFriends"
ShutdownPlatformClient = "ShutdownPlatformClient"
LaunchPlatformClient = "LaunchPlatformClient"
ImportGameLibrarySettings = "ImportGameLibrarySettings"
ImportOSCompatibility = "ImportOSCompatibility"
ImportUserPresence = "ImportUserPresence"
ImportLocalSize = "ImportLocalSize"
ImportSubscriptions = "ImportSubscriptions"
ImportSubscriptionGames = "ImportSubscriptionGames"
class LicenseType(Enum):
"""Possible game license types, understandable for the GOG Galaxy client."""
Unknown = "Unknown"
SinglePurchase = "SinglePurchase"
FreeToPlay = "FreeToPlay"
OtherUserLicense = "OtherUserLicense"
class LocalGameState(Flag):
"""Possible states that a local game can be in.
For example a game which is both installed and currently running should have its state set as a "bitwise or" of Running and Installed flags:
``local_game_state=<LocalGameState.Running|Installed: 3>``
"""
None_ = 0
Installed = 1
Running = 2
class OSCompatibility(Flag):
"""Possible game OS compatibility.
Use "bitwise or" to express multiple OSs compatibility, e.g. ``os=OSCompatibility.Windows|OSCompatibility.MacOS``
"""
Windows = 0b001
MacOS = 0b010
Linux = 0b100
class PresenceState(Enum):
Unknown = "Unknown"
""""Possible states of a user."""
Unknown = "unknown"
Online = "online"
Offline = "offline"
Away = "away"
+6 -14
View File
@@ -1,6 +1,6 @@
from galaxy.api.jsonrpc import ApplicationError, UnknownError
UnknownError = UnknownError
assert UnknownError
class AuthenticationRequired(ApplicationError):
def __init__(self, data=None):
@@ -20,7 +20,11 @@ class BackendError(ApplicationError):
class UnknownBackendResponse(ApplicationError):
def __init__(self, data=None):
super().__init__(4, "Backend responded in uknown way", data)
super().__init__(4, "Backend responded in unknown way", data)
class TooManyRequests(ApplicationError):
def __init__(self, data=None):
super().__init__(5, "Too many requests. Try again later", data)
class InvalidCredentials(ApplicationError):
def __init__(self, data=None):
@@ -50,18 +54,6 @@ class AccessDenied(ApplicationError):
def __init__(self, data=None):
super().__init__(106, "Access denied", data)
class ParentalControlBlock(ApplicationError):
def __init__(self, data=None):
super().__init__(107, "Parental control block", data)
class DeviceBlocked(ApplicationError):
def __init__(self, data=None):
super().__init__(108, "Device blocked", data)
class RegionBlocked(ApplicationError):
def __init__(self, data=None):
super().__init__(109, "Region blocked", data)
class FailedParsingManifest(ApplicationError):
def __init__(self, data=None):
super().__init__(200, "Failed parsing manifest", data)
+89
View File
@@ -0,0 +1,89 @@
import asyncio
import logging
from galaxy.api.jsonrpc import ApplicationError
from galaxy.api.errors import ImportInProgress, UnknownError
logger = logging.getLogger(__name__)
class Importer:
def __init__(
self,
task_manger,
name,
get,
prepare_context,
notification_success,
notification_failure,
notification_finished,
complete,
):
self._task_manager = task_manger
self._name = name
self._get = get
self._prepare_context = prepare_context
self._notification_success = notification_success
self._notification_failure = notification_failure
self._notification_finished = notification_finished
self._complete = complete
self._import_in_progress = False
async def _import_element(self, id_, context_):
try:
element = await self._get(id_, context_)
self._notification_success(id_, element)
except ApplicationError as error:
self._notification_failure(id_, error)
except asyncio.CancelledError:
pass
except Exception:
logger.exception("Unexpected exception raised in %s importer", self._name)
self._notification_failure(id_, UnknownError())
async def _import_elements(self, ids_, context_):
try:
imports = [self._import_element(id_, context_) for id_ in ids_]
await asyncio.gather(*imports)
self._notification_finished()
self._complete()
except asyncio.CancelledError:
logger.debug("Importing %s cancelled", self._name)
finally:
self._import_in_progress = False
async def start(self, ids):
if self._import_in_progress:
raise ImportInProgress()
self._import_in_progress = True
try:
context = await self._prepare_context(ids)
self._task_manager.create_task(
self._import_elements(ids, context),
"{} import".format(self._name),
handle_exceptions=False
)
except:
self._import_in_progress = False
raise
class CollectionImporter(Importer):
def __init__(self, notification_partially_finished, *args):
super().__init__(*args)
self._notification_partially_finished = notification_partially_finished
async def _import_element(self, id_, context_):
try:
async for element in self._get(id_, context_):
self._notification_success(id_, element)
except ApplicationError as error:
self._notification_failure(id_, error)
except asyncio.CancelledError:
pass
except Exception:
logger.exception("Unexpected exception raised in %s importer", self._name)
self._notification_failure(id_, UnknownError())
finally:
self._notification_partially_finished(id_)
+179 -98
View File
@@ -5,6 +5,13 @@ import logging
import inspect
import json
from galaxy.reader import StreamLineReader
from galaxy.task_manager import TaskManager
logger = logging.getLogger(__name__)
class JsonRpcError(Exception):
def __init__(self, code, message, data=None):
self.code = code
@@ -15,6 +22,17 @@ class JsonRpcError(Exception):
def __eq__(self, other):
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["data"] = self.data
return obj
class ParseError(JsonRpcError):
def __init__(self):
super().__init__(-32700, "Parse error")
@@ -50,56 +68,98 @@ class UnknownError(ApplicationError):
super().__init__(0, "Unknown error", data)
Request = namedtuple("Request", ["method", "params", "id"], defaults=[{}, None])
Method = namedtuple("Method", ["callback", "signature", "internal", "sensitive_params"])
Response = namedtuple("Response", ["id", "result", "error"], defaults=[None, {}, {}])
Method = namedtuple("Method", ["callback", "signature", "immediate", "sensitive_params"])
def anonymise_sensitive_params(params, sensitive_params):
anomized_data = "****"
if not sensitive_params:
return params
if isinstance(sensitive_params, bool):
if sensitive_params:
return {k:anomized_data for k,v in params.items()}
if isinstance(sensitive_params, Iterable):
anomized_params = params.copy()
for key in anomized_params.keys():
if key in sensitive_params:
anomized_params[key] = anomized_data
return anomized_params
return {k: anomized_data if k in sensitive_params else v for k, v in params.items()}
return anomized_data
return params
class Server():
class Connection():
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 = {}
self._notifications = {}
self._eof_listeners = []
self._task_manager = TaskManager("jsonrpc server")
self._last_request_id = 0
self._requests_futures = {}
def register_method(self, name, callback, internal, sensitive_params=False):
def register_method(self, name, callback, immediate, sensitive_params=False):
"""
Register method
:param name:
:param callback:
:param internal: if True the callback will be processed immediately (synchronously)
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
are considered sensitive, if True - all params are considered sensitive
:param sensitive_params: list of parameters that are anonymized before logging; \
if False - no params are considered sensitive, if True - all params are considered sensitive
"""
self._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
:param name:
:param callback:
:param internal: if True the callback will be processed immediately (synchronously)
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
are considered sensitive, if True - all params are considered sensitive
:param sensitive_params: list of parameters that are anonymized before logging; \
if False - no params are considered sensitive, if True - all params are considered sensitive
"""
self._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 send_request(self, method, params, sensitive_params):
"""
Send request
:param method:
:param params:
:param sensitive_params: list of parameters that are anonymized before logging; \
if False - no params are considered sensitive, if True - all params are considered sensitive
"""
self._last_request_id += 1
request_id = str(self._last_request_id)
loop = asyncio.get_running_loop()
future = loop.create_future()
self._requests_futures[self._last_request_id] = (future, sensitive_params)
logger.info(
"Sending request: id=%s, method=%s, params=%s",
request_id, method, anonymise_sensitive_params(params, sensitive_params)
)
self._send_request(request_id, method, params)
return await future
def send_notification(self, method, params, sensitive_params=False):
"""
Send notification
:param method:
:param params:
:param sensitive_params: list of parameters that are anonymized before logging; \
if False - no params are considered sensitive, if True - all params are considered sensitive
"""
logger.info(
"Sending notification: method=%s, params=%s",
method, anonymise_sensitive_params(params, sensitive_params)
)
self._send_notification(method, params)
async def run(self):
while self._active:
@@ -112,37 +172,66 @@ class Server():
self._eof()
continue
data = data.strip()
logging.debug("Received %d bytes of data", len(data))
logger.debug("Received %d bytes of data", len(data))
self._handle_input(data)
await asyncio.sleep(0) # To not starve task queue
def stop(self):
def close(self):
if self._active:
logger.info("Closing JSON-RPC server - not more messages will be read")
self._active = False
async def wait_closed(self):
await self._task_manager.wait()
def _eof(self):
logging.info("Received EOF")
self.stop()
for listener in self._eof_listeners:
listener()
logger.info("Received EOF")
self.close()
def _handle_input(self, data):
try:
request = self._parse_request(data)
message = self._parse_message(data)
except JsonRpcError as error:
self._send_error(None, error)
return
if request.id is not None:
self._handle_request(request)
if isinstance(message, Request):
if message.id is not None:
self._handle_request(message)
else:
self._handle_notification(request)
self._handle_notification(message)
elif isinstance(message, Response):
self._handle_response(message)
def _handle_response(self, response):
request_future = self._requests_futures.get(int(response.id))
if request_future is None:
response_type = "response" if response.result is not None else "error"
logger.warning("Received %s for unknown request: %s", response_type, response.id)
return
future, sensitive_params = request_future
if response.error:
error = JsonRpcError(
response.error.setdefault("code", 0),
response.error.setdefault("message", ""),
response.error.setdefault("data", None)
)
self._log_error(response, error, sensitive_params)
future.set_exception(error)
return
self._log_response(response, sensitive_params)
future.set_result(response.result)
def _handle_notification(self, request):
method = self._notifications.get(request.method)
if not method:
logging.error("Received unknown notification: %s", request.method)
logger.error("Received unknown notification: %s", request.method)
return
callback, signature, internal, sensitive_params = method
callback, signature, immediate, sensitive_params = method
self._log_request(request, sensitive_params)
try:
@@ -150,23 +239,22 @@ class Server():
except TypeError:
self._send_error(request.id, InvalidParams())
if internal:
# internal requests are handled immediately
if immediate:
callback(*bound_args.args, **bound_args.kwargs)
else:
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:
logging.exception("Unexpected exception raised in notification handler")
logger.exception("Unexpected exception raised in notification handler")
def _handle_request(self, request):
method = self._methods.get(request.method)
if not method:
logging.error("Received unknown request: %s", request.method)
logger.error("Received unknown request: %s", request.method)
self._send_error(request.id, MethodNotFound())
return
callback, signature, internal, sensitive_params = method
callback, signature, immediate, sensitive_params = method
self._log_request(request, sensitive_params)
try:
@@ -174,8 +262,7 @@ class Server():
except TypeError:
self._send_error(request.id, InvalidParams())
if internal:
# internal requests are handled immediately
if immediate:
response = callback(*bound_args.args, **bound_args.kwargs)
self._send_response(request.id, response)
else:
@@ -187,34 +274,42 @@ class Server():
self._send_error(request.id, MethodNotFound())
except JsonRpcError as error:
self._send_error(request.id, error)
except asyncio.CancelledError:
self._send_error(request.id, Aborted())
except Exception as e: #pylint: disable=broad-except
logging.exception("Unexpected exception raised in plugin handler")
logger.exception("Unexpected exception raised in plugin handler")
self._send_error(request.id, UnknownError(str(e)))
asyncio.create_task(handle())
self._task_manager.create_task(handle(), request.method)
@staticmethod
def _parse_request(data):
def _parse_message(data):
try:
jsonrpc_request = json.loads(data, encoding="utf-8")
if jsonrpc_request.get("jsonrpc") != "2.0":
jsonrpc_message = json.loads(data, encoding="utf-8")
if jsonrpc_message.get("jsonrpc") != "2.0":
raise InvalidRequest()
del jsonrpc_request["jsonrpc"]
return Request(**jsonrpc_request)
del jsonrpc_message["jsonrpc"]
if "result" in jsonrpc_message.keys() or "error" in jsonrpc_message.keys():
return Response(**jsonrpc_message)
else:
return Request(**jsonrpc_message)
except json.JSONDecodeError:
raise ParseError()
except TypeError:
raise InvalidRequest()
def _send(self, data):
def _send(self, data, sensitive=True):
try:
line = self._encoder.encode(data)
logging.debug("Sending data: %s", line)
data = (line + "\n").encode("utf-8")
if sensitive:
logger.debug("Sending %d bytes of data", len(data))
else:
logging.debug("Sending data: %s", line)
self._writer.write(data)
asyncio.create_task(self._writer.drain())
except TypeError as error:
logging.error(str(error))
logger.error(str(error))
def _send_response(self, request_id, result):
response = {
@@ -222,64 +317,50 @@ class Server():
"id": request_id,
"result": result
}
self._send(response)
self._send(response, sensitive=False)
def _send_error(self, request_id, error):
response = {
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": error.code,
"message": error.message
}
"error": error.json()
}
if error.data is not None:
response["error"]["data"] = error.data
self._send(response, sensitive=False)
self._send(response)
def _send_request(self, request_id, method, params):
request = {
"jsonrpc": "2.0",
"method": method,
"id": request_id,
"params": params
}
self._send(request, sensitive=True)
@staticmethod
def _log_request(request, sensitive_params):
params = anonymise_sensitive_params(request.params, sensitive_params)
if request.id is not None:
logging.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
else:
logging.info("Handling notification: method=%s, params=%s", request.method, params)
class NotificationClient():
def __init__(self, writer, encoder=json.JSONEncoder()):
self._writer = writer
self._encoder = encoder
self._methods = {}
def notify(self, method, params, sensitive_params=False):
"""
Send notification
:param method:
:param params:
:param sensitive_params: list of parameters that will by anonymized before logging; if False - no params
are considered sensitive, if True - all params are considered sensitive
"""
def _send_notification(self, method, params):
notification = {
"jsonrpc": "2.0",
"method": method,
"params": params
}
self._log(method, params, sensitive_params)
self._send(notification)
def _send(self, data):
try:
line = self._encoder.encode(data)
data = (line + "\n").encode("utf-8")
logging.debug("Sending %d byte of data", len(data))
self._writer.write(data)
asyncio.create_task(self._writer.drain())
except TypeError as error:
logging.error("Failed to parse outgoing message: %s", str(error))
self._send(notification, sensitive=True)
@staticmethod
def _log(method, params, sensitive_params):
params = anonymise_sensitive_params(params, sensitive_params)
logging.info("Sending notification: method=%s, params=%s", method, params)
def _log_request(request, sensitive_params):
params = anonymise_sensitive_params(request.params, sensitive_params)
if request.id is not None:
logger.info("Handling request: id=%s, method=%s, params=%s", request.id, request.method, params)
else:
logger.info("Handling notification: method=%s, params=%s", request.method, params)
@staticmethod
def _log_response(response, sensitive_params):
result = anonymise_sensitive_params(response.result, sensitive_params)
logger.info("Handling response: id=%s, result=%s", response.id, result)
@staticmethod
def _log_error(response, error, sensitive_params):
data = anonymise_sensitive_params(error.data, sensitive_params)
logger.info("Handling error: id=%s, code=%s, description=%s, data=%s",
response.id, error.code, error.message, data
)
+940 -259
View File
File diff suppressed because it is too large. Load diff
+188 -37
View File
@@ -1,47 +1,125 @@
from dataclasses import dataclass
from typing import List, Dict, Optional
from typing import Dict, List, Optional
from galaxy.api.consts import LicenseType, LocalGameState, PresenceState
@dataclass
class Authentication():
class Authentication:
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials`
to inform the client that authentication has successfully finished.
:param user_id: id of the authenticated user
:param user_name: username of the authenticated user
"""
user_id: str
user_name: str
@dataclass
class Cookie():
class Cookie:
"""Cookie
:param name: name of the cookie
:param value: value of the cookie
:param domain: optional domain of the cookie
:param path: optional path of the cookie
"""
name: str
value: str
domain: Optional[str] = None
path: Optional[str] = None
@dataclass
class NextStep():
class NextStep:
"""Return this from :meth:`.authenticate` or :meth:`.pass_login_credentials` to open client built-in browser with given url.
For example:
.. code-block:: python
:linenos:
PARAMS = {
"window_title": "Login to platform",
"window_width": 800,
"window_height": 600,
"start_uri": URL,
"end_uri_regex": r"^https://platform_website\.com/.*"
}
JS = {r"^https://platform_website\.com/.*": [
r'''
location.reload();
'''
]}
COOKIES = [Cookie("Cookie1", "ok", ".platform.com"),
Cookie("Cookie2", "ok", ".platform.com")
]
async def authenticate(self, stored_credentials=None):
if not stored_credentials:
return NextStep("web_session", PARAMS, cookies=COOKIES, js=JS)
:param auth_params: configuration options: {"window_title": :class:`str`, "window_width": :class:`str`,
"window_height": :class:`int`, "start_uri": :class:`int`, "end_uri_regex": :class:`str`}
:param 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.
"""
next_step: str
auth_params: Dict[str, str]
cookies: Optional[List[Cookie]] = None
js: Optional[Dict[str, List[str]]] = None
@dataclass
class LicenseInfo():
class LicenseInfo:
"""Information about the license of related product.
:param license_type: type of license
:param owner: optional owner of the related product, defaults to currently authenticated user
"""
license_type: LicenseType
owner: Optional[str] = None
@dataclass
class Dlc():
class Dlc:
"""Downloadable content object.
:param dlc_id: id of the dlc
:param dlc_title: title of the dlc
:param license_info: information about the license attached to the dlc
"""
dlc_id: str
dlc_title: str
license_info: LicenseInfo
@dataclass
class Game():
class Game:
"""Game object.
:param game_id: unique identifier of the game, this will be passed as parameter for methods such as launch_game
:param game_title: title of the game
:param dlcs: list of dlcs available for the game
:param license_info: information about the license attached to the game
"""
game_id: str
game_title: str
dlcs: Optional[List[Dlc]]
license_info: LicenseInfo
@dataclass
class Achievement():
class Achievement:
"""Achievement, has to be initialized with either id or name.
:param unlock_time: unlock time of the achievement
:param achievement_id: optional id of the achievement
:param achievement_name: optional name of the achievement
"""
unlock_time: int
achievement_id: Optional[str] = None
achievement_name: Optional[str] = None
@@ -50,45 +128,118 @@ class Achievement():
assert self.achievement_id or self.achievement_name, \
"One of achievement_id or achievement_name is required"
@dataclass
class LocalGame():
class LocalGame:
"""Game locally present on the authenticated user's computer.
:param game_id: id of the game
:param local_game_state: state of the game
"""
game_id: str
local_game_state: LocalGameState
@dataclass
class Presence():
class FriendInfo:
"""
.. deprecated:: 0.56
Use :class:`UserInfo`.
Information about a friend of the currently authenticated user.
:param user_id: id of the user
:param user_name: username of the user
"""
user_id: str
user_name: str
@dataclass
class UserInfo:
"""Information about a user of related user.
:param user_id: id of the user
:param user_name: username of the user
:param avatar_url: the URL of the user avatar
:param profile_url: the URL of the user profile
"""
user_id: str
user_name: str
avatar_url: Optional[str]
profile_url: Optional[str]
@dataclass
class GameTime:
"""Game time of a game, defines the total time spent in the game
and the last time the game was played.
:param game_id: id of the related game
:param time_played: the total time spent in the game in **minutes**
:param last_played_time: last time the game was played (**unix timestamp**)
"""
game_id: str
time_played: Optional[int]
last_played_time: Optional[int]
@dataclass
class GameLibrarySettings:
"""Library settings of a game, defines assigned tags and visibility flag.
:param game_id: id of the related game
:param tags: collection of tags assigned to the game
:param hidden: indicates if the game should be hidden in GOG Galaxy client
"""
game_id: str
tags: Optional[List[str]]
hidden: Optional[bool]
@dataclass
class UserPresence:
"""Presence information of a user.
The GOG Galaxy client will prefer to generate user status basing on `game_id` (or `game_title`)
and `in_game_status` fields but if plugin is not capable of delivering it then the `full_status` will be used if
available
:param presence_state: the state of the user
:param game_id: id of the game a user is currently in
:param game_title: name of the game a user is currently in
:param in_game_status: status set by the game itself e.x. "In Main Menu"
:param full_status: full user status e.x. "Playing <title_name>: <in_game_status>"
"""
presence_state: PresenceState
game_id: Optional[str] = None
presence_status: Optional[str] = None
game_title: Optional[str] = None
in_game_status: Optional[str] = None
full_status: Optional[str] = None
@dataclass
class UserInfo():
user_id: str
is_friend: bool
user_name: str
avatar_url: str
presence: Presence
class Subscription:
"""Information about a subscription.
:param subscription_name: name of the subscription, will also be used as its identifier.
:param owned: whether the subscription is owned or not, None if unknown.
:param end_time: unix timestamp of when the subscription ends, None if unknown.
"""
subscription_name: str
owned: Optional[bool] = None
end_time: Optional[int] = None
@dataclass
class FriendInfo():
user_id: str
user_name: str
class SubscriptionGame:
"""Information about a game from a subscription.
@dataclass
class Room():
room_id: str
unread_message_count: int
last_message_id: str
@dataclass
class Message():
message_id: str
sender_id: str
sent_time: int
message_text: str
@dataclass
class GameTime():
:param game_title: title of the game
:param game_id: id of the game
:param start_time: unix timestamp of when the game has been added to subscription
:param end_time: unix timestamp of when the game will be removed from subscription.
"""
game_title: str
game_id: str
time_played: int
last_played_time: int
start_time: Optional[int] = None
end_time: Optional[int] = None
+126 -25
View File
@@ -1,47 +1,148 @@
"""
This module standardizes http traffic and the error handling for further communication with the GOG Galaxy 2.0.
It is recommended to use provided convenient methods for HTTP requests, especially when dealing with authorized sessions.
Exemplary 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 ssl
from contextlib import contextmanager
from http import HTTPStatus
import aiohttp
import certifi
import logging
from galaxy.api.errors import (
AccessDenied, AuthenticationRequired,
BackendTimeout, BackendNotAvailable, BackendError, NetworkError, UnknownBackendResponse, UnknownError
AccessDenied, AuthenticationRequired, BackendTimeout, BackendNotAvailable, BackendError, NetworkError,
TooManyRequests, UnknownBackendResponse, UnknownError
)
logger = logging.getLogger(__name__)
#: 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:
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:: 0.41
Use http module functions instead
"""
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):
"""Closes connection. Should be called in :meth:`~galaxy.api.plugin.Plugin.shutdown`"""
await self._session.close()
async def request(self, method, *args, **kwargs):
async def request(self, method, url, *args, **kwargs):
with handle_exception():
return await self._session.request(method, url, *args, **kwargs)
def create_tcp_connector(*args, **kwargs) -> aiohttp.TCPConnector:
"""
Creates TCP connector with reasonable 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)
# due to https://github.com/python/mypy/issues/4001
return aiohttp.TCPConnector(*args, **kwargs) # type: ignore
def create_client_session(*args, **kwargs) -> aiohttp.ClientSession:
"""
Creates client session with reasonable defaults.
For details about available parameters refer to
`aiohttp.ClientSession <https://docs.aiohttp.org/en/stable/client_reference.html>`_
Exemplary 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)
# due to https://github.com/python/mypy/issues/4001
return aiohttp.ClientSession(*args, **kwargs) # type: ignore
@contextmanager
def handle_exception():
"""
Context manager translating network related exceptions
to custom :mod:`~galaxy.api.errors`.
"""
try:
response = await self._session.request(method, *args, **kwargs)
yield
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 >= 500:
raise BackendError()
if response.status >= 400:
raise UnknownError()
return response
except aiohttp.ContentTypeError as error:
raise UnknownBackendResponse(error.message)
except aiohttp.ClientResponseError as error:
if error.status == HTTPStatus.UNAUTHORIZED:
raise AuthenticationRequired(error.message)
if error.status == HTTPStatus.FORBIDDEN:
raise AccessDenied(error.message)
if error.status == HTTPStatus.SERVICE_UNAVAILABLE:
raise BackendNotAvailable(error.message)
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
raise TooManyRequests(error.message)
if error.status >= 500:
raise BackendError(error.message)
if error.status >= 400:
logger.warning(
"Got status %d while performing %s request for %s",
error.status, error.request_info.method, str(error.request_info.url)
)
raise UnknownError(error.message)
except aiohttp.ClientError as e:
logger.exception("Caught exception while performing request")
raise UnknownError(repr(e))
+87
View File
@@ -0,0 +1,87 @@
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
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*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
+99
View File
@@ -0,0 +1,99 @@
import sys
if sys.platform == "win32":
import logging
import ctypes
from ctypes.wintypes import LONG, HKEY, LPCWSTR, DWORD, BOOL, HANDLE, LPVOID
LPSECURITY_ATTRIBUTES = LPVOID
RegOpenKeyEx = ctypes.windll.advapi32.RegOpenKeyExW
RegOpenKeyEx.restype = LONG
RegOpenKeyEx.argtypes = [HKEY, LPCWSTR, DWORD, DWORD, ctypes.POINTER(HKEY)]
RegCloseKey = ctypes.windll.advapi32.RegCloseKey
RegCloseKey.restype = LONG
RegCloseKey.argtypes = [HKEY]
RegNotifyChangeKeyValue = ctypes.windll.advapi32.RegNotifyChangeKeyValue
RegNotifyChangeKeyValue.restype = LONG
RegNotifyChangeKeyValue.argtypes = [HKEY, BOOL, DWORD, HANDLE, BOOL]
CloseHandle = ctypes.windll.kernel32.CloseHandle
CloseHandle.restype = BOOL
CloseHandle.argtypes = [HANDLE]
CreateEvent = ctypes.windll.kernel32.CreateEventW
CreateEvent.restype = BOOL
CreateEvent.argtypes = [LPSECURITY_ATTRIBUTES, BOOL, BOOL, LPCWSTR]
WaitForSingleObject = ctypes.windll.kernel32.WaitForSingleObject
WaitForSingleObject.restype = DWORD
WaitForSingleObject.argtypes = [HANDLE, DWORD]
ERROR_SUCCESS = 0x00000000
KEY_READ = 0x00020019
KEY_QUERY_VALUE = 0x00000001
REG_NOTIFY_CHANGE_NAME = 0x00000001
REG_NOTIFY_CHANGE_LAST_SET = 0x00000004
WAIT_OBJECT_0 = 0x00000000
WAIT_TIMEOUT = 0x00000102
class RegistryMonitor:
def __init__(self, root, subkey):
self._root = root
self._subkey = subkey
self._event = CreateEvent(None, False, False, None)
self._key = None
self._open_key()
if self._key:
self._set_key_update_notification()
def close(self):
CloseHandle(self._event)
if self._key:
RegCloseKey(self._key)
self._key = None
def is_updated(self):
wait_result = WaitForSingleObject(self._event, 0)
# previously watched
if wait_result == WAIT_OBJECT_0:
self._set_key_update_notification()
return True
# no changes or no key before
if wait_result != WAIT_TIMEOUT:
# unexpected error
logging.warning("Unexpected WaitForSingleObject result %s", wait_result)
return False
if self._key is None:
self._open_key()
if self._key is not None:
self._set_key_update_notification()
return False
def _set_key_update_notification(self):
filter_ = REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET
status = RegNotifyChangeKeyValue(self._key, True, filter_, self._event, True)
if status != ERROR_SUCCESS:
# key was deleted
RegCloseKey(self._key)
self._key = None
def _open_key(self):
access = KEY_QUERY_VALUE | KEY_READ
self._key = HKEY()
rc = RegOpenKeyEx(self._root, self._subkey, 0, access, ctypes.byref(self._key))
if rc != ERROR_SUCCESS:
self._key = None
+53
View File
@@ -0,0 +1,53 @@
import asyncio
import logging
from collections import OrderedDict
from itertools import count
logger = logging.getLogger(__name__)
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
logger.debug("Task manager %s: finished task %d (%s)", self._name, task_id, description)
return result
except asyncio.CancelledError:
if handle_exceptions:
logger.debug("Task manager %s: canceled task %d (%s)", self._name, task_id, description)
else:
raise
except Exception:
if handle_exceptions:
logger.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)
logger.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)
+2
View File
@@ -3,6 +3,7 @@ import os
import zipfile
from glob import glob
def zip_folder(folder):
files = glob(os.path.join(folder, "**"), recursive=True)
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)
return zip_buffer
def zip_folder_to_file(folder, filename):
zip_content = zip_folder(folder).getbuffer()
with open(filename, "wb") as archive:
+29 -2
View File
@@ -1,12 +1,39 @@
from asyncio import coroutine
import asyncio
from unittest.mock import MagicMock
class AsyncMock(MagicMock):
"""
.. deprecated:: 0.45
Use: :class:`MagicMock` with meth:`~.async_return_value`.
"""
async def __call__(self, *args, **kwargs):
return super(AsyncMock, self).__call__(*args, **kwargs)
def coroutine_mock():
"""
.. deprecated:: 0.45
Use: :class:`MagicMock` with meth:`~.async_return_value`.
"""
coro = MagicMock(name="CoroutineResult")
corofunc = MagicMock(name="CoroutineFunction", side_effect=coroutine(coro))
corofunc = MagicMock(name="CoroutineFunction", side_effect=asyncio.coroutine(coro))
corofunc.coro = coro
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):
if loop_iterations_delay > 0:
await skip_loop(loop_iterations_delay)
return return_value
async def async_raise(error, loop_iterations_delay=0):
if loop_iterations_delay > 0:
await skip_loop(loop_iterations_delay)
raise error
+16
View File
@@ -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
+46 -26
View File
@@ -1,66 +1,86 @@
from contextlib import ExitStack
import logging
from unittest.mock import patch, MagicMock
from contextlib import ExitStack
from unittest.mock import MagicMock, patch
import pytest
from galaxy.api.plugin import Plugin
from galaxy.api.consts import Platform
from galaxy.unittest.mock import AsyncMock, coroutine_mock
from galaxy.api.plugin import Plugin
from galaxy.unittest.mock import async_return_value
@pytest.fixture()
def reader():
stream = MagicMock(name="stream_reader")
stream.readline = AsyncMock()
stream.read = MagicMock()
yield stream
@pytest.fixture()
def writer():
async def writer():
stream = MagicMock(name="stream_writer")
stream.write = MagicMock()
stream.drain = AsyncMock()
stream.drain.side_effect = lambda: async_return_value(None)
yield stream
@pytest.fixture()
def readline(reader):
yield reader.readline
def read(reader):
yield reader.read
@pytest.fixture()
def write(writer):
yield writer.write
@pytest.fixture()
def plugin(reader, writer):
async def plugin(reader, writer):
"""Return plugin instance with all feature methods mocked"""
async_methods = (
methods = (
"handshake_complete",
"authenticate",
"get_owned_games",
"prepare_achievements_context",
"get_unlocked_achievements",
"achievements_import_complete",
"get_local_games",
"launch_game",
"launch_platform_client",
"install_game",
"uninstall_game",
"get_friends",
"get_users",
"send_message",
"mark_as_read",
"get_rooms",
"get_room_history_from_message",
"get_room_history_from_timestamp",
"get_game_times"
)
methods = (
"get_game_time",
"prepare_game_times_context",
"game_times_import_complete",
"shutdown_platform_client",
"shutdown",
"tick"
"tick",
"get_game_library_settings",
"prepare_game_library_settings_context",
"game_library_settings_import_complete",
"get_os_compatibility",
"prepare_os_compatibility_context",
"os_compatibility_import_complete",
"get_user_presence",
"prepare_user_presence_context",
"user_presence_import_complete",
"get_local_size",
"prepare_local_size_context",
"local_size_import_complete",
"get_subscriptions",
"get_subscription_games",
"prepare_subscription_games_context",
"subscription_games_import_complete"
)
with ExitStack() as stack:
for method in async_methods:
stack.enter_context(patch.object(Plugin, method, new_callable=coroutine_mock))
for method in methods:
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)
def my_caplog(caplog):
+142 -118
View File
@@ -1,44 +1,58 @@
import asyncio
import json
from unittest.mock import call
import pytest
from pytest import raises
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, skip_loop
from tests import create_message, get_messages
def test_initialization_no_unlock_time():
with raises(Exception):
Achievement(achievement_id="lvl30", achievement_name="Got level 30")
def test_initialization_no_id_nor_name():
with raises(AssertionError):
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 = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_unlocked_achievements",
"method": "start_achievements_import",
"params": {
"game_id": "14"
"game_ids": ["14"]
}
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_unlocked_achievements.coro.return_value = [
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_unlocked_achievements.return_value = async_return_value([
Achievement(achievement_id="lvl10", unlock_time=1548421241),
Achievement(achievement_name="Got level 20", unlock_time=1548422395),
Achievement(achievement_id="lvl30", achievement_name="Got level 30", unlock_time=1548495633)
]
asyncio.run(plugin.run())
plugin.get_unlocked_achievements.assert_called_with(game_id="14")
response = json.loads(write.call_args[0][0])
])
await plugin.run()
plugin.prepare_achievements_context.assert_called_with(["14"])
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",
"result": {
"result": None
},
{
"jsonrpc": "2.0",
"method": "game_achievements_import_success",
"params": {
"game_id": "14",
"unlocked_achievements": [
{
"achievement_id": "lvl10",
@@ -55,40 +69,139 @@ def test_success(plugin, readline, write):
}
]
}
},
{
"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 = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_unlocked_achievements",
"method": "start_achievements_import",
"params": {
"game_id": "14"
"game_ids": ["14"]
}
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_unlocked_achievements.coro.side_effect = UnknownError()
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_unlocked_achievements.side_effect = exception
await plugin.run()
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",
"id": "3",
"method": "start_achievements_import",
"params": {
"game_ids": ["14"]
}
}
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": 0,
"message": "Unknown error"
"code": 4,
"message": "Backend error"
}
}
]
def test_unlock_achievement(plugin, write):
@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)
async def couritine():
plugin.unlock_achievement("14", achievement)
asyncio.run(couritine())
await skip_loop()
response = json.loads(write.call_args[0][0])
assert response == {
@@ -102,92 +215,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()
+41 -38
View File
@@ -1,29 +1,29 @@
import asyncio
import json
import pytest
from galaxy.api.types import Authentication
from galaxy.api.errors import (
UnknownError, InvalidCredentials, NetworkError, LoggedInElsewhere, ProtocolError,
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied,
ParentalControlBlock, DeviceBlocked, RegionBlocked
BackendNotAvailable, BackendTimeout, BackendError, TemporaryBlocked, Banned, AccessDenied
)
from galaxy.unittest.mock import async_return_value, skip_loop
def test_success(plugin, readline, write):
from tests import create_message, get_messages
@pytest.mark.asyncio
async def test_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "init_authentication"
}
readline.side_effect = [json.dumps(request), ""]
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
await plugin.run()
plugin.authenticate.assert_called_with()
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"result": {
@@ -31,7 +31,10 @@ def test_success(plugin, readline, write):
"user_name": "Zenek"
}
}
]
@pytest.mark.asyncio
@pytest.mark.parametrize("error,code,message", [
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
pytest.param(BackendNotAvailable, 2, "Backend not available", id="backend_not_available"),
@@ -44,24 +47,21 @@ def test_success(plugin, readline, write):
pytest.param(TemporaryBlocked, 104, "Temporary blocked", id="temporary_blocked"),
pytest.param(Banned, 105, "Banned", id="banned"),
pytest.param(AccessDenied, 106, "Access denied", id="access_denied"),
pytest.param(ParentalControlBlock, 107, "Parental control block", id="parental_control_clock"),
pytest.param(DeviceBlocked, 108, "Device blocked", id="device_blocked"),
pytest.param(RegionBlocked, 109, "Region blocked", id="region_blocked")
])
def test_failure(plugin, readline, write, error, code, message):
async def test_failure(plugin, read, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "init_authentication"
}
readline.side_effect = [json.dumps(request), ""]
plugin.authenticate.coro.side_effect = error()
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.authenticate.side_effect = error()
await plugin.run()
plugin.authenticate.assert_called_with()
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"error": {
@@ -69,8 +69,11 @@ def test_failure(plugin, readline, write, error, code, message):
"message": message
}
}
]
def test_stored_credentials(plugin, readline, write):
@pytest.mark.asyncio
async def test_stored_credentials(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
@@ -81,39 +84,39 @@ def test_stored_credentials(plugin, readline, write):
}
}
}
readline.side_effect = [json.dumps(request), ""]
plugin.authenticate.coro.return_value = Authentication("132", "Zenek")
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.authenticate.return_value = async_return_value(Authentication("132", "Zenek"))
await plugin.run()
plugin.authenticate.assert_called_with(stored_credentials={"token": "ABC"})
write.assert_called()
def test_store_credentials(plugin, write):
@pytest.mark.asyncio
async def test_store_credentials(plugin, write):
credentials = {
"token": "ABC"
}
async def couritine():
plugin.store_credentials(credentials)
await skip_loop()
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "store_credentials",
"params": credentials
}
]
def test_lost_authentication(plugin, readline, write):
async def couritine():
@pytest.mark.asyncio
async def test_lost_authentication(plugin, write):
plugin.lost_authentication()
await skip_loop()
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "authentication_lost",
"params": None
}
]
-354
View File
@@ -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
View 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()
+30 -19
View File
@@ -1,9 +1,28 @@
from galaxy.api.consts import Feature, Platform
from galaxy.api.plugin import Plugin
from galaxy.api.consts import Platform, Feature
def test_base_class():
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,
Feature.ImportOSCompatibility,
Feature.ImportUserPresence,
Feature.ImportLocalSize,
Feature.ImportSubscriptions,
Feature.ImportSubscriptionGames
}
def test_no_overloads():
class PluginImpl(Plugin): # pylint: disable=abstract-method
@@ -12,6 +31,7 @@ def test_no_overloads():
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
assert plugin.features == []
def test_one_method_feature():
class PluginImpl(Plugin): # pylint: disable=abstract-method
async def get_owned_games(self):
@@ -20,26 +40,17 @@ def test_one_method_feature():
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
assert plugin.features == [Feature.ImportOwnedGames]
def test_multiple_methods_feature_all():
def test_multi_features():
class PluginImpl(Plugin): # pylint: disable=abstract-method
async def send_message(self, room_id, message):
async def get_owned_games(self):
pass
async def mark_as_read(self, room_id, last_message_id):
async def get_unlocked_achievements(self, game_id, context):
pass
async def get_rooms(self):
pass
async def get_room_history_from_message(self, room_id, message_id):
pass
async def get_room_history_from_timestamp(self, room_id, timestamp):
async def get_game_time(self, game_id, context):
pass
plugin = PluginImpl(Platform.Generic, "0.1", None, None, None)
assert plugin.features == [Feature.Chat]
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 == []
assert set(plugin.features) == {Feature.ImportAchievements, Feature.ImportOwnedGames, Feature.ImportGameTime}
+69 -35
View File
@@ -1,52 +1,57 @@
import asyncio
import json
from galaxy.api.types import FriendInfo
from galaxy.api.types import UserInfo
from galaxy.api.errors import UnknownError
from galaxy.unittest.mock import async_return_value, skip_loop
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 = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_friends"
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_friends.coro.return_value = [
FriendInfo("3", "Jan"),
FriendInfo("5", "Ola")
]
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_friends.return_value = async_return_value([
UserInfo("3", "Jan", "https://avatar.url/u3", None),
UserInfo("5", "Ola", None, "https://profile.url/u5")
])
await plugin.run()
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",
"result": {
"friend_info_list": [
{"user_id": "3", "user_name": "Jan"},
{"user_id": "5", "user_name": "Ola"}
{"user_id": "3", "user_name": "Jan", "avatar_url": "https://avatar.url/u3"},
{"user_id": "5", "user_name": "Ola", "profile_url": "https://profile.url/u5"}
]
}
}
]
def test_get_friends_failure(plugin, readline, write):
@pytest.mark.asyncio
async def test_get_friends_failure(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_friends"
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_friends.coro.side_effect = UnknownError()
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_friends.side_effect = UnknownError()
await plugin.run()
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",
"error": {
@@ -54,37 +59,66 @@ def test_get_friends_failure(plugin, readline, write):
"message": "Unknown error",
}
}
]
def test_add_friend(plugin, write):
friend = FriendInfo("7", "Kuba")
@pytest.mark.asyncio
async def test_add_friend(plugin, write):
friend = UserInfo("7", "Kuba", avatar_url="https://avatar.url/kuba.jpg", profile_url="https://profile.url/kuba")
async def couritine():
plugin.add_friend(friend)
await skip_loop()
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "friend_added",
"params": {
"friend_info": {"user_id": "7", "user_name": "Kuba"}
"friend_info": {
"user_id": "7",
"user_name": "Kuba",
"avatar_url": "https://avatar.url/kuba.jpg",
"profile_url": "https://profile.url/kuba"
}
}
}
]
def test_remove_friend(plugin, write):
async def couritine():
@pytest.mark.asyncio
async def test_remove_friend(plugin, write):
plugin.remove_friend("5")
await skip_loop()
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "friend_removed",
"params": {
"user_id": "5"
}
}
]
@pytest.mark.asyncio
async def test_update_friend_info(plugin, write):
plugin.update_friend_info(
UserInfo("7", "Jakub", avatar_url="https://new-avatar.url/kuba2.jpg", profile_url="https://profile.url/kuba")
)
await skip_loop()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "friend_updated",
"params": {
"friend_info": {
"user_id": "7",
"user_name": "Jakub",
"avatar_url": "https://new-avatar.url/kuba2.jpg",
"profile_url": "https://profile.url/kuba"
}
}
}
]
+196
View 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_library_settings_success(plugin, read, write):
plugin.prepare_game_library_settings_context.return_value = async_return_value("abc")
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "start_game_library_settings_import",
"params": {
"game_ids": ["3", "5", "7"]
}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_game_library_settings.side_effect = [
async_return_value(GameLibrarySettings("3", None, True)),
async_return_value(GameLibrarySettings("5", [], False)),
async_return_value(GameLibrarySettings("7", ["tag1", "tag2", "tag3"], None)),
]
await plugin.run()
plugin.get_game_library_settings.assert_has_calls([
call("3", "abc"),
call("5", "abc"),
call("7", "abc"),
])
plugin.game_library_settings_import_complete.assert_called_once_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"result": None
},
{
"jsonrpc": "2.0",
"method": "game_library_settings_import_success",
"params": {
"game_library_settings": {
"game_id": "3",
"hidden": True
}
}
},
{
"jsonrpc": "2.0",
"method": "game_library_settings_import_success",
"params": {
"game_library_settings": {
"game_id": "5",
"tags": [],
"hidden": False
}
}
},
{
"jsonrpc": "2.0",
"method": "game_library_settings_import_success",
"params": {
"game_library_settings": {
"game_id": "7",
"tags": ["tag1", "tag2", "tag3"]
}
}
},
{
"jsonrpc": "2.0",
"method": "game_library_settings_import_finished",
"params": None
}
]
@pytest.mark.asyncio
@pytest.mark.parametrize("exception,code,message", [
(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
+170 -129
View File
@@ -1,78 +1,208 @@
import asyncio
import json
from unittest.mock import call
import pytest
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, skip_loop
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 = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_game_times"
"method": "start_game_times_import",
"params": {
"game_ids": ["3", "5", "7"]
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_game_times.coro.return_value = [
GameTime("3", 60, 1549550504),
GameTime("5", 10, 1549550502)
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_game_time.side_effect = [
async_return_value(GameTime("3", 60, 1549550504)),
async_return_value(GameTime("5", 10, None)),
async_return_value(GameTime("7", None, 1549550502)),
]
asyncio.run(plugin.run())
plugin.get_game_times.assert_called_with()
response = json.loads(write.call_args[0][0])
await plugin.run()
plugin.get_game_time.assert_has_calls([
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": {
"game_times": [
{
"game_id": "3",
"time_played": 60,
"last_played_time": 1549550504
"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,
"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
}
]
}
}
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_game_time_error(exception, code, message, plugin, read, write):
plugin.prepare_game_times_context.return_value = async_return_value(None)
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_game_times"
"method": "start_game_times_import",
"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()
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 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
}
]
assert response == {
@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": 0,
"message": "Unknown error",
"code": 4,
"message": "Backend error"
}
}
]
def test_update_game(plugin, write):
@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)
async def couritine():
plugin.update_game_time(game_time)
await skip_loop()
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "game_time_updated",
"params": {
@@ -83,93 +213,4 @@ def test_update_game(plugin, write):
}
}
}
@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
View 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
+10 -6
View File
@@ -1,7 +1,12 @@
import asyncio
import json
import pytest
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 = {
"jsonrpc": "2.0",
"method": "install_game",
@@ -10,7 +15,6 @@ def test_success(plugin, readline):
}
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_owned_games.return_value = None
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
await plugin.run()
plugin.install_game.assert_called_with(game_id="3")
+47 -20
View File
@@ -1,10 +1,14 @@
import asyncio
import json
import pytest
from galaxy.api.plugin import Plugin
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
async def get_owned_games(self):
pass
@@ -16,10 +20,10 @@ 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), ""]
asyncio.run(plugin.run())
response = json.loads(write.call_args[0][0])
assert response == {
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
await plugin.run()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"result": {
@@ -30,39 +34,62 @@ def test_get_capabilites(reader, writer, readline, write):
"token": token
}
}
]
def test_shutdown(plugin, readline, write):
@pytest.mark.asyncio
async def test_shutdown(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "5",
"method": "shutdown"
}
readline.side_effect = [json.dumps(request)]
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request))]
await plugin.run()
await plugin.wait_closed()
plugin.shutdown.assert_called_with()
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "5",
"result": None
}
]
def test_ping(plugin, readline, write):
@pytest.mark.asyncio
async def test_ping(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "7",
"method": "ping"
}
readline.side_effect = [json.dumps(request), ""]
asyncio.run(plugin.run())
response = json.loads(write.call_args[0][0])
assert response == {
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
await plugin.run()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "7",
"result": None
}
]
def test_tick(plugin, readline):
readline.side_effect = [""]
asyncio.run(plugin.run())
@pytest.mark.asyncio
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()
+10 -6
View File
@@ -1,7 +1,12 @@
import asyncio
import json
import pytest
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 = {
"jsonrpc": "2.0",
"method": "launch_game",
@@ -10,7 +15,6 @@ def test_success(plugin, readline):
}
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_owned_games.return_value = None
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
await plugin.run()
plugin.launch_game.assert_called_with(game_id="3")
+17
View 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()
+28 -25
View File
@@ -1,31 +1,32 @@
import asyncio
import json
import pytest
from galaxy.api.types import LocalGame
from galaxy.api.consts import LocalGameState
from galaxy.api.errors import UnknownError, FailedParsingManifest
from galaxy.unittest.mock import async_return_value, skip_loop
def test_success(plugin, readline, write):
from tests import create_message, get_messages
@pytest.mark.asyncio
async def test_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"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.coro.return_value = [
plugin.get_local_games.return_value = async_return_value([
LocalGame("1", LocalGameState.Running),
LocalGame("2", LocalGameState.Installed),
LocalGame("3", LocalGameState.Installed | LocalGameState.Running)
]
asyncio.run(plugin.run())
])
await plugin.run()
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",
"result": {
@@ -45,7 +46,10 @@ def test_success(plugin, readline, write):
]
}
}
]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"error,code,message",
[
@@ -53,20 +57,19 @@ 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):
async 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), ""]
plugin.get_local_games.coro.side_effect = error()
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_local_games.side_effect = error()
await plugin.run()
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",
"error": {
@@ -74,17 +77,16 @@ def test_failure(plugin, readline, write, error, code, message):
"message": message
}
}
]
def test_local_game_state_update(plugin, write):
@pytest.mark.asyncio
async def test_local_game_state_update(plugin, write):
game = LocalGame("1", LocalGameState.Running)
async def couritine():
plugin.update_local_game_status(game)
await skip_loop()
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "local_game_status_changed",
"params": {
@@ -94,3 +96,4 @@ def test_local_game_state_update(plugin, write):
}
}
}
]
+188
View File
@@ -0,0 +1,188 @@
from unittest.mock import call
import pytest
from galaxy.api.errors import FailedParsingManifest
from galaxy.unittest.mock import async_return_value
from tests import create_message, get_messages
@pytest.mark.asyncio
async def test_get_local_size_success(plugin, read, write):
context = {'abc': 'def'}
plugin.prepare_local_size_context.return_value = async_return_value(context)
request = {
"jsonrpc": "2.0",
"id": "11",
"method": "start_local_size_import",
"params": {"game_ids": ["777", "13", "42"]}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_local_size.side_effect = [
async_return_value(100000000000),
async_return_value(None),
async_return_value(3333333)
]
await plugin.run()
plugin.get_local_size.assert_has_calls([
call("777", context),
call("13", context),
call("42", context)
])
plugin.local_size_import_complete.assert_called_once_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "11",
"result": None
},
{
"jsonrpc": "2.0",
"method": "local_size_import_success",
"params": {
"game_id": "777",
"local_size": 100000000000
}
},
{
"jsonrpc": "2.0",
"method": "local_size_import_success",
"params": {
"game_id": "13",
"local_size": None
}
},
{
"jsonrpc": "2.0",
"method": "local_size_import_success",
"params": {
"game_id": "42",
"local_size": 3333333
}
},
{
"jsonrpc": "2.0",
"method": "local_size_import_finished",
"params": None
}
]
@pytest.mark.asyncio
@pytest.mark.parametrize("exception,code,message", [
(FailedParsingManifest, 200, "Failed parsing manifest"),
(KeyError, 0, "Unknown error")
])
async def test_get_local_size_error(exception, code, message, plugin, read, write):
game_id = "6"
request_id = "55"
plugin.prepare_local_size_context.return_value = async_return_value(None)
request = {
"jsonrpc": "2.0",
"id": request_id,
"method": "start_local_size_import",
"params": {"game_ids": [game_id]}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_local_size.side_effect = exception
await plugin.run()
plugin.get_local_size.assert_called()
plugin.local_size_import_complete.assert_called_once_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": request_id,
"result": None
},
{
"jsonrpc": "2.0",
"method": "local_size_import_failure",
"params": {
"game_id": game_id,
"error": {
"code": code,
"message": message
}
}
},
{
"jsonrpc": "2.0",
"method": "local_size_import_finished",
"params": None
}
]
@pytest.mark.asyncio
async def test_prepare_get_local_size_context_error(plugin, read, write):
request_id = "31415"
error_details = "Unexpected syntax"
error_message, error_code = FailedParsingManifest().message, FailedParsingManifest().code
plugin.prepare_local_size_context.side_effect = FailedParsingManifest(error_details)
request = {
"jsonrpc": "2.0",
"id": request_id,
"method": "start_local_size_import",
"params": {"game_ids": ["6"]}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
await plugin.run()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": error_code,
"message": error_message,
"data": error_details
}
}
]
@pytest.mark.asyncio
async def test_import_already_in_progress_error(plugin, read, write):
plugin.prepare_local_size_context.return_value = async_return_value(None)
requests = [
{
"jsonrpc": "2.0",
"id": "3",
"method": "start_local_size_import",
"params": {
"game_ids": ["42"]
}
},
{
"jsonrpc": "2.0",
"id": "4",
"method": "start_local_size_import",
"params": {
"game_ids": ["13"]
}
}
]
read.side_effect = [
async_return_value(create_message(requests[0])),
async_return_value(create_message(requests[1])),
async_return_value(b"", 10)
]
await plugin.run()
responses = get_messages(write)
assert {
"jsonrpc": "2.0",
"id": "3",
"result": None
} in responses
assert {
"jsonrpc": "2.0",
"id": "4",
"error": {
"code": 600,
"message": "Import already in progress"
}
} in responses
+187
View File
@@ -0,0 +1,187 @@
from unittest.mock import call
import pytest
from galaxy.api.consts import OSCompatibility
from galaxy.api.errors import BackendError
from galaxy.unittest.mock import async_return_value
from tests import create_message, get_messages
@pytest.mark.asyncio
async def test_get_os_compatibility_success(plugin, read, write):
context = "abc"
plugin.prepare_os_compatibility_context.return_value = async_return_value(context)
request = {
"jsonrpc": "2.0",
"id": "11",
"method": "start_os_compatibility_import",
"params": {"game_ids": ["666", "13", "42"]}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_os_compatibility.side_effect = [
async_return_value(OSCompatibility.Linux),
async_return_value(None),
async_return_value(OSCompatibility.Windows | OSCompatibility.MacOS),
]
await plugin.run()
plugin.get_os_compatibility.assert_has_calls([
call("666", context),
call("13", context),
call("42", context),
])
plugin.os_compatibility_import_complete.assert_called_once_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "11",
"result": None
},
{
"jsonrpc": "2.0",
"method": "os_compatibility_import_success",
"params": {
"game_id": "666",
"os_compatibility": OSCompatibility.Linux.value
}
},
{
"jsonrpc": "2.0",
"method": "os_compatibility_import_success",
"params": {
"game_id": "13",
"os_compatibility": None
}
},
{
"jsonrpc": "2.0",
"method": "os_compatibility_import_success",
"params": {
"game_id": "42",
"os_compatibility": (OSCompatibility.Windows | OSCompatibility.MacOS).value
}
},
{
"jsonrpc": "2.0",
"method": "os_compatibility_import_finished",
"params": None
}
]
@pytest.mark.asyncio
@pytest.mark.parametrize("exception,code,message", [
(BackendError, 4, "Backend error"),
(KeyError, 0, "Unknown error")
])
async def test_get_os_compatibility_error(exception, code, message, plugin, read, write):
game_id = "6"
request_id = "55"
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
request = {
"jsonrpc": "2.0",
"id": request_id,
"method": "start_os_compatibility_import",
"params": {"game_ids": [game_id]}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_os_compatibility.side_effect = exception
await plugin.run()
plugin.get_os_compatibility.assert_called()
plugin.os_compatibility_import_complete.assert_called_once_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": request_id,
"result": None
},
{
"jsonrpc": "2.0",
"method": "os_compatibility_import_failure",
"params": {
"game_id": game_id,
"error": {
"code": code,
"message": message
}
}
},
{
"jsonrpc": "2.0",
"method": "os_compatibility_import_finished",
"params": None
}
]
@pytest.mark.asyncio
async def test_prepare_get_os_compatibility_context_error(plugin, read, write):
request_id = "31415"
plugin.prepare_os_compatibility_context.side_effect = BackendError()
request = {
"jsonrpc": "2.0",
"id": request_id,
"method": "start_os_compatibility_import",
"params": {"game_ids": ["6"]}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
await plugin.run()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": 4,
"message": "Backend error"
}
}
]
@pytest.mark.asyncio
async def test_import_already_in_progress_error(plugin, read, write):
plugin.prepare_os_compatibility_context.return_value = async_return_value(None)
requests = [
{
"jsonrpc": "2.0",
"id": "3",
"method": "start_os_compatibility_import",
"params": {
"game_ids": ["42"]
}
},
{
"jsonrpc": "2.0",
"id": "4",
"method": "start_os_compatibility_import",
"params": {
"game_ids": ["666"]
}
}
]
read.side_effect = [
async_return_value(create_message(requests[0])),
async_return_value(create_message(requests[1])),
async_return_value(b"", 10)
]
await plugin.run()
responses = get_messages(write)
assert {
"jsonrpc": "2.0",
"id": "3",
"result": None
} in responses
assert {
"jsonrpc": "2.0",
"id": "4",
"error": {
"code": 600,
"message": "Import already in progress"
}
} in responses
+44 -40
View File
@@ -1,19 +1,23 @@
import asyncio
import json
import pytest
from galaxy.api.types import Game, Dlc, LicenseInfo
from galaxy.api.consts import LicenseType
from galaxy.api.errors import UnknownError
from galaxy.unittest.mock import async_return_value, skip_loop
def test_success(plugin, readline, write):
from tests import create_message, get_messages
@pytest.mark.asyncio
async def test_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"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.coro.return_value = [
plugin.get_owned_games.return_value = async_return_value([
Game("3", "Doom", None, LicenseInfo(LicenseType.SinglePurchase, None)),
Game(
"5",
@@ -23,12 +27,11 @@ def test_success(plugin, readline, write):
Dlc("8", "Temerian Armor Set", LicenseInfo(LicenseType.FreeToPlay, None)),
],
LicenseInfo(LicenseType.SinglePurchase, None))
]
asyncio.run(plugin.run())
])
await plugin.run()
plugin.get_owned_games.assert_called_with()
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"result": {
@@ -66,21 +69,23 @@ def test_success(plugin, readline, write):
]
}
}
]
def test_failure(plugin, readline, write):
@pytest.mark.asyncio
async def test_failure(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_owned_games"
}
readline.side_effect = [json.dumps(request), ""]
plugin.get_owned_games.coro.side_effect = UnknownError()
asyncio.run(plugin.run())
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_owned_games.side_effect = UnknownError()
await plugin.run()
plugin.get_owned_games.assert_called_with()
response = json.loads(write.call_args[0][0])
assert response == {
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"error": {
@@ -88,17 +93,16 @@ def test_failure(plugin, readline, write):
"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))
async def couritine():
plugin.add_game(game)
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
assert response == {
await skip_loop()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "owned_game_added",
"params": {
@@ -111,32 +115,31 @@ def test_add_game(plugin, write):
}
}
}
]
def test_remove_game(plugin, write):
async def couritine():
@pytest.mark.asyncio
async def test_remove_game(plugin, write):
plugin.remove_game("5")
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
assert response == {
await skip_loop()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "owned_game_removed",
"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))
async def couritine():
plugin.update_game(game)
asyncio.run(couritine())
response = json.loads(write.call_args[0][0])
assert response == {
await skip_loop()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "owned_game_updated",
"params": {
@@ -149,3 +152,4 @@ def test_update_game(plugin, write):
}
}
}
]
+75
View File
@@ -0,0 +1,75 @@
import pytest
from galaxy.unittest.mock import async_return_value, skip_loop
from tests import create_message, get_messages
def assert_rpc_response(write, response_id, result=None):
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": str(response_id),
"result": result
}
]
def assert_rpc_request(write, method, params=None):
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": method,
"params": {"data": params}
}
]
@pytest.fixture
def cache_data():
return {
"persistent key": "persistent value",
"persistent object": {"answer to everything": 42}
}
@pytest.mark.asyncio
async def test_initialize_cache(plugin, read, write, cache_data):
request_id = 3
request = {
"jsonrpc": "2.0",
"id": str(request_id),
"method": "initialize_cache",
"params": {"data": cache_data}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"")]
assert {} == plugin.persistent_cache
await plugin.run()
plugin.handshake_complete.assert_called_once_with()
assert cache_data == plugin.persistent_cache
assert_rpc_response(write, response_id=request_id)
@pytest.mark.asyncio
async def test_set_cache(plugin, write, cache_data):
assert {} == plugin.persistent_cache
plugin.persistent_cache.update(cache_data)
plugin.push_cache()
await skip_loop()
assert_rpc_request(write, "push_cache", cache_data)
assert cache_data == plugin.persistent_cache
@pytest.mark.asyncio
async def test_clear_cache(plugin, write, cache_data):
plugin._persistent_cache = cache_data
plugin.persistent_cache.clear()
plugin.push_cache()
await skip_loop()
assert_rpc_request(write, "push_cache", {})
assert {} == plugin.persistent_cache
+72
View File
@@ -0,0 +1,72 @@
import pytest
import asyncio
from galaxy.unittest.mock import async_return_value
from tests import create_message, get_messages
from galaxy.api.errors import (
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied
)
from galaxy.api.jsonrpc import JsonRpcError
@pytest.mark.asyncio
async def test_refresh_credentials_success(plugin, read, write):
run_task = asyncio.create_task(plugin.run())
refreshed_credentials = {
"access_token": "new_access_token"
}
response = {
"jsonrpc": "2.0",
"id": "1",
"result": refreshed_credentials
}
# 2 loop iterations delay is to force sending response after request has been sent
read.side_effect = [async_return_value(create_message(response), loop_iterations_delay=2)]
result = await plugin.refresh_credentials({}, False)
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "refresh_credentials",
"params": {
},
"id": "1"
}
]
assert result == refreshed_credentials
await run_task
@pytest.mark.asyncio
@pytest.mark.parametrize("exception", [
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied
])
async def test_refresh_credentials_failure(exception, plugin, read, write):
run_task = asyncio.create_task(plugin.run())
error = exception()
response = {
"jsonrpc": "2.0",
"id": "1",
"error": error.json()
}
# 2 loop iterations delay is to force sending response after request has been sent
read.side_effect = [async_return_value(create_message(response), loop_iterations_delay=2)]
with pytest.raises(JsonRpcError) as e:
await plugin.refresh_credentials({}, False)
assert error == e.value
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "refresh_credentials",
"params": {
},
"id": "1"
}
]
await run_task
+17
View 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
View 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
+335
View File
@@ -0,0 +1,335 @@
import pytest
from galaxy.api.types import Subscription, SubscriptionGame
from galaxy.api.errors import FailedParsingManifest, BackendError, UnknownError
from galaxy.unittest.mock import async_return_value
from tests import create_message, get_messages
@pytest.mark.asyncio
async def test_get_subscriptions_success(plugin, read, write):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_subscriptions"
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_subscriptions.return_value = async_return_value([
Subscription("1"),
Subscription("2", False),
Subscription("3", True, 1580899100)
])
await plugin.run()
plugin.get_subscriptions.assert_called_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"result": {
"subscriptions": [
{
"subscription_name": "1"
},
{
"subscription_name": "2",
"owned": False
},
{
"subscription_name": "3",
"owned": True,
"end_time": 1580899100
}
]
}
}
]
@pytest.mark.asyncio
@pytest.mark.parametrize(
"error,code,message",
[
pytest.param(UnknownError, 0, "Unknown error", id="unknown_error"),
pytest.param(FailedParsingManifest, 200, "Failed parsing manifest", id="failed_parsing")
],
)
async def test_get_subscriptions_failure_generic(plugin, read, write, error, code, message):
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "import_subscriptions"
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_subscriptions.side_effect = error()
await plugin.run()
plugin.get_subscriptions.assert_called_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"error": {
"code": code,
"message": message
}
}
]
@pytest.mark.asyncio
async def test_get_subscription_games_success(plugin, read, write):
plugin.prepare_subscription_games_context.return_value = async_return_value(5)
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "start_subscription_games_import",
"params": {
"subscription_names": ["sub_a"]
}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
async def sub_games():
games = [
SubscriptionGame(game_title="game A", game_id="game_A"),
SubscriptionGame(game_title="game B", game_id="game_B", start_time=1548495632),
SubscriptionGame(game_title="game C", game_id="game_C", end_time=1548495633),
SubscriptionGame(game_title="game D", game_id="game_D", start_time=1548495632, end_time=1548495633),
]
yield [game for game in games]
plugin.get_subscription_games.return_value = sub_games()
await plugin.run()
plugin.prepare_subscription_games_context.assert_called_with(["sub_a"])
plugin.get_subscription_games.assert_called_with("sub_a", 5)
plugin.subscription_games_import_complete.asert_called_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"result": None
},
{
"jsonrpc": "2.0",
"method": "subscription_games_import_success",
"params": {
"subscription_name": "sub_a",
"subscription_games": [
{
"game_title": "game A",
"game_id": "game_A"
},
{
"game_title": "game B",
"game_id": "game_B",
"start_time": 1548495632
},
{
"game_title": "game C",
"game_id": "game_C",
"end_time": 1548495633
},
{
"game_title": "game D",
"game_id": "game_D",
"start_time": 1548495632,
"end_time": 1548495633
}
]
}
},
{
'jsonrpc': '2.0',
'method':
'subscription_games_partial_import_finished',
'params': {
"subscription_name": "sub_a"
}
},
{
"jsonrpc": "2.0",
"method": "subscription_games_import_finished",
"params": None
}
]
@pytest.mark.asyncio
async def test_get_subscription_games_success_empty(plugin, read, write):
plugin.prepare_subscription_games_context.return_value = async_return_value(5)
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "start_subscription_games_import",
"params": {
"subscription_names": ["sub_a"]
}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
async def sub_games():
yield None
plugin.get_subscription_games.return_value = sub_games()
await plugin.run()
plugin.prepare_subscription_games_context.assert_called_with(["sub_a"])
plugin.get_subscription_games.assert_called_with("sub_a", 5)
plugin.subscription_games_import_complete.asert_called_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"result": None
},
{
"jsonrpc": "2.0",
"method": "subscription_games_import_success",
"params": {
"subscription_name": "sub_a",
"subscription_games": None
}
},
{
'jsonrpc': '2.0',
'method':
'subscription_games_partial_import_finished',
'params': {
"subscription_name": "sub_a"
}
},
{
"jsonrpc": "2.0",
"method": "subscription_games_import_finished",
"params": None
}
]
@pytest.mark.asyncio
@pytest.mark.parametrize("exception,code,message", [
(BackendError, 4, "Backend error"),
(KeyError, 0, "Unknown error")
])
async def test_get_subscription_games_error(exception, code, message, plugin, read, write):
plugin.prepare_subscription_games_context.return_value = async_return_value(None)
request = {
"jsonrpc": "2.0",
"id": "3",
"method": "start_subscription_games_import",
"params": {
"subscription_names": ["sub_a"]
}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_subscription_games.side_effect = exception
await plugin.run()
plugin.get_subscription_games.assert_called()
plugin.subscription_games_import_complete.asert_called_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "3",
"result": None
},
{
"jsonrpc": "2.0",
"method": "subscription_games_import_failure",
"params": {
"subscription_name": "sub_a",
"error": {
"code": code,
"message": message
}
}
},
{
'jsonrpc': '2.0',
'method':
'subscription_games_partial_import_finished',
'params': {
"subscription_name": "sub_a"
}
},
{
"jsonrpc": "2.0",
"method": "subscription_games_import_finished",
"params": None
}
]
@pytest.mark.asyncio
async def test_prepare_get_subscription_games_context_error(plugin, read, write):
request_id = "31415"
error_details = "Unexpected backend error"
error_message, error_code = BackendError().message, BackendError().code
plugin.prepare_subscription_games_context.side_effect = BackendError(error_details)
request = {
"jsonrpc": "2.0",
"id": request_id,
"method": "start_subscription_games_import",
"params": {"subscription_names": ["sub_a", "sub_b"]}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
await plugin.run()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": error_code,
"message": error_message,
"data": error_details
}
}
]
@pytest.mark.asyncio
async def test_import_already_in_progress_error(plugin, read, write):
plugin.prepare_subscription_games_context.return_value = async_return_value(None)
requests = [
{
"jsonrpc": "2.0",
"id": "3",
"method": "start_subscription_games_import",
"params": {
"subscription_names": ["sub_a"]
}
},
{
"jsonrpc": "2.0",
"id": "4",
"method": "start_subscription_games_import",
"params": {
"subscription_names": ["sub_a","sub_b"]
}
}
]
read.side_effect = [
async_return_value(create_message(requests[0])),
async_return_value(create_message(requests[1])),
async_return_value(b"", 10)
]
await plugin.run()
responses = get_messages(write)
assert {
"jsonrpc": "2.0",
"id": "3",
"result": None
} in responses
assert {
"jsonrpc": "2.0",
"id": "4",
"error": {
"code": 600,
"message": "Import already in progress"
}
} in responses
+9 -6
View File
@@ -1,7 +1,11 @@
import asyncio
import json
import pytest
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 = {
"jsonrpc": "2.0",
"method": "uninstall_game",
@@ -9,8 +13,7 @@ def test_success(plugin, readline):
"game_id": "3"
}
}
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
asyncio.run(plugin.run())
await plugin.run()
plugin.uninstall_game.assert_called_with(game_id="3")
+276
View File
@@ -0,0 +1,276 @@
from unittest.mock import call
import pytest
from galaxy.api.consts import PresenceState
from galaxy.api.errors import BackendError
from galaxy.api.types import UserPresence
from galaxy.unittest.mock import async_return_value, skip_loop
from tests import create_message, get_messages
@pytest.mark.asyncio
async def test_get_user_presence_success(plugin, read, write):
context = "abc"
user_id_list = ["666", "13", "42", "69", "22"]
plugin.prepare_user_presence_context.return_value = async_return_value(context)
request = {
"jsonrpc": "2.0",
"id": "11",
"method": "start_user_presence_import",
"params": {"user_id_list": user_id_list}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_user_presence.side_effect = [
async_return_value(UserPresence(
PresenceState.Unknown,
"game-id1",
None,
"unknown state",
None
)),
async_return_value(UserPresence(
PresenceState.Offline,
None,
None,
"Going to grandma's house",
None
)),
async_return_value(UserPresence(
PresenceState.Online,
"game-id3",
"game-title3",
"Pew pew",
None
)),
async_return_value(UserPresence(
PresenceState.Away,
None,
"game-title4",
"AFKKTHXBY",
None
)),
async_return_value(UserPresence(
PresenceState.Away,
None,
"game-title5",
None,
"Playing game-title5: In Menu"
)),
]
await plugin.run()
plugin.get_user_presence.assert_has_calls([
call(user_id, context) for user_id in user_id_list
])
plugin.user_presence_import_complete.assert_called_once_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": "11",
"result": None
},
{
"jsonrpc": "2.0",
"method": "user_presence_import_success",
"params": {
"user_id": "666",
"presence": {
"presence_state": PresenceState.Unknown.value,
"game_id": "game-id1",
"in_game_status": "unknown state"
}
}
},
{
"jsonrpc": "2.0",
"method": "user_presence_import_success",
"params": {
"user_id": "13",
"presence": {
"presence_state": PresenceState.Offline.value,
"in_game_status": "Going to grandma's house"
}
}
},
{
"jsonrpc": "2.0",
"method": "user_presence_import_success",
"params": {
"user_id": "42",
"presence": {
"presence_state": PresenceState.Online.value,
"game_id": "game-id3",
"game_title": "game-title3",
"in_game_status": "Pew pew"
}
}
},
{
"jsonrpc": "2.0",
"method": "user_presence_import_success",
"params": {
"user_id": "69",
"presence": {
"presence_state": PresenceState.Away.value,
"game_title": "game-title4",
"in_game_status": "AFKKTHXBY"
}
}
},
{
"jsonrpc": "2.0",
"method": "user_presence_import_success",
"params": {
"user_id": "22",
"presence": {
"presence_state": PresenceState.Away.value,
"game_title": "game-title5",
"full_status": "Playing game-title5: In Menu"
}
}
},
{
"jsonrpc": "2.0",
"method": "user_presence_import_finished",
"params": None
}
]
@pytest.mark.asyncio
@pytest.mark.parametrize("exception,code,message", [
(BackendError, 4, "Backend error"),
(KeyError, 0, "Unknown error")
])
async def test_get_user_presence_error(exception, code, message, plugin, read, write):
user_id = "69"
request_id = "55"
plugin.prepare_user_presence_context.return_value = async_return_value(None)
request = {
"jsonrpc": "2.0",
"id": request_id,
"method": "start_user_presence_import",
"params": {"user_id_list": [user_id]}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
plugin.get_user_presence.side_effect = exception
await plugin.run()
plugin.get_user_presence.assert_called()
plugin.user_presence_import_complete.assert_called_once_with()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": request_id,
"result": None
},
{
"jsonrpc": "2.0",
"method": "user_presence_import_failure",
"params": {
"user_id": user_id,
"error": {
"code": code,
"message": message
}
}
},
{
"jsonrpc": "2.0",
"method": "user_presence_import_finished",
"params": None
}
]
@pytest.mark.asyncio
async def test_prepare_get_user_presence_context_error(plugin, read, write):
request_id = "31415"
plugin.prepare_user_presence_context.side_effect = BackendError()
request = {
"jsonrpc": "2.0",
"id": request_id,
"method": "start_user_presence_import",
"params": {"user_id_list": ["6"]}
}
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
await plugin.run()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"id": request_id,
"error": {
"code": 4,
"message": "Backend error"
}
}
]
@pytest.mark.asyncio
async def test_import_already_in_progress_error(plugin, read, write):
plugin.prepare_user_presence_context.return_value = async_return_value(None)
requests = [
{
"jsonrpc": "2.0",
"id": "3",
"method": "start_user_presence_import",
"params": {
"user_id_list": ["42"]
}
},
{
"jsonrpc": "2.0",
"id": "4",
"method": "start_user_presence_import",
"params": {
"user_id_list": ["666"]
}
}
]
read.side_effect = [
async_return_value(create_message(requests[0])),
async_return_value(create_message(requests[1])),
async_return_value(b"", 10)
]
await plugin.run()
responses = get_messages(write)
assert {
"jsonrpc": "2.0",
"id": "3",
"result": None
} in responses
assert {
"jsonrpc": "2.0",
"id": "4",
"error": {
"code": 600,
"message": "Import already in progress"
}
} in responses
@pytest.mark.asyncio
async def test_update_user_presence(plugin, write):
plugin.update_user_presence("42", UserPresence(PresenceState.Online, "game-id", "game-title", "Pew pew"))
await skip_loop()
assert get_messages(write) == [
{
"jsonrpc": "2.0",
"method": "user_presence_updated",
"params": {
"user_id": "42",
"presence": {
"presence_state": PresenceState.Online.value,
"game_id": "game-id",
"game_title": "game-title",
"in_game_status": "Pew pew"
}
}
}
]
-69
View File
@@ -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"
}
}