mirror of
https://github.com/gogcom/galaxy-integrations-python-api.git
synced 2026-01-01 11:28:12 -05:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
947c578121 | ||
|
|
aba9b0ed6b | ||
|
|
f0d65a72ff | ||
|
|
96cb48fcaf | ||
|
|
17b0542fdf | ||
|
|
0cf447bdcf | ||
|
|
259702e0de | ||
|
|
b96c55397e | ||
|
|
f82cab2770 | ||
|
|
1e7c284035 | ||
|
|
0c49ee315e | ||
|
|
aaeca6b47e | ||
|
|
fe8f7e929a | ||
|
|
49da4d4d37 | ||
|
|
9745dcd8ef |
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
11
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: GOG GALAXY 2.0 issue
|
||||||
|
url: https://mantis2.gog.com/
|
||||||
|
about: Report issues related to GOG GALAXY 2.0, official integrations or the whole ecosystem
|
||||||
|
- name: Platform ID request
|
||||||
|
url: https://github.com/gogcom/galaxy-integrations-python-api/issues/160
|
||||||
|
about: Report missing platform id
|
||||||
|
- name: Community integrations
|
||||||
|
url: https://github.com/Mixaill/awesome-gog-galaxy
|
||||||
|
about: Find integrations and their maintainers, request new integrations or report issues related to unofficial integrations.
|
||||||
14
.github/ISSUE_TEMPLATE/problem_report.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/problem_report.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: API issue
|
||||||
|
about: Report a bug or problem with current API architecture
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Problem**
|
||||||
|
<!-- Describe the problem you faced. -->
|
||||||
|
|
||||||
|
**Solution**
|
||||||
|
<!-- Describe the solution you'd like. -->
|
||||||
|
|
||||||
|
**Alternatives**
|
||||||
|
<!-- Optionally describe possible alternatives or current workarounds if any. -->
|
||||||
@@ -4,10 +4,10 @@ Platform ID list for GOG Galaxy 2.0 Integrations
|
|||||||
|
|
||||||
| ID | Name |
|
| ID | Name |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|
| test | Testing purposes |
|
||||||
| steam | Steam |
|
| steam | Steam |
|
||||||
| psn | PlayStation Network |
|
| psn | PlayStation Network |
|
||||||
| xboxone | Xbox Live |
|
| xboxone | Xbox Live |
|
||||||
| generic | Manually added games |
|
|
||||||
| origin | Origin |
|
| origin | Origin |
|
||||||
| uplay | Uplay |
|
| uplay | Uplay |
|
||||||
| battlenet | Battle.net |
|
| battlenet | Battle.net |
|
||||||
@@ -80,3 +80,12 @@ Platform ID list for GOG Galaxy 2.0 Integrations
|
|||||||
| nds | Nintendo DS |
|
| nds | Nintendo DS |
|
||||||
| 3ds | Nintendo 3DS |
|
| 3ds | Nintendo 3DS |
|
||||||
| pathofexile | Path of Exile |
|
| pathofexile | Path of Exile |
|
||||||
|
| twitch | Twitch |
|
||||||
|
| minecraft | Minecraft |
|
||||||
|
| gamesessions | GameSessions |
|
||||||
|
| nuuvem | Nuuvem |
|
||||||
|
| fxstore | FX Store |
|
||||||
|
| indiegala | IndieGala |
|
||||||
|
| playfire | Playfire |
|
||||||
|
| oculus | Oculus |
|
||||||
|
| rockstar | Rockstar |
|
||||||
|
|||||||
40
README.md
40
README.md
@@ -36,20 +36,31 @@ Communication between an integration and the client is also possible with the us
|
|||||||
import sys
|
import sys
|
||||||
from galaxy.api.plugin import Plugin, create_and_run_plugin
|
from galaxy.api.plugin import Plugin, create_and_run_plugin
|
||||||
from galaxy.api.consts import Platform
|
from galaxy.api.consts import Platform
|
||||||
|
from galaxy.api.types import Authentication, Game, LicenseInfo, LicenseType
|
||||||
|
|
||||||
|
|
||||||
class PluginExample(Plugin):
|
class PluginExample(Plugin):
|
||||||
def __init__(self, reader, writer, token):
|
def __init__(self, reader, writer, token):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
Platform.Generic, # Choose platform from available list
|
Platform.Test, # choose platform from available list
|
||||||
"0.1", # Version
|
"0.1", # version
|
||||||
reader,
|
reader,
|
||||||
writer,
|
writer,
|
||||||
token
|
token
|
||||||
)
|
)
|
||||||
|
|
||||||
# implement methods
|
# implement methods
|
||||||
|
|
||||||
|
# required
|
||||||
async def authenticate(self, stored_credentials=None):
|
async def authenticate(self, stored_credentials=None):
|
||||||
pass
|
return Authentication('test_user_id', 'Test User Name')
|
||||||
|
|
||||||
|
# required
|
||||||
|
async def get_owned_games(self):
|
||||||
|
return [
|
||||||
|
Game('test', 'The Test', None, LicenseInfo(LicenseType.SinglePurchase))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
create_and_run_plugin(PluginExample, sys.argv)
|
create_and_run_plugin(PluginExample, sys.argv)
|
||||||
@@ -76,6 +87,20 @@ In order to be found by GOG Galaxy 2.0 an integration folder should be placed in
|
|||||||
|
|
||||||
`~/Library/Application Support/GOG.com/Galaxy/plugins/installed`
|
`~/Library/Application Support/GOG.com/Galaxy/plugins/installed`
|
||||||
|
|
||||||
|
### Logging
|
||||||
|
<a href='https://docs.python.org/3.7/howto/logging.html'>Root logger</a> is already setup by GOG Galaxy to store rotated log files in:
|
||||||
|
|
||||||
|
- Windows:
|
||||||
|
|
||||||
|
`%programdata%\GOG.com\Galaxy\logs`
|
||||||
|
|
||||||
|
- macOS:
|
||||||
|
|
||||||
|
`/Users/Shared/GOG.com/Galaxy/Logs`
|
||||||
|
|
||||||
|
Plugin logs are kept in `plugin-<platform>-<guid>.log`.
|
||||||
|
When debugging, inspecting the other side of communication in the `GalaxyClient.log` can be helpful as well.
|
||||||
|
|
||||||
### Manifest
|
### Manifest
|
||||||
|
|
||||||
<a name="deploy-manifest"></a>
|
<a name="deploy-manifest"></a>
|
||||||
@@ -84,8 +109,8 @@ Obligatory JSON file to be placed in an integration folder.
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "Example plugin",
|
"name": "Example plugin",
|
||||||
"platform": "generic",
|
"platform": "test",
|
||||||
"guid": "UNIQUE-GUID",
|
"guid": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||||||
"version": "0.1",
|
"version": "0.1",
|
||||||
"description": "Example plugin",
|
"description": "Example plugin",
|
||||||
"author": "Name",
|
"author": "Name",
|
||||||
@@ -97,9 +122,8 @@ Obligatory JSON file to be placed in an integration folder.
|
|||||||
|
|
||||||
| property | description |
|
| property | description |
|
||||||
|---------------|---|
|
|---------------|---|
|
||||||
| `guid` | |
|
| `guid` | custom Globally Unique Identifier |
|
||||||
| `description` | |
|
| `version` | the same string as `version` in `Plugin` constructor |
|
||||||
| `url` | |
|
|
||||||
| `script` | path of the entry point module, relative to the integration folder |
|
| `script` | path of the entry point module, relative to the integration folder |
|
||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|||||||
@@ -7,4 +7,4 @@ pytest-flakes==4.0.0
|
|||||||
# because of pip bug https://github.com/pypa/pip/issues/4780
|
# because of pip bug https://github.com/pypa/pip/issues/4780
|
||||||
aiohttp==3.5.4
|
aiohttp==3.5.4
|
||||||
certifi==2019.3.9
|
certifi==2019.3.9
|
||||||
psutil==5.6.3; sys_platform == 'darwin'
|
psutil==5.6.6; sys_platform == 'darwin'
|
||||||
|
|||||||
4
setup.py
4
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="galaxy.plugin.api",
|
name="galaxy.plugin.api",
|
||||||
version="0.65",
|
version="0.68",
|
||||||
description="GOG Galaxy Integrations Python API",
|
description="GOG Galaxy Integrations Python API",
|
||||||
author='Galaxy team',
|
author='Galaxy team',
|
||||||
author_email='galaxy@gog.com',
|
author_email='galaxy@gog.com',
|
||||||
@@ -11,6 +11,6 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
"aiohttp>=3.5.4",
|
"aiohttp>=3.5.4",
|
||||||
"certifi>=2019.3.9",
|
"certifi>=2019.3.9",
|
||||||
"psutil>=5.6.3; sys_platform == 'darwin'"
|
"psutil>=5.6.6; sys_platform == 'darwin'"
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
logging.getLogger(__name__).setLevel(logging.INFO)
|
||||||
|
|
||||||
|
__path__: str = __import__('pkgutil').extend_path(__path__, __name__) # type: ignore
|
||||||
|
|||||||
@@ -87,3 +87,16 @@ class CollectionImporter(Importer):
|
|||||||
self._notification_failure(id_, UnknownError())
|
self._notification_failure(id_, UnknownError())
|
||||||
finally:
|
finally:
|
||||||
self._notification_partially_finished(id_)
|
self._notification_partially_finished(id_)
|
||||||
|
|
||||||
|
|
||||||
|
class SynchroneousImporter(Importer):
|
||||||
|
async def _import_elements(self, ids_, context_):
|
||||||
|
try:
|
||||||
|
for id_ in ids_:
|
||||||
|
await self._import_element(id_, context_)
|
||||||
|
self._notification_finished()
|
||||||
|
self._complete()
|
||||||
|
except asyncio.CancelledError:
|
||||||
|
logger.debug("Importing %s cancelled", self._name)
|
||||||
|
finally:
|
||||||
|
self._import_in_progress = False
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ class Connection():
|
|||||||
if sensitive:
|
if sensitive:
|
||||||
logger.debug("Sending %d bytes of data", len(data))
|
logger.debug("Sending %d bytes of data", len(data))
|
||||||
else:
|
else:
|
||||||
logging.debug("Sending data: %s", line)
|
logger.debug("Sending data: %s", line)
|
||||||
self._writer.write(data)
|
self._writer.write(data)
|
||||||
except TypeError as error:
|
except TypeError as error:
|
||||||
logger.error(str(error))
|
logger.error(str(error))
|
||||||
@@ -360,7 +360,8 @@ class Connection():
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _log_error(response, error, sensitive_params):
|
def _log_error(response, error, sensitive_params):
|
||||||
data = anonymise_sensitive_params(error.data, sensitive_params)
|
params = error.data if error.data is not None else {}
|
||||||
|
data = anonymise_sensitive_params(params, sensitive_params)
|
||||||
logger.info("Handling error: id=%s, code=%s, description=%s, data=%s",
|
logger.info("Handling error: id=%s, code=%s, description=%s, data=%s",
|
||||||
response.id, error.code, error.message, data
|
response.id, error.code, error.message, data
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from galaxy.api.types import (
|
|||||||
Subscription, SubscriptionGame
|
Subscription, SubscriptionGame
|
||||||
)
|
)
|
||||||
from galaxy.task_manager import TaskManager
|
from galaxy.task_manager import TaskManager
|
||||||
from galaxy.api.importer import Importer, CollectionImporter
|
from galaxy.api.importer import Importer, CollectionImporter, SynchroneousImporter
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -104,7 +104,7 @@ class Plugin:
|
|||||||
self._user_presence_import_finished,
|
self._user_presence_import_finished,
|
||||||
self.user_presence_import_complete
|
self.user_presence_import_complete
|
||||||
)
|
)
|
||||||
self._local_size_importer = Importer(
|
self._local_size_importer = SynchroneousImporter(
|
||||||
self._external_task_manager,
|
self._external_task_manager,
|
||||||
"local size",
|
"local size",
|
||||||
self.get_local_size,
|
self.get_local_size,
|
||||||
@@ -292,7 +292,7 @@ class Plugin:
|
|||||||
await self._external_task_manager.wait()
|
await self._external_task_manager.wait()
|
||||||
await self._internal_task_manager.wait()
|
await self._internal_task_manager.wait()
|
||||||
await self._connection.wait_closed()
|
await self._connection.wait_closed()
|
||||||
logger.debug("Plugin closed")
|
logger.info("Plugin closed")
|
||||||
|
|
||||||
def create_task(self, coro, description):
|
def create_task(self, coro, description):
|
||||||
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
"""Wrapper around asyncio.create_task - takes care of canceling tasks on shutdown"""
|
||||||
@@ -1027,10 +1027,9 @@ class Plugin:
|
|||||||
It is preferable to avoid iterating over local game files when overriding this method.
|
It is preferable to avoid iterating over local game files when overriding this method.
|
||||||
If possible, please use a more efficient way of game size retrieval.
|
If possible, please use a more efficient way of game size retrieval.
|
||||||
|
|
||||||
|
:param game_id: the id of the installed game
|
||||||
:param context: the value returned from :meth:`prepare_local_size_context`
|
:param context: the value returned from :meth:`prepare_local_size_context`
|
||||||
:return: game size (in bytes) or `None` if game size cannot be determined;
|
:return: the size of the game on a user-owned storage device (in bytes) or `None` if the size cannot be determined
|
||||||
'0' if the game is not installed, or if it is not present locally (e.g. installed
|
|
||||||
on another machine and accessible via remote connection, playable via web browser etc.)
|
|
||||||
"""
|
"""
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|||||||
@@ -166,8 +166,8 @@ class UserInfo:
|
|||||||
"""
|
"""
|
||||||
user_id: str
|
user_id: str
|
||||||
user_name: str
|
user_name: str
|
||||||
avatar_url: Optional[str]
|
avatar_url: Optional[str] = None
|
||||||
profile_url: Optional[str]
|
profile_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ Exemplary simple web service could looks like:
|
|||||||
|
|
||||||
.. code-block:: python
|
.. code-block:: python
|
||||||
|
|
||||||
import logging
|
|
||||||
from galaxy.http import create_client_session, handle_exception
|
from galaxy.http import create_client_session, handle_exception
|
||||||
|
|
||||||
class BackendClient:
|
class BackendClient:
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ async def test_get_friends_success(plugin, read, write):
|
|||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_friends.return_value = async_return_value([
|
plugin.get_friends.return_value = async_return_value([
|
||||||
UserInfo("3", "Jan", "https://avatar.url/u3", None),
|
UserInfo("3", "Jan", "https://avatar.url/u3", None),
|
||||||
UserInfo("5", "Ola", None, "https://profile.url/u5")
|
UserInfo("5", "Ola", None, "https://profile.url/u5"),
|
||||||
|
UserInfo("6", "Ola2", None),
|
||||||
|
UserInfo("7", "Ola3"),
|
||||||
])
|
])
|
||||||
await plugin.run()
|
await plugin.run()
|
||||||
plugin.get_friends.assert_called_with()
|
plugin.get_friends.assert_called_with()
|
||||||
@@ -30,7 +32,9 @@ async def test_get_friends_success(plugin, read, write):
|
|||||||
"result": {
|
"result": {
|
||||||
"friend_info_list": [
|
"friend_info_list": [
|
||||||
{"user_id": "3", "user_name": "Jan", "avatar_url": "https://avatar.url/u3"},
|
{"user_id": "3", "user_name": "Jan", "avatar_url": "https://avatar.url/u3"},
|
||||||
{"user_id": "5", "user_name": "Ola", "profile_url": "https://profile.url/u5"}
|
{"user_id": "5", "user_name": "Ola", "profile_url": "https://profile.url/u5"},
|
||||||
|
{"user_id": "6", "user_name": "Ola2"},
|
||||||
|
{"user_id": "7", "user_name": "Ola3"},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ async def test_get_local_size_success(plugin, read, write):
|
|||||||
}
|
}
|
||||||
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
read.side_effect = [async_return_value(create_message(request)), async_return_value(b"", 10)]
|
||||||
plugin.get_local_size.side_effect = [
|
plugin.get_local_size.side_effect = [
|
||||||
async_return_value(100000000000),
|
async_return_value(100000000000, 1),
|
||||||
async_return_value(None),
|
async_return_value(None),
|
||||||
async_return_value(3333333)
|
async_return_value(3333333)
|
||||||
]
|
]
|
||||||
@@ -89,12 +89,15 @@ async def test_get_local_size_error(exception, code, message, plugin, read, writ
|
|||||||
plugin.get_local_size.assert_called()
|
plugin.get_local_size.assert_called()
|
||||||
plugin.local_size_import_complete.assert_called_once_with()
|
plugin.local_size_import_complete.assert_called_once_with()
|
||||||
|
|
||||||
assert get_messages(write) == [
|
direct_response = {
|
||||||
{
|
"jsonrpc": "2.0",
|
||||||
"jsonrpc": "2.0",
|
"id": request_id,
|
||||||
"id": request_id,
|
"result": None
|
||||||
"result": None
|
}
|
||||||
},
|
responses = get_messages(write)
|
||||||
|
assert direct_response in responses
|
||||||
|
responses.remove(direct_response)
|
||||||
|
assert responses == [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": "local_size_import_failure",
|
"method": "local_size_import_failure",
|
||||||
@@ -145,6 +148,7 @@ async def test_prepare_get_local_size_context_error(plugin, read, write):
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_import_already_in_progress_error(plugin, read, write):
|
async def test_import_already_in_progress_error(plugin, read, write):
|
||||||
plugin.prepare_local_size_context.return_value = async_return_value(None)
|
plugin.prepare_local_size_context.return_value = async_return_value(None)
|
||||||
|
plugin.get_local_size.return_value = async_return_value(100, 5)
|
||||||
requests = [
|
requests = [
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
@@ -185,4 +189,3 @@ async def test_import_already_in_progress_error(plugin, read, write):
|
|||||||
"message": "Import already in progress"
|
"message": "Import already in progress"
|
||||||
}
|
}
|
||||||
} in responses
|
} in responses
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import asyncio
|
|||||||
from galaxy.unittest.mock import async_return_value
|
from galaxy.unittest.mock import async_return_value
|
||||||
from tests import create_message, get_messages
|
from tests import create_message, get_messages
|
||||||
from galaxy.api.errors import (
|
from galaxy.api.errors import (
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied
|
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied, UnknownError
|
||||||
)
|
)
|
||||||
from galaxy.api.jsonrpc import JsonRpcError
|
from galaxy.api.jsonrpc import JsonRpcError
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@@ -40,7 +40,7 @@ async def test_refresh_credentials_success(plugin, read, write):
|
|||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
@pytest.mark.parametrize("exception", [
|
@pytest.mark.parametrize("exception", [
|
||||||
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied
|
BackendNotAvailable, BackendTimeout, BackendError, InvalidCredentials, NetworkError, AccessDenied, UnknownError
|
||||||
])
|
])
|
||||||
async def test_refresh_credentials_failure(exception, plugin, read, write):
|
async def test_refresh_credentials_failure(exception, plugin, read, write):
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user