├── xumm ├── resource │ ├── types │ │ ├── meta │ │ │ ├── __init__.py │ │ │ ├── pong.py │ │ │ ├── kyc_info_response.py │ │ │ ├── kyc_status_response.py │ │ │ ├── user_tokens.py │ │ │ ├── xrpl_transaction.py │ │ │ └── rates_response.py │ │ ├── oauth2 │ │ │ ├── __init__.py │ │ │ ├── oauth2_token_response.py │ │ │ └── oauth2_user_info_response.py │ │ ├── push │ │ │ ├── __init__.py │ │ │ ├── push_push_response.py │ │ │ └── push_event_response.py │ │ ├── payload │ │ │ ├── __init__.py │ │ │ ├── on_payload_event.py │ │ │ ├── subscription_callback_params.py │ │ │ ├── payload_subscription.py │ │ │ └── payload_and_subscription.py │ │ ├── storage │ │ │ ├── __init__.py │ │ │ ├── storage_response.py │ │ │ ├── storage_get_response.py │ │ │ ├── storage_set_response.py │ │ │ └── storage_delete_response.py │ │ └── __init__.py │ ├── curated_assets.py │ ├── ping.py │ ├── user_tokens.py │ ├── xrpl_tx.py │ ├── rates.py │ ├── kyc_status.py │ ├── push.py │ ├── storage.py │ ├── oauth2.py │ ├── base.py │ ├── __init__.py │ └── payload.py ├── __init__.py ├── util.py ├── error.py ├── client.py └── ws_client.py ├── samples ├── fixtures │ ├── __init__.py │ └── sample_payload.py ├── misc.py ├── storage.py ├── oauth2.py └── payload.py ├── tox.ini ├── basedir.py ├── pyproject.toml ├── .env.sample ├── .gitignore ├── test.py ├── tests ├── types │ ├── meta │ │ ├── test_kyc_info_response.py │ │ ├── test_application_details.py │ │ ├── test_kyc_status_response copy.py │ │ ├── test_pong_response.py │ │ ├── test_user_tokens.py │ │ ├── test_xrpl_transaction.py │ │ └── test_curated_asset_response.py │ ├── test_misc.py │ ├── payload │ │ ├── test_on_payload_event.py │ │ ├── test_subscription_callback_params.py │ │ ├── test_payload_subscription.py │ │ └── test_payload_and_subscription.py │ └── xumm_api │ │ └── test_xumm_api_types.py ├── fixtures │ ├── xumm_ws.py │ └── xumm_api.py ├── test_push.py ├── test_app_storage.py ├── test_payload_create.py ├── test_payload_get.py ├── test_payload_cancel.py ├── test_common.py └── test_payload_subscribe.py ├── .github └── workflows │ └── workflows.yml ├── LICENSE ├── RELEASE.md ├── setup.py └── testing_config.py /xumm/resource/types/meta/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xumm/resource/types/oauth2/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xumm/resource/types/push/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xumm/resource/types/payload/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /xumm/resource/types/storage/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/fixtures/__init__.py: -------------------------------------------------------------------------------- 1 | from .sample_payload import payload as sample_payload -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = .git,__pycache__,samples,tests,build,dist,lib,bin 3 | -------------------------------------------------------------------------------- /basedir.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import os 5 | basedir = os.path.abspath(os.path.dirname(__file__)) 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires=[ 3 | "setuptools>=42", 4 | "wheel", 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /xumm/resource/types/payload/on_payload_event.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from typing import Union, Callable, Any 5 | from .subscription_callback_params import SubscriptionCallbackParams 6 | 7 | 8 | def on_payload_event( 9 | subscriptionCallback: SubscriptionCallbackParams 10 | ) -> Union[None, Callable[[Any], Any]]: 11 | return subscriptionCallback 12 | -------------------------------------------------------------------------------- /xumm/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | api_base = 'https://xumm.app/api/v1/' 4 | oauth2_base = 'https://oauth2.xumm.app' 5 | api_key = os.environ.get('XUMM_APIKEY', None) 6 | api_secret = os.environ.get('XUMM_APISECRET', None) 7 | api_version = 'v1' 8 | env = 'production' 9 | debug = True if os.environ.get('DEBUG') == 'xumm-sdk*' else False # noqa: E501 10 | 11 | from xumm.resource.base import ( # noqa 12 | XummSdk, 13 | ) 14 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # The XUMM SDK supports dotenv :) 2 | # 3 | # Make sure the XUMM_APIKEY and XUMM_APISECRET 4 | # vars are in your environment, or use a dotenv file. 5 | # 6 | # Rename this file to .env (hidden file, starts with a dot) 7 | # and enter your XUMM API Key and API Secret, 8 | # to be obtained from the XUMM App Console: 9 | # » https://apps.xumm.dev 10 | # 11 | 12 | XUMM_APIKEY=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 13 | XUMM_APISECRET=XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .eggs 3 | guesty.egg-info 4 | *.pyc 5 | venv 6 | .cache 7 | 8 | # PyTest # 9 | ###################### 10 | .pytest_cache 11 | # .github 12 | 13 | # Lint # 14 | /logs 15 | 16 | # Production # 17 | /build 18 | /dist 19 | /xumm_sdk_py.egg-info 20 | .env 21 | .pypirc 22 | README.html 23 | 24 | # virtualenv 25 | .Python 26 | [Bb]in 27 | [Ii]nclude 28 | [Ll]ib 29 | [Ll]ib64 30 | [Ll]ocal 31 | [Ss]cripts 32 | pyvenv.cfg 33 | .venv 34 | pip-selfcheck.json -------------------------------------------------------------------------------- /xumm/resource/curated_assets.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class CuratedAssetsResource(XummResource): 8 | 9 | @classmethod 10 | def get_url(cls) -> str: 11 | """ 12 | Gets the GET url of this KycStatusResource 13 | 14 | :return: The GET url of this KycStatusResource. 15 | :rtype: str 16 | """ 17 | return super(CuratedAssetsResource, cls).platform_url() + 'curated-assets' # noqa: E501 18 | -------------------------------------------------------------------------------- /xumm/resource/ping.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class PingResource(XummResource): 8 | 9 | @classmethod 10 | def get_url(cls) -> str: 11 | """ 12 | Gets the GET url of this PingResource 13 | 14 | :param id: A string id. 15 | :type: str 16 | :return: The GET url of this PingResource. 17 | :rtype: str 18 | """ 19 | return super(PingResource, cls).platform_url() + 'ping' + '/' 20 | -------------------------------------------------------------------------------- /xumm/resource/user_tokens.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class UserTokensResource(XummResource): 8 | 9 | @classmethod 10 | def post_url(cls) -> str: 11 | """ 12 | Gets the POST url of this UserTokensResource 13 | 14 | :param id: A string id. 15 | :type: str 16 | :return: The POST url of this UserTokensResource. 17 | :rtype: str 18 | """ 19 | return super(UserTokensResource, cls).platform_url() + 'user-tokens' + '/' # noqa: E501 20 | -------------------------------------------------------------------------------- /xumm/resource/xrpl_tx.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class XrplTxResource(XummResource): 8 | 9 | @classmethod 10 | def get_url(cls, tx_hash: str) -> str: 11 | """ 12 | Gets the GET url of this XrplTxResource 13 | 14 | :param tx_hash: A string contain transaction hash. 15 | :type: str 16 | :return: The GET url of this XrplTxResource. 17 | :rtype: str 18 | """ 19 | return super(XrplTxResource, cls).platform_url() + 'xrpl-tx' + '/' + tx_hash # noqa: E501 20 | -------------------------------------------------------------------------------- /xumm/resource/rates.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class RatesResource(XummResource): 8 | 9 | @classmethod 10 | def get_url(cls, id: str = None) -> str: 11 | """ 12 | Gets the GET url of this RatesResource 13 | 14 | :param id: A string id. 15 | :type: str 16 | :return: The GET url of this RatesResource. 17 | :rtype: str 18 | """ 19 | return '{}{}/{}'.format( 20 | super(RatesResource, cls).platform_url(), 21 | 'rates', 22 | id.strip().upper() 23 | ) 24 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from basedir import basedir 5 | import os 6 | import shutil 7 | import sys 8 | import pytest 9 | 10 | 11 | def main(): 12 | argv = [] 13 | 14 | argv.extend(sys.argv[1:]) 15 | 16 | pytest.main(argv) 17 | 18 | try: 19 | os.remove(os.path.join(basedir, '.coverage')) 20 | except OSError: 21 | pass 22 | 23 | try: 24 | shutil.rmtree(os.path.join(basedir, '.cache')) 25 | except OSError: 26 | pass 27 | 28 | try: 29 | shutil.rmtree(os.path.join(basedir, 'tests/.cache')) 30 | except OSError: 31 | pass 32 | 33 | 34 | if __name__ == '__main__': 35 | main() 36 | -------------------------------------------------------------------------------- /xumm/resource/kyc_status.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class KycStatusResource(XummResource): 8 | 9 | @classmethod 10 | def get_url(cls, id: str = None) -> str: 11 | """ 12 | Gets the GET url of this KycStatusResource 13 | 14 | :param id: A string id. 15 | :type: str 16 | :return: The GET url of this KycStatusResource. 17 | :rtype: str 18 | """ 19 | return super(KycStatusResource, cls).platform_url() + 'kyc-status/' + id # noqa: E501 20 | 21 | @classmethod 22 | def post_url(cls) -> str: 23 | """ 24 | Gets the POST url of this KycStatusResource 25 | 26 | :return: The POST url of this KycStatusResource. 27 | :rtype: str 28 | """ 29 | return super(KycStatusResource, cls).platform_url() + 'kyc-status' 30 | -------------------------------------------------------------------------------- /tests/types/meta/test_kyc_info_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import pytest 5 | from testing_config import BaseTestConfig 6 | 7 | from xumm.resource.types import ( 8 | KycInfoResponse 9 | ) 10 | 11 | class TestKycInfoResponse(BaseTestConfig): 12 | 13 | def test_kyc_info_response(cls): 14 | print('should set kyc info response') 15 | 16 | dict = cls.json_fixtures['kycStatus']['get'] 17 | cls.assertEqual(KycInfoResponse(**dict).to_dict(), dict) 18 | 19 | def test_kyc_info_response_fail(cls): 20 | print('should fail to set kyc info details') 21 | 22 | dict = { 23 | "account": 1, # FAILS 24 | "kycApproved": True 25 | } 26 | 27 | with pytest.raises(ValueError, match=r"Invalid value: 1 for `account`, must be a `` found: "): 28 | KycInfoResponse(**dict) 29 | cls.fail("KycInfoResponse: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /.github/workflows/workflows.yml: -------------------------------------------------------------------------------- 1 | name: Python 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | node: 11 | strategy: 12 | matrix: 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | python-version: [3.7, 3.8, 3.9] 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - name: Fix Windows Git autocrlf 18 | run: git config --global core.autocrlf false 19 | if: matrix.os == 'windows-latest' 20 | - uses: actions/checkout@v2 21 | - name: Use Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@v2 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | - name: Install dependencies 26 | run: | 27 | python3 -m pip install --upgrade pip 28 | pip install -e ".[develop]" 29 | - run: python3 -m flake8 30 | - run: python3 test.py tests/ 31 | env: 32 | XUMM_APIKEY: aaaaaaaa-cccc-eeee-1111-333333333333 33 | XUMM_APISECRET: bbbbbbbb-dddd-ffff-2222-444444444444 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 XRPL Labs, Wietse Wind 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /tests/types/test_misc.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from testing_config import BaseTestConfig 3 | 4 | from xumm.resource.types import ( 5 | ReturnUrl, 6 | Options, 7 | ) 8 | 9 | class TestMiscTypes(BaseTestConfig): 10 | 11 | def test_return_url(cls): 12 | print('should set return url') 13 | 14 | dict = cls.json_fixtures['webhook']['payloadResponse']['return_url'] 15 | cls.assertEqual(ReturnUrl(**dict).to_dict(), dict) 16 | 17 | def test_return_url_fail(cls): 18 | print('should fail to set return url') 19 | 20 | dict = {} 21 | with pytest.raises(ValueError, match=r'Invalid value for `app`, must not be `None`'): 22 | ReturnUrl(**dict) 23 | cls.fail("ReturnUrl: raised Exception unexpectedly!") 24 | 25 | def test_return_options(cls): 26 | print('should set options') 27 | 28 | dict = { 29 | 'submit': True, 30 | 'multisign': True, 31 | 'expire': 0, 32 | 'signers': ['test'], 33 | 'return_url': { 34 | 'app': 'string', 35 | 'web': 'string' 36 | } 37 | } 38 | cls.assertEqual(Options(**dict).to_dict(), dict) -------------------------------------------------------------------------------- /xumm/util.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from typing import Dict, Tuple # noqa: F401 5 | import json 6 | 7 | import requests 8 | 9 | 10 | def read_json(path: str) -> Dict[str, object]: 11 | """ 12 | Reads json from file path 13 | :return: Dict[str, object] 14 | """ 15 | with open(path) as json_file: 16 | return json.load(json_file) 17 | 18 | 19 | def write_json(data: Dict[str, object], path: str): 20 | """ 21 | Writes json to file path 22 | :return: 23 | """ 24 | with open(path, 'w') as json_file: 25 | json.dump(data, json_file) 26 | 27 | 28 | def parse_oauth2_response(url: str) -> Tuple[str, str]: 29 | if not isinstance(url, str): 30 | raise ValueError('invalid url type. must be `string`') 31 | 32 | query = requests.utils.urlparse(url).query 33 | params = dict(x.split('=') for x in query.split('&')) 34 | 35 | if 'authorization_code' not in params: 36 | raise ValueError('invalid response param: `authorization_code`') 37 | 38 | if 'state' not in params: 39 | raise ValueError('invalid response param: `state`') 40 | 41 | return params.get('authorization_code'), params.get('state') 42 | -------------------------------------------------------------------------------- /samples/fixtures/sample_payload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import binascii 5 | from uuid import uuid4 6 | 7 | payload = { 8 | 'custom_meta': { 9 | 'instruction': 'Hey! Please sign for:\n\nThis\nand\nthat 🍻', 10 | 'blob': { 11 | 'myOwnProp': 'Whereever', 12 | 'clientCountry': 'Moon', 13 | }, 14 | 'identifier': str(uuid4()) 15 | }, 16 | 'options': { 17 | 'submit': True, 18 | 'multisign': False, 19 | 'expire': 500, 20 | 'return_url': { 21 | 'app': 'https://example.com/callback?payload={id}&blob={txblob}', 22 | 'web': 'https://example.com/callback?identifier={cid}&tx={txid}' 23 | } 24 | }, 25 | 'txjson': { 26 | 'TransactionType' : 'Payment', 27 | 'Destination' : 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY', 28 | 'DestinationTag': 495, 29 | 'Amount': '1337', 30 | 'LastLedgerSequence': 20, 31 | 'Fee': '12', 32 | 'Memos': [ 33 | { 34 | 'Memo': { 35 | 'MemoData': binascii.hexlify('Sample XUMM payload'.encode('utf8')).decode('utf-8').upper(), 36 | 'MemoFormat': binascii.hexlify('some/memo'.encode('utf8')).decode('utf-8').upper(), 37 | } 38 | } 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /tests/types/meta/test_application_details.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import pytest 5 | from testing_config import BaseTestConfig 6 | 7 | from xumm.resource.types import ( 8 | ApplicationDetails 9 | ) 10 | 11 | class TestApplicationDetails(BaseTestConfig): 12 | 13 | def test_application_details(cls): 14 | print('should set application details') 15 | 16 | dict = cls.json_fixtures['ping']['pong']['auth'] 17 | cls.assertEqual(ApplicationDetails(**dict).to_dict(), dict) 18 | 19 | def test_application_details_fail(cls): 20 | print('should fail to set application details') 21 | 22 | dict = { 23 | "quota": {}, 24 | "application": { 25 | "uuidv4": 1, # FAILS 26 | "name": "SomeApplication", 27 | "webhookurl": "https://webhook.site/00000000-0000-4e34-8112-c4391247a8ee", 28 | "disabled": 0 29 | }, 30 | "call": { 31 | "uuidv4": "2904b05f-5b37-4f3e-a624-940ad817943c" 32 | } 33 | } 34 | 35 | with pytest.raises(ValueError, match=r"Invalid value: 1 for `uuidv4`, must be a `` found: "): 36 | ApplicationDetails(**dict) 37 | cls.fail("ApplicationDetails: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /tests/types/meta/test_kyc_status_response copy.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import pytest 5 | from testing_config import BaseTestConfig 6 | 7 | from xumm.resource.types import ( 8 | KycStatusResponse 9 | ) 10 | 11 | class TestKycStatusResponse(BaseTestConfig): 12 | 13 | def test_kyc_status_response(cls): 14 | print('should set kyc status response') 15 | 16 | dict = cls.json_fixtures['kycStatus']['post'] 17 | cls.assertEqual(KycStatusResponse(**dict).to_dict(), dict) 18 | 19 | def test_kyc_status_response_fail(cls): 20 | print('should fail to set application details') 21 | 22 | dict = { 23 | "kycStatus": "TEMP", # FAILS 24 | "possibleStatuses": { 25 | "NONE": "No KYC attempt has been made", 26 | "IN_PROGRESS": "KYC flow has been started, but did not finish (yet)", 27 | "REJECTED": "KYC flow has been started and rejected (NO SUCCESSFUL KYC)", 28 | "SUCCESSFUL": "KYC flow has been started and was SUCCESSFUL :)" 29 | } 30 | } 31 | 32 | with pytest.raises(ValueError, match=r"Invalid value for `kyc_status` \(TEMP\), must be one of \['NONE', 'IN_PROGRESS', 'REJECTED', 'SUCCESSFUL']"): 33 | KycStatusResponse(**dict) 34 | cls.fail("KycStatusResponse: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /tests/types/payload/test_on_payload_event.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from testing_config import BaseTestConfig 3 | 4 | from xumm.resource.types import ( 5 | SubscriptionCallbackParams 6 | ) 7 | 8 | def resolve_callback(): 9 | print('CALLING BACK') 10 | 11 | class TestOnPayloadEvent(BaseTestConfig): 12 | 13 | def test_on_payload_event(cls): 14 | print('should set on payload event') 15 | 16 | dict = { 17 | 'uuid': '00000000-0000-4839-af2f-f794874a80b0', 18 | 'data': { 19 | 'opened': True 20 | }, 21 | 'payload': cls.json_fixtures['payload']['get'], 22 | 'resolve': resolve_callback, 23 | } 24 | cls.assertEqual(SubscriptionCallbackParams(**dict).to_dict(), dict) 25 | 26 | def test_on_payload_event_fail(cls): 27 | print('should fail to set application details') 28 | 29 | dict = { 30 | 'uuid': '00000000-0000-4839-af2f-f794874a80b0', 31 | 'data': { 32 | 'opened': True 33 | }, 34 | 'payload': None, 35 | 'resolve': resolve_callback, 36 | } 37 | 38 | with pytest.raises(TypeError, match=r'argument after \*\* must be a mapping, not NoneType'): 39 | SubscriptionCallbackParams(**dict) 40 | cls.fail("SubscriptionCallbackParams: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /tests/types/payload/test_subscription_callback_params.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from testing_config import BaseTestConfig 3 | 4 | from xumm.resource.types import ( 5 | SubscriptionCallbackParams 6 | ) 7 | 8 | def resolve_callback(): 9 | print('CALLING BACK') 10 | 11 | class TestSubscriptionCallbackParams(BaseTestConfig): 12 | 13 | def test_subscription_callback_params(cls): 14 | print('should set on payload event') 15 | 16 | dict = { 17 | 'uuid': '00000000-0000-4839-af2f-f794874a80b0', 18 | 'data': { 19 | 'opened': True 20 | }, 21 | 'payload': cls.json_fixtures['payload']['get'], 22 | 'resolve': resolve_callback, 23 | } 24 | cls.assertEqual(SubscriptionCallbackParams(**dict).to_dict(), dict) 25 | 26 | def test_subscription_callback_params_fail(cls): 27 | print('should fail to set application details') 28 | 29 | dict = { 30 | 'uuid': '00000000-0000-4839-af2f-f794874a80b0', 31 | 'data': None, # FAILS 32 | 'payload': cls.json_fixtures['payload']['get'], 33 | 'resolve': resolve_callback, 34 | } 35 | 36 | with pytest.raises(ValueError, match=r'Invalid value for `data`, must not be `None`'): 37 | SubscriptionCallbackParams(**dict) 38 | cls.fail("SubscriptionCallbackParams: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /tests/types/meta/test_pong_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import pytest 5 | from testing_config import BaseTestConfig 6 | 7 | from xumm.resource.types import ( 8 | PongResponse 9 | ) 10 | 11 | class TestPongResponse(BaseTestConfig): 12 | 13 | def test_pong_response(cls): 14 | print('should set pong response') 15 | 16 | dict = cls.json_fixtures['ping']['pong'] 17 | cls.assertEqual(PongResponse(**dict).to_dict(), dict) 18 | 19 | def test_pong_response_fail(cls): 20 | print('should fail to pong details') 21 | 22 | dict = { 23 | "pong": 'true', # FAILS 24 | "auth": { 25 | "quota": {}, 26 | "application": { 27 | "uuidv4": "00000000-0000-4839-af2f-f794874a80b0", 28 | "name": "SomeApplication", 29 | "webhookurl": "https://webhook.site/00000000-0000-4e34-8112-c4391247a8ee", 30 | "disabled": 0 31 | }, 32 | "call": { 33 | "uuidv4": "2904b05f-5b37-4f3e-a624-940ad817943c" 34 | } 35 | } 36 | } 37 | 38 | with pytest.raises(ValueError, match=r"Invalid value: true for `pong`, must be a `` found: "): 39 | PongResponse(**dict) 40 | cls.fail("PongResponse: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /tests/types/xumm_api/test_xumm_api_types.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from testing_config import BaseTestConfig 3 | 4 | from xumm.resource.types import ( 5 | XummWebhookBody 6 | ) 7 | 8 | class TestXummApiTypes(BaseTestConfig): 9 | 10 | def test_webhook_body(cls): 11 | print('should set on webhook body') 12 | 13 | dict = { 14 | 'meta': cls.json_fixtures['webhook']['meta'], 15 | 'custom_meta': cls.json_fixtures['webhook']['custom_meta'], 16 | 'payloadResponse': cls.json_fixtures['webhook']['payloadResponse'], 17 | # 'userToken': cls.json_fixtures['webhook']['userToken'], # Can be null 18 | } 19 | cls.assertEqual(XummWebhookBody(**dict).to_dict(), dict) 20 | 21 | def test_webhook_body_fail(cls): 22 | print('should fail to set webhook body') 23 | 24 | dict = { 25 | 'meta': cls.json_fixtures['webhook']['meta'], 26 | 'custom_meta': cls.json_fixtures['webhook']['custom_meta'], 27 | 'payloadResponse': cls.json_fixtures['webhook']['payloadResponse'], 28 | 'userToken': cls.json_fixtures['webhook']['userToken'], 29 | } 30 | dict['payloadResponse']['txid'] = None 31 | 32 | with pytest.raises(ValueError, match=r'Invalid value for `txid`, must not be `None`'): 33 | XummWebhookBody(**dict) 34 | cls.fail("XummWebhookBody: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /tests/fixtures/xumm_ws.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import asyncio 5 | import json 6 | from tests.fixtures import xumm_api as test_fixtures 7 | from xumm.util import read_json 8 | 9 | import websockets 10 | import time 11 | 12 | json_fixtures = read_json('./tests/fixtures/xumm_api.json') 13 | 14 | 15 | async def start_server(ws, path): 16 | try: 17 | print('MOCK SOCKET OPEN: {}'.format(ws.open)) 18 | 19 | await ws.send(json.dumps(test_fixtures.subscription_updates()['expire'])) # noqa: E501 20 | print('SENT EXPIRE') 21 | print(test_fixtures.subscription_updates()['expire']) 22 | 23 | await ws.send(json.dumps(test_fixtures.subscription_updates()['opened'])) # noqa: E501 24 | print('SENT OPENED') 25 | print(test_fixtures.subscription_updates()['opened']) 26 | 27 | await ws.send(json.dumps(test_fixtures.subscription_updates()['rejected'])) # noqa: E501 28 | print('SENT REJECTED') 29 | print(test_fixtures.subscription_updates()['rejected']) 30 | 31 | await asyncio.sleep(1) 32 | 33 | except KeyboardInterrupt: 34 | ws.close() 35 | 36 | except Exception as e: 37 | print('on_open Error: {}'.format(e)) 38 | ws.close() 39 | 40 | 41 | async def main(): 42 | print('STARTING SOCKET') 43 | async with websockets.serve(start_server, "127.0.0.1", 8765): 44 | print('SERVING SOCKET') 45 | await asyncio.Future() # run forever 46 | 47 | 48 | # loop = asyncio.get_event_loop() 49 | # loop.run_until_complete(main()) 50 | -------------------------------------------------------------------------------- /xumm/resource/types/push/push_push_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class PushPushResponse(XummResource): 8 | """ 9 | Attributes: 10 | model_types (dict): The key is attribute name 11 | and the value is attribute type. 12 | attribute_map (dict): The key is attribute name 13 | and the value is json key in definition. 14 | """ 15 | required = { 16 | 'pushed': True, 17 | } 18 | 19 | model_types = { 20 | 'pushed': bool, 21 | } 22 | 23 | attribute_map = { 24 | 'pushed': 'pushed', 25 | } 26 | 27 | def refresh_from(cls, **kwargs): 28 | """Returns the dict as a model 29 | 30 | :param kwargs: A dict. 31 | :type: dict 32 | :return: The PushPushResponse of this PushPushResponse. # noqa: E501 33 | :rtype: PushPushResponse 34 | """ 35 | cls.sanity_check(kwargs) 36 | cls._pushed = None 37 | cls.pushed = kwargs['pushed'] 38 | 39 | @property 40 | def pushed(self) -> bool: 41 | """Gets the pushed of this PushPushResponse. 42 | 43 | 44 | :return: The pushed of this PushPushResponse. 45 | :rtype: bool 46 | """ 47 | return self._pushed 48 | 49 | @pushed.setter 50 | def pushed(self, pushed: bool): 51 | """Sets the pushed of this PushPushResponse. 52 | 53 | 54 | :param pushed: The pushed of this PushPushResponse. 55 | :type pushed: bool 56 | """ 57 | self._pushed = pushed 58 | -------------------------------------------------------------------------------- /samples/misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import logging 4 | import asyncio 5 | 6 | import xumm 7 | 8 | class MiscExample: 9 | def __init__(self): 10 | logging.debug('') 11 | self.sdk = xumm.XummSdk('API_KEY', 'API_SECRET') 12 | self.logger = logging.getLogger(self.__module__) 13 | self.logger.setLevel(level=logging.DEBUG) 14 | 15 | async def run(self): 16 | # send ping to the backend and print application name 17 | ping_response = self.sdk.ping() 18 | self.logger.info("Ping response: application name: %s" % ping_response.application.name) 19 | 20 | # get curated assets and print available currencies count 21 | curated_assets_response = self.sdk.get_curated_assets() 22 | self.logger.info("Curated_assets response: currencies count %s" % str(len(curated_assets_response.currencies))) 23 | 24 | 25 | # get kyc status of a xrpl address 26 | address = "rDWLGshgAxSX2G4TEv3gA6QhtLgiXrWQXB" 27 | kyc_status_response = self.sdk.get_kyc_status(address) 28 | self.logger.info("Kyc status for account %s: %s" % (address, kyc_status_response)) 29 | 30 | 31 | # get transaction by hash from ledger 32 | tx_hash = "017DED8F5E20F0335C6F56E3D5EE7EF5F7E83FB81D2904072E665EEA69402567" 33 | get_tx_response = self.sdk.get_transaction(tx_hash) 34 | self.logger.info("transaction balance changes: %s" % get_tx_response.balance_changes) 35 | 36 | 37 | if __name__ == "__main__": 38 | example = MiscExample() 39 | 40 | loop = asyncio.get_event_loop() 41 | loop.run_until_complete(example.run()) 42 | 43 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Xumm Sdk Release Process 2 | 3 | ## Cutting a Release 4 | 5 | The full process for cutting a release is as follows: 6 | 7 | 0. Checkout a new branch: 8 | `git checkout -b v1.0.2` # 1.0.2-release 9 | 10 | 2. Change the version in the setup.py file: 11 | `VERSION = "1.0.2"` 12 | 13 | 3. Add, and commit the changes, push up the branch, and open a PR: 14 | `git add .` 15 | `git commit -m 'RELEASE v1.0.2'` 16 | `git push --set-upstream origin HEAD` 17 | 18 | 4. Open PR request 19 | 20 | `` 21 | 22 | 5. Once the PR is merged, checkout the `main` branch: 23 | `git checkout main` 24 | 25 | 6. Delete `main` branch (Optional): 26 | `git branch -d v1.0.2` 27 | 28 | 7. Make a new Git tag that matches the new version (make sure it is associated with the right commit SHA): FIXUP 29 | `git tag -a v1.0.2 -m "cut v1.0.2"` 30 | 31 | 8. Push up the tag from `main`: 32 | `git push origin v1.0.2` 33 | 34 | ## Packaging & Releasing 35 | 36 | Update pip build (optional) 37 | 38 | `python3 -m pip install --upgrade build` 39 | 40 | Build Repo 41 | 42 | `python3 -m build` 43 | 44 | ``` 45 | dist/ 46 | xumm-sdk-py-1.0.2-py3-none-any.whl 47 | xumm-sdk-py-1.0.2.tar.gz 48 | ``` 49 | 50 | Install Twine 51 | 52 | `python3 -m pip install --upgrade twine` 53 | 54 | Config .pypirc 55 | 56 | ``` 57 | [pypi] 58 | username = __token__ 59 | password = pypi-TokenString 60 | ``` 61 | 62 | Check Distribution 63 | 64 | `twine check dist/*` 65 | 66 | Push on Staging 67 | 68 | `twine upload --skip-existing --config-file="./.pypirc" -r testpypi dist/*` 69 | 70 | Push to Production 71 | 72 | `twine upload --config-file="./.pypirc" -r dist/*` 73 | -------------------------------------------------------------------------------- /tests/types/payload/test_payload_subscription.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pytest 3 | from testing_config import BaseTestConfig 4 | 5 | from xumm.ws_client import WSClient 6 | from xumm.resource.types import ( 7 | PayloadSubscription 8 | ) 9 | 10 | def resolve_callback(*args): 11 | print('CALLING BACK') 12 | resolved_callback(args) 13 | 14 | def resolved_callback(*args): 15 | print('CALLING BACK') 16 | time.sleep(3) 17 | return args 18 | 19 | class TestPayloadSubscription(BaseTestConfig): 20 | 21 | def test_payload_subscription(cls): 22 | print('should set subscription response') 23 | 24 | websocket_conn = WSClient( 25 | server='ws://localhost:8765', # noqa: E501 26 | ) 27 | 28 | dict = { 29 | 'payload': cls.json_fixtures['payload']['get'], 30 | 'resolve': resolve_callback, 31 | 'resolved': resolved_callback, 32 | 'websocket': websocket_conn, 33 | } 34 | cls.assertEqual(PayloadSubscription(**dict).to_dict(), dict) 35 | 36 | def test_payload_subscription_fail(cls): 37 | print('should fail to set subscription response') 38 | 39 | dict = { 40 | 'payload': cls.json_fixtures['payload']['get'], 41 | 'resolve': resolve_callback, 42 | 'resolved': resolved_callback, 43 | 'websocket': None, # FAILS 44 | } 45 | 46 | with pytest.raises(ValueError, match=r'Invalid value for `websocket`, must not be `None`'): 47 | PayloadSubscription(**dict) 48 | cls.fail("PayloadAndSubscription: raised Exception unexpectedly!") 49 | 50 | 51 | -------------------------------------------------------------------------------- /samples/storage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import logging 4 | import asyncio 5 | 6 | import xumm 7 | 8 | 9 | class StorageExample: 10 | def __init__(self): 11 | logging.debug('') 12 | self.sdk = xumm.XummSdk('API_KEY', 'API_SECRET') 13 | self.logger = logging.getLogger(self.__module__) 14 | self.logger.setLevel(level=logging.DEBUG) 15 | 16 | async def run(self): 17 | # storge some json value in storage 18 | self.logger.info("Set storage value") 19 | set_storage_result = self.sdk.storage.set({'name': 'Wietse', 'age': 32, 'male': True}) 20 | # True 21 | 22 | if not set_storage_result: 23 | self.logger.error("Unable to set to storage: %s" % e) 24 | return 25 | 26 | # GET the storage content 27 | get_storage_result = self.sdk.storage.get() 28 | self.logger.info("Current storage value: %s" % get_storage_result.data) 29 | # { 'name': 'Wietse', 'age': 32, 'male': True } 30 | 31 | 32 | self.logger.info("Delete storage value") 33 | delete_storage_result = self.sdk.storage.delete() 34 | 35 | if not delete_storage_result: 36 | self.logger.error("Unable to delete the storage: %s" % delete_storage_result) 37 | 38 | get_storage_result_after_delete = self.sdk.storage.get() 39 | self.logger.info("Current storage value after delete: %s" % get_storage_result_after_delete.data) 40 | # None 41 | 42 | 43 | 44 | if __name__ == "__main__": 45 | example = StorageExample() 46 | 47 | loop = asyncio.get_event_loop() 48 | loop.run_until_complete(example.run()) 49 | 50 | -------------------------------------------------------------------------------- /tests/test_push.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from testing_config import BaseTestConfig 5 | from unittest.mock import Mock, patch 6 | 7 | from xumm.resource.types import ( 8 | XummPushEventRequest, 9 | ) 10 | 11 | import xumm 12 | 13 | class TestXapp(BaseTestConfig): 14 | 15 | token: str = '' 16 | 17 | @classmethod 18 | def setUp(cls): 19 | xumm.api_key = cls.json_fixtures['api']['key'] 20 | xumm.api_secret = cls.json_fixtures['api']['secret'] 21 | cls.sdk = xumm.XummSdk() 22 | 23 | @patch('xumm.client.requests.post') 24 | def test_push_event(cls, mock_post): 25 | print('should set push event') 26 | 27 | mock_post.return_value = Mock(status_code=200) 28 | mock_post.return_value.json.return_value = cls.json_fixtures['xApp']['event'] 29 | 30 | payload: XummPushEventRequest = XummPushEventRequest( 31 | user_token='', 32 | body='', 33 | ) 34 | result = cls.sdk.push.event(payload) 35 | cls.assertEqual(result.to_dict(), cls.json_fixtures['xApp']['event']) 36 | 37 | @patch('xumm.client.requests.post') 38 | def test_push_push(cls, mock_post): 39 | print('should set push event') 40 | 41 | mock_post.return_value = Mock(status_code=200) 42 | mock_post.return_value.json.return_value = cls.json_fixtures['xApp']['push'] 43 | 44 | payload: XummPushEventRequest = XummPushEventRequest( 45 | user_token='', 46 | body='', 47 | ) 48 | result = cls.sdk.push.push(payload) 49 | cls.assertEqual(result.to_dict(), cls.json_fixtures['xApp']['push']) -------------------------------------------------------------------------------- /xumm/error.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from typing import Dict # noqa: F401 5 | 6 | 7 | class RPCError(Exception): 8 | def __init__( 9 | cls, 10 | error: str, 11 | status_code: int = None, 12 | headers: Dict[str, object] = None 13 | ): 14 | super(RPCError, cls).__init__(error) 15 | 16 | cls.error = error 17 | cls.status_code = status_code 18 | cls.headers = headers 19 | 20 | def __unicode__(cls) -> str: 21 | return cls.error 22 | 23 | 24 | class APIError(RPCError): 25 | pass 26 | 27 | 28 | class APIConnectionError(RPCError): 29 | pass 30 | 31 | 32 | class InvalidRequestError(RPCError): 33 | pass 34 | 35 | 36 | class AuthenticationError(RPCError): 37 | pass 38 | 39 | 40 | class WSError(Exception): 41 | def __init__( 42 | self, 43 | message: str = None, 44 | data: Dict[str, object] = None 45 | ): 46 | super(WSError, self).__init__(message) 47 | 48 | self.message = message 49 | self.data = data 50 | 51 | def __str__(self) -> str: 52 | result = '[(' + self.message 53 | if self.data: 54 | result += ', ' + str(self.data) 55 | result += ')]' 56 | return result 57 | 58 | 59 | class UnexpectedError(WSError): 60 | pass 61 | 62 | 63 | class ConnectionError(WSError): 64 | pass 65 | 66 | 67 | class NotConnectedError(ConnectionError): 68 | pass 69 | 70 | 71 | class DisconnectedError(ConnectionError): 72 | pass 73 | 74 | 75 | class TimeoutError(ConnectionError): 76 | pass 77 | 78 | 79 | class ResponseFormatError(ConnectionError): 80 | pass 81 | -------------------------------------------------------------------------------- /tests/test_app_storage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from testing_config import BaseTestConfig 5 | from unittest.mock import Mock, patch 6 | 7 | import xumm 8 | 9 | class TestAppStorage(BaseTestConfig): 10 | 11 | @classmethod 12 | def setUp(cls): 13 | xumm.api_key = cls.json_fixtures['api']['key'] 14 | xumm.api_secret = cls.json_fixtures['api']['secret'] 15 | cls.sdk = xumm.XummSdk() 16 | 17 | @patch('xumm.client.requests.post') 18 | def test_storage_set(cls, mock_post): 19 | print('should set app storage') 20 | 21 | mock_post.return_value = Mock(status_code=200) 22 | mock_post.return_value.json.return_value = cls.json_fixtures['storage']['setResponse'] 23 | result = cls.sdk.storage.set({'name': 'Wietse'}) 24 | cls.assertEqual(result.to_dict(), cls.json_fixtures['storage']['setResponse']) 25 | 26 | @patch('xumm.client.requests.get') 27 | def test_storage_get(cls, mock_get): 28 | print('should get app storage') 29 | 30 | mock_get.return_value = Mock(status_code=200) 31 | mock_get.return_value.json.return_value = cls.json_fixtures['storage']['getResponse'] 32 | cls.assertEqual(cls.sdk.storage.get().to_dict(), cls.json_fixtures['storage']['getResponse']) 33 | 34 | @patch('xumm.client.requests.delete') 35 | def test_storage_delete(cls, mock_delete): 36 | print('should clear app storage') 37 | 38 | mock_delete.return_value = Mock(status_code=200) 39 | mock_delete.return_value.json.return_value = cls.json_fixtures['storage']['deleteResponse'] 40 | cls.assertEqual(cls.sdk.storage.delete().to_dict(), cls.json_fixtures['storage']['deleteResponse']) -------------------------------------------------------------------------------- /samples/oauth2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import logging 4 | 5 | import xumm 6 | from xumm.util import parse_oauth2_response 7 | 8 | 9 | class Oauth2Example: 10 | def __init__(self): 11 | logging.debug('') 12 | self.sdk = xumm.XummSdk('API_KEY', 'API_SECRET') 13 | self.logger = logging.getLogger(self.__module__) 14 | self.logger.setLevel(level=logging.DEBUG) 15 | 16 | def oauth_example(self): 17 | redirect_url: str = 'http://localhost:3000' 18 | auth_url, pre_state = self.sdk.oauth2.auth(redirect_url) 19 | print(auth_url) 20 | print(pre_state) 21 | 22 | # FOR TESTING NON HTTP REDIRECT URI 23 | import os 24 | os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' 25 | # FOR TESTING NON HTTP REDIRECT URI 26 | 27 | # 'http://localhost:3000/?authorization_code=gAAAAABjgAd2TCQb7Fj1VACAaCRHl_dUsMlsSsrgYY-rDnoUG4KsvN9XXrO3U5_IfynzSZ2lyGvznGY8JsusHTo2fsWQD5jWwUxfoQlclVYsbPyWZO6jOxB5YXKFdg4HOlezpiCfvVDPEd5b7LJVlEb9kuDyhirucfb3OGJrsJPIMtqYwOMgReeIgnCm3vudDLV4CHZLr9RY&code=gAAAAABjgAd2TCQb7Fj1VACAaCRHl_dUsMlsSsrgYY-rDnoUG4KsvN9XXrO3U5_IfynzSZ2lyGvznGY8JsusHTo2fsWQD5jWwUxfoQlclVYsbPyWZO6jOxB5YXKFdg4HOlezpiCfvVDPEd5b7LJVlEb9kuDyhirucfb3OGJrsJPIMtqYwOMgReeIgnCm3vudDLV4CHZLr9RY&state=NCnOPAxGX3ZIb27vAyWqPT9PzDrTR6' 28 | response_url = input("\n\nPlease paste the response entire url here: ") 29 | code, post_state = parse_oauth2_response(response_url) 30 | 31 | # pre_state != post_state => FAIL 32 | 33 | oauth2_config = self.sdk.oauth2.token(redirect_url, code) 34 | res = self.sdk.oauth2.userinfo(oauth2_config.access_token) 35 | print(res.to_dict()) 36 | 37 | 38 | if __name__ == "__main__": 39 | Oauth2Example().oauth_example() 40 | -------------------------------------------------------------------------------- /xumm/resource/push.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm import client 5 | from xumm.resource import XummResource 6 | 7 | from .types import ( 8 | PushEventResponse, 9 | PushPushResponse, 10 | XummPushEventRequest, 11 | ) 12 | 13 | 14 | class PushResource(XummResource): 15 | 16 | @classmethod 17 | def event_url(cls) -> str: 18 | """ 19 | Gets the POST url of this PushResource 20 | 21 | :return: The POST url of this PushResource. 22 | :rtype: str 23 | """ 24 | return super(PushResource, cls).platform_url() + 'xapp/event' + '/' # noqa: E501 25 | 26 | @classmethod 27 | def push_url(cls) -> str: 28 | """ 29 | Gets the POST url of this PushResource 30 | 31 | :return: The POST url of this PushResource. 32 | :rtype: str 33 | """ 34 | return super(PushResource, cls).platform_url() + 'xapp/push' + '/' # noqa: E501 35 | 36 | def refresh_from(cls, **kwargs) -> 'PushResource': 37 | return cls 38 | 39 | def event(cls, payload: XummPushEventRequest) -> PushEventResponse: 40 | """Returns the dict as a model 41 | 42 | :return: The PushEventResponse of this PushEventResponse. # noqa: E501 43 | :rtype: PushEventResponse 44 | """ 45 | 46 | res = client.post(cls.event_url(), payload.to_dict()) 47 | return PushEventResponse(**res) 48 | 49 | def push(cls, payload: XummPushEventRequest) -> PushPushResponse: 50 | """Returns the dict as a model 51 | 52 | :return: The PushPushResponse of this PushPushResponse. # noqa: E501 53 | :rtype: PushPushResponse 54 | """ 55 | 56 | res = client.post(cls.push_url(), payload.to_dict()) 57 | return PushPushResponse(**res) 58 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | import os 3 | from setuptools import setup, find_packages 4 | from codecs import open 5 | 6 | NAME = "xumm-sdk-py" 7 | VERSION = "1.0.4" 8 | 9 | # Get the long description from the README.md file 10 | base_dir = os.path.abspath(os.path.dirname(__file__)) 11 | with open(os.path.join(base_dir, 'README.md'), encoding='utf-8') as f: 12 | long_description = f.read() 13 | 14 | setup( 15 | name=NAME, 16 | version=VERSION, 17 | description='Xumm SDK for Python', 18 | long_description=long_description, 19 | long_description_content_type='text/markdown', 20 | license='MIT', 21 | author='XRPL-Labs', 22 | author_email='support@xrpl-labs.com', 23 | url='https://github.com/XRPL-Labs/xumm-sdk-py', 24 | packages=find_packages(include=('xumm*',)), 25 | include_package_data=True, 26 | install_requires=[ 27 | "requests>=2.26.0,<=2.28.2", 28 | "requests-oauthlib>=1.3.1", 29 | "websocket-client>=1.2.3,<=1.4.2", 30 | "six==1.16.0", 31 | "python-dotenv>=0.19.2,<=0.21.1" 32 | ], 33 | extras_require={ 34 | 'develop': [ 35 | 'pytest==6.2.5', 36 | 'websockets==9.1', 37 | 'flake8==4.0.1' 38 | ] 39 | }, 40 | test_suite='pytest', 41 | tests_require=['pytest'], 42 | classifiers=[ 43 | 'Development Status :: 4 - Beta', 44 | 'Intended Audience :: Developers', 45 | 'License :: OSI Approved :: MIT License', 46 | 'Operating System :: OS Independent', 47 | 'Programming Language :: Python :: 3.6', 48 | 'Programming Language :: Python :: 3.7', 49 | 'Programming Language :: Python :: 3.8', 50 | 'Programming Language :: Python :: 3.9', 51 | ], 52 | keywords='xrp, ledger, ripple, xumm, sdk' 53 | ) 54 | -------------------------------------------------------------------------------- /samples/payload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import logging 4 | import asyncio 5 | 6 | import xumm 7 | 8 | from fixtures import sample_payload 9 | 10 | 11 | class PayloadExample: 12 | def __init__(self): 13 | logging.debug('') 14 | self.sdk = xumm.XummSdk('API_KEY', 'API_SECRET') 15 | self.logger = logging.getLogger(self.__module__) 16 | self.logger.setLevel(level=logging.DEBUG) 17 | 18 | def subscription_callback(self, event): 19 | self.logger.info('Subscription Event data: {}'.format(event['data'])) 20 | if 'expired' in event['data'] or 'signed' in event['data']: 21 | # payload is reolved reuturn the data 22 | return event['data'] 23 | 24 | 25 | async def run(self): 26 | # create the payload by passing the details and get payload UUID 27 | try: 28 | create_payload_resp = self.sdk.payload.create(sample_payload) 29 | self.logger.info("Payload created with id: %s" % create_payload_resp.uuid) 30 | except Exception as e: 31 | self.logger.error("Unable to create the payload: %s" % e) 32 | return 33 | 34 | # start the websocket subscription on this payload and listen for changes 35 | subscription = await self.sdk.payload.subscribe(create_payload_resp.uuid, self.subscription_callback) 36 | # wait for the payload to resolve 37 | resolve_data = await subscription.resolved() 38 | # now we can cancel the subscription 39 | self.sdk.payload.unsubscribe() 40 | 41 | # fetch the payload from backend 42 | get_payload_resp = self.sdk.payload.get(create_payload_resp.uuid) 43 | self.logger.info("Payload resolved: %s" % get_payload_resp.meta.resolved) 44 | 45 | 46 | if __name__ == "__main__": 47 | example = PayloadExample() 48 | 49 | loop = asyncio.get_event_loop() 50 | loop.run_until_complete(example.run()) 51 | 52 | -------------------------------------------------------------------------------- /tests/types/meta/test_user_tokens.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import pytest 5 | from testing_config import BaseTestConfig 6 | 7 | from xumm.resource.types import ( 8 | UserTokenValidity, 9 | UserTokenResponse, 10 | ) 11 | 12 | class TestUserTokens(BaseTestConfig): 13 | 14 | def test_user_token_validity(cls): 15 | print('should set user token validity') 16 | 17 | dict = cls.json_fixtures['userTokens']['tokens'][0] 18 | cls.assertEqual(UserTokenValidity(**dict).to_dict(), dict) 19 | 20 | def test_user_token_validity_fail(cls): 21 | print('should fail to set user token validity') 22 | 23 | dict = { 24 | "user_token": "00000001-1111-2222-af2f-f794874a80b0", 25 | "active": True, 26 | "token_issued": '0', # FAILS 27 | "token_expiration": 3600 28 | } 29 | 30 | with pytest.raises(ValueError, match=r"Invalid value: 0 for `token_issued`, must be a `` found: "): 31 | UserTokenValidity(**dict) 32 | cls.fail("UserTokenValidity: raised Exception unexpectedly!") 33 | 34 | def test_user_token_response(cls): 35 | print('should set user token response') 36 | 37 | dict = cls.json_fixtures['userTokens'] 38 | cls.assertEqual(UserTokenResponse(**dict).to_dict(), dict) 39 | 40 | def test_user_token_response_fail(cls): 41 | print('should fail to set user token response') 42 | 43 | dict = { 44 | "tokens": [{ 45 | "user_token": "00000001-1111-2222-af2f-f794874a80b0", 46 | "active": True, 47 | "token_issued": '0', # FAILS 48 | "token_expiration": 3600 49 | }] 50 | } 51 | 52 | with pytest.raises(ValueError, match=r"Invalid value: 0 for `token_issued`, must be a `` found: "): 53 | UserTokenResponse(**dict) 54 | cls.fail("UserTokenResponse: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /tests/test_payload_create.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import pytest 4 | from testing_config import BaseTestConfig 5 | from tests.fixtures import xumm_api as test_fixtures 6 | from unittest.mock import Mock, patch 7 | 8 | import xumm 9 | 10 | class TestPayloadCreate(BaseTestConfig): 11 | 12 | @classmethod 13 | def setUp(cls): 14 | cls.sdk = xumm.XummSdk( 15 | cls.json_fixtures['api']['key'], 16 | cls.json_fixtures['api']['secret'] 17 | ) 18 | 19 | @patch('xumm.client.requests.post') 20 | def test_payload_create(cls, mock_post): 21 | print('should create a simple payment') 22 | mock_post.return_value = Mock(status_code=200) 23 | mock_post.return_value.json.return_value = cls.json_fixtures['payload']['created'] 24 | result = cls.sdk.payload.create(test_fixtures.valid_payload()) 25 | cls.assertEqual(result.to_dict(), cls.json_fixtures['payload']['created']) 26 | 27 | @patch('xumm.client.requests.post') 28 | def test_payload_create_unwrapped(cls, mock_post): 29 | print('should create a simple payment based on only (unwrapped payload) TX Json') 30 | mock_post.return_value = Mock(status_code=200) 31 | mock_post.return_value.json.return_value = cls.json_fixtures['payload']['created'] 32 | result = cls.sdk.payload.create(test_fixtures.valid_payload()['txjson']) 33 | cls.assertEqual(result.to_dict(), cls.json_fixtures['payload']['created']) 34 | 35 | @patch('xumm.client.requests.post') 36 | def test_payload_create_invalid_errors(cls, mock_post): 37 | print('should throw on invalid payload with `returnErrors`') 38 | 39 | mock_post.return_value = Mock(status_code=400) 40 | mock_post.return_value.json.return_value = cls.json_fixtures['payload']['error'] 41 | 42 | with pytest.raises(xumm.error.InvalidRequestError, match=r"Error code 602, see XUMM Dev Console, reference: a61ba59a-0304-44ae-a86e-efefegewgew4"): 43 | cls.sdk.payload.create(test_fixtures.invalid_payload()) 44 | cls.fail("payload_create() raised Exception unexpectedly!") 45 | 46 | 47 | -------------------------------------------------------------------------------- /xumm/resource/types/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # noqa: E501 4 | 5 | from .xumm_api import * # noqa: F401 F403 6 | 7 | # from .meta.any_json import AnyJson # TODO: add whatever is needed here 8 | from .meta.application_details import ApplicationDetails # noqa: F401 9 | from .meta.curated_assets_response import CuratedAssetsResponse # noqa: F401 10 | from .meta.kyc_status_response import ( # noqa: F401 11 | KycStatusResponse, 12 | # PossibleKycStatuses # noqa: F401 13 | ) 14 | from .meta.kyc_info_response import KycInfoResponse # noqa: F401 15 | from .meta.pong import PongResponse # noqa: F401 16 | from .meta.xrpl_transaction import XrplTransaction # noqa: F401 17 | from .meta.rates_response import RatesResponse # noqa: F401 18 | 19 | from .payload.on_payload_event import on_payload_event # noqa: F401 20 | from .payload.payload_and_subscription import PayloadAndSubscription # noqa: F401 E501 21 | from .payload.payload_subscription import PayloadSubscription # noqa: F401 22 | from .payload.subscription_callback_params import SubscriptionCallbackParams # noqa: F401 E501 23 | 24 | from .storage.storage_delete_response import StorageDeleteResponse # noqa: F401 E501 25 | from .storage.storage_get_response import StorageGetResponse # noqa: F401 26 | from .storage.storage_response import StorageResponse # noqa: F401 27 | from .storage.storage_set_response import StorageSetResponse # noqa: F401 28 | 29 | from .push.push_event_response import PushEventResponse # noqa: F401 30 | from .push.push_push_response import PushPushResponse # noqa: F401 31 | 32 | from .meta.user_tokens import ( # noqa: F401 33 | UserTokenValidity, 34 | UserTokenResponse, 35 | ) 36 | 37 | # /** 38 | # * Aliasses 39 | # */ 40 | from .xumm_api import ( # noqa: F401 41 | XummPostPayloadResponse as CreatedPayload, 42 | XummDeletePayloadResponse as DeletedPayload, 43 | XummGetPayloadResponse as XummPayload, 44 | XummWebhookBody, 45 | XummPushEventRequest, 46 | ) 47 | 48 | from .oauth2.oauth2_token_response import OAuth2TokenResponse # noqa: F401 49 | from .oauth2.oauth2_user_info_response import OAuth2UserInfoResponse # noqa: F401 E501 50 | -------------------------------------------------------------------------------- /tests/types/payload/test_payload_and_subscription.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pytest 3 | 4 | from testing_config import BaseTestConfig 5 | 6 | from xumm.ws_client import WSClient 7 | from xumm.resource.types import ( 8 | PayloadAndSubscription 9 | ) 10 | 11 | def resolve_callback(*args): 12 | print('CALLING BACK') 13 | resolved_callback(args) 14 | 15 | def resolved_callback(*args): 16 | print('CALLING BACK') 17 | time.sleep(3) 18 | return args 19 | 20 | class TestPayloadAndSubscription(BaseTestConfig): 21 | 22 | def test_payload_and_subscription(cls): 23 | print('should set subscription w/created response') 24 | 25 | websocket_conn = WSClient( 26 | server='ws://localhost:8765', # noqa: E501 27 | # on_response=on_message, 28 | # on_error=on_error, 29 | # on_close=on_close, 30 | # on_open=on_open 31 | ) 32 | 33 | dict = { 34 | 'created': cls.json_fixtures['payload']['created'], 35 | 'payload': cls.json_fixtures['payload']['get'], 36 | 'resolve': resolve_callback, 37 | 'resolved': resolved_callback, 38 | 'websocket': websocket_conn, 39 | } 40 | cls.assertEqual(PayloadAndSubscription(**dict).to_dict(), dict) 41 | 42 | def test_payload_and_subscription_fail(cls): 43 | print('should fail to set subscription w/created response') 44 | 45 | websocket_conn = WSClient( 46 | server='ws://localhost:8765', # noqa: E501 47 | # on_response=on_message, 48 | # on_error=on_error, 49 | # on_close=on_close, 50 | # on_open=on_open 51 | ) 52 | 53 | dict = { 54 | 'created': None, # FAILS 55 | 'payload': cls.json_fixtures['payload']['get'], 56 | 'resolve': resolve_callback, 57 | 'resolved': resolved_callback, 58 | 'websocket': websocket_conn, 59 | } 60 | 61 | with pytest.raises(TypeError, match=r'argument after \*\* must be a mapping, not NoneType'): 62 | PayloadAndSubscription(**dict) 63 | cls.fail("PayloadAndSubscription: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /xumm/resource/types/push/push_event_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class PushEventResponse(XummResource): 8 | """ 9 | Attributes: 10 | model_types (dict): The key is attribute name 11 | and the value is attribute type. 12 | attribute_map (dict): The key is attribute name 13 | and the value is json key in definition. 14 | """ 15 | required = { 16 | 'pushed': True, 17 | 'uuid': True, 18 | } 19 | 20 | model_types = { 21 | 'pushed': bool, 22 | 'uuid': str, 23 | } 24 | 25 | attribute_map = { 26 | 'pushed': 'pushed', 27 | 'uuid': 'uuid', 28 | } 29 | 30 | def refresh_from(cls, **kwargs): 31 | """Returns the dict as a model 32 | 33 | :param kwargs: A dict. 34 | :type: dict 35 | :return: The PushEventResponse of this PushEventResponse. # noqa: E501 36 | :rtype: PushEventResponse 37 | """ 38 | cls.sanity_check(kwargs) 39 | cls._pushed = None 40 | cls._uuid = None 41 | cls.pushed = kwargs['pushed'] 42 | cls.uuid = kwargs['uuid'] 43 | 44 | @property 45 | def pushed(self) -> bool: 46 | """Gets the pushed of this PushEventResponse. 47 | 48 | 49 | :return: The pushed of this PushEventResponse. 50 | :rtype: bool 51 | """ 52 | return self._pushed 53 | 54 | @pushed.setter 55 | def pushed(self, pushed: bool): 56 | """Sets the pushed of this PushEventResponse. 57 | 58 | 59 | :param pushed: The pushed of this PushEventResponse. 60 | :type pushed: bool 61 | """ 62 | self._pushed = pushed 63 | 64 | @property 65 | def uuid(self) -> str: 66 | """Gets the uuid of this PushEventResponse. 67 | 68 | 69 | :return: The uuid of this PushEventResponse. 70 | :rtype: str 71 | """ 72 | return self._uuid 73 | 74 | @uuid.setter 75 | def uuid(self, uuid: str): 76 | """Sets the uuid of this PushEventResponse. 77 | 78 | 79 | :param uuid: The uuid of this PushEventResponse. 80 | :type uuid: str 81 | """ 82 | self._uuid = uuid 83 | -------------------------------------------------------------------------------- /tests/types/meta/test_xrpl_transaction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import pytest 5 | from testing_config import BaseTestConfig 6 | 7 | from xumm.resource.types import ( 8 | XrplTransaction 9 | ) 10 | 11 | class TestXrplTransactionResponse(BaseTestConfig): 12 | 13 | def test_xrpl_tx_response(cls): 14 | print('should set xrpl tx response') 15 | 16 | dict = cls.json_fixtures['xrplTx'] 17 | cls.assertEqual(XrplTransaction(**dict).to_dict(), dict) 18 | 19 | def test_xrpl_tx_response_fail(cls): 20 | print('should fail to xrpl tx details') 21 | 22 | dict = { 23 | "txid": 1, # FAILS 24 | "balanceChanges": { 25 | "r4bA4uZgXadPMzURqGLCvCmD48FmXJWHCG": [ 26 | { 27 | "counterparty": "", 28 | "currency": "XRP", 29 | "value": "-1.000012" 30 | } 31 | ], 32 | "rPdvC6ccq8hCdPKSPJkPmyZ4Mi1oG2FFkT": [ 33 | { 34 | "counterparty": "", 35 | "currency": "XRP", 36 | "value": "1" 37 | } 38 | ] 39 | }, 40 | "node": "wss://xrplcluster.com", 41 | "transaction": { 42 | "Account": "r4bA4uZgXadPMzURqGLCvCmD48FmXJWHCG", 43 | "Amount": "1000000", 44 | "Destination": "rPdvC6ccq8hCdPKSPJkPmyZ4Mi1oG2FFkT", 45 | "Fee": "12", 46 | "Flags": 2147483648, 47 | "Sequence": 58549314, 48 | "SigningPubKey": "0260F06C0590C470E7E7FA9DE3D9E85B1825E19196D8893DD84431F6E9491739AC", 49 | "TransactionType": "Payment", 50 | "meta": { 51 | "TransactionIndex": 0, 52 | "TransactionResult": "tesSUCCESS", 53 | "delivered_amount": "1000000" 54 | }, 55 | "validated": True 56 | } 57 | } 58 | 59 | with pytest.raises(ValueError, match=r"Invalid value: 1 for `txid`, must be a `` found: "): 60 | XrplTransaction(**dict) 61 | cls.fail("XrplTransaction: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /tests/test_payload_get.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import pytest 4 | from testing_config import BaseTestConfig 5 | from tests.fixtures import xumm_api as test_fixtures 6 | from unittest.mock import Mock, patch 7 | 8 | import xumm 9 | 10 | 11 | class TestPayloadGet(BaseTestConfig): 12 | 13 | @classmethod 14 | def setUp(cls): 15 | cls.sdk = xumm.XummSdk( 16 | cls.json_fixtures['api']['key'], 17 | cls.json_fixtures['api']['secret'] 18 | ) 19 | 20 | @patch('xumm.client.requests.get') 21 | def test_payload_get(cls, mock_post): 22 | print('should get a simple payment') 23 | mock_post.return_value = Mock(status_code=200) 24 | mock_post.return_value.json.return_value = cls.json_fixtures['payload']['get'] 25 | 26 | result = cls.sdk.payload.get('00000000-0000-4839-af2f-f794874a80b0') 27 | cls.assertEqual(result.to_dict(), cls.json_fixtures['payload']['get']) 28 | 29 | @patch('xumm.client.requests.get') 30 | @patch('xumm.client.requests.post') 31 | def test_payload_create_cancel(cls, mock_post, mock_delete): 32 | print('should get a payload by Created Payload') 33 | 34 | mock_post.return_value = Mock(status_code=200) 35 | mock_post.return_value.json.return_value = cls.json_fixtures['payload']['created'] 36 | 37 | created_payload = cls.sdk.payload.create(test_fixtures.valid_payload()) 38 | if created_payload: 39 | mock_delete.return_value = Mock(status_code=200) 40 | mock_delete.return_value.json.return_value = cls.json_fixtures['payload']['get'] 41 | cls.assertEqual(cls.sdk.payload.get(created_payload.uuid).to_dict(), cls.json_fixtures['payload']['get']) 42 | 43 | @patch('xumm.client.requests.get') 44 | def test_payload_get_invalid_errors(cls, mock_get): 45 | print('should throw on getting an invalid/non existent payload with `returnErrors`') 46 | 47 | mock_get.return_value = Mock(status_code=400) 48 | mock_get.return_value.json.return_value = cls.json_fixtures['payload']['notfound'] 49 | 50 | with pytest.raises(xumm.error.InvalidRequestError, match=r"Error code 404, see XUMM Dev Console, reference: a61ba59a-0304-44ae-a86e-d74808bd5190"): 51 | cls.sdk.payload.get('00000000-0000-4839-af2f-f794874a80b0') 52 | cls.fail("payload_get() raised Exception unexpectedly!") 53 | -------------------------------------------------------------------------------- /xumm/resource/storage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from typing import List, Dict, Any # noqa: F401 5 | 6 | from xumm import client 7 | from xumm.resource import XummResource 8 | 9 | from .types import ( 10 | StorageSetResponse, 11 | StorageGetResponse, 12 | StorageDeleteResponse, 13 | ) 14 | 15 | 16 | class StorageResource(XummResource): 17 | 18 | @classmethod 19 | def post_url(cls) -> str: 20 | """ 21 | Gets the POST url of this StorageResource 22 | 23 | :return: The POST url of this StorageResource. 24 | :rtype: str 25 | """ 26 | return super(StorageResource, cls).platform_url() + 'app-storage' 27 | 28 | @classmethod 29 | def get_url(cls) -> str: 30 | """ 31 | Gets the GET url of this StorageResource 32 | 33 | :return: The GET url of this StorageResource. 34 | :rtype: str 35 | """ 36 | return super(StorageResource, cls).platform_url() + 'app-storage' 37 | 38 | @classmethod 39 | def delete_url(cls) -> str: 40 | """ 41 | Gets the DELETE url of this StorageResource 42 | 43 | :return: The DELETE url of this StorageResource. 44 | :rtype: str 45 | """ 46 | return super(StorageResource, cls).platform_url() + 'app-storage' 47 | 48 | def refresh_from(cls, **kwargs) -> 'StorageResource': 49 | return cls 50 | 51 | def set(cls, data: Dict[str, object]) -> StorageSetResponse: 52 | """Returns the dict as a model 53 | 54 | :return: The StorageSetResponse of this StorageSetResponse. # noqa: E501 55 | :rtype: StorageSetResponse 56 | """ 57 | 58 | res = client.post(cls.post_url(), data) 59 | return StorageSetResponse(**res) 60 | 61 | def get(cls) -> StorageGetResponse: 62 | """Returns the dict as a model 63 | 64 | :return: The StorageGetResponse of this StorageGetResponse. # noqa: E501 65 | :rtype: StorageGetResponse 66 | """ 67 | 68 | res = client.get(cls.get_url()) 69 | return StorageGetResponse(**res) 70 | 71 | def delete(cls) -> StorageDeleteResponse: 72 | """Returns the dict as a model 73 | 74 | :return: The StorageDeleteResponse of this StorageDeleteResponse. # noqa: E501 75 | :rtype: StorageDeleteResponse 76 | """ 77 | 78 | res = client.delete(cls.delete_url()) 79 | return StorageDeleteResponse(**res) 80 | -------------------------------------------------------------------------------- /xumm/resource/types/storage/storage_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class StorageResponse(XummResource): 8 | """ 9 | Attributes: 10 | model_types (dict): The key is attribute name 11 | and the value is attribute type. 12 | attribute_map (dict): The key is attribute name 13 | and the value is json key in definition. 14 | """ 15 | required = { 16 | 'name': True, 17 | 'uuidv4': True 18 | } 19 | 20 | model_types = { 21 | 'name': str, 22 | 'uuidv4': str 23 | } 24 | 25 | attribute_map = { 26 | 'name': 'name', 27 | 'uuidv4': 'uuidv4' 28 | } 29 | 30 | def refresh_from(cls, **kwargs): 31 | """Returns the dict as a model 32 | 33 | :param kwargs: A dict. 34 | :type: dict 35 | :return: The StorageResponse of this StorageResponse. # noqa: E501 36 | :rtype: StorageResponse 37 | """ 38 | cls.sanity_check(kwargs) 39 | cls._name = None 40 | cls._uuidv4 = None 41 | cls.name = kwargs['name'] 42 | cls.uuidv4 = kwargs['uuidv4'] 43 | 44 | @property 45 | def name(cls) -> str: 46 | """Gets the name of this StorageResponse. 47 | 48 | 49 | :return: The name of this StorageResponse. 50 | :rtype: str 51 | """ 52 | return cls._name 53 | 54 | @name.setter 55 | def name(cls, name: str): 56 | """Sets the name of this StorageResponse. 57 | 58 | 59 | :param name: The name of this StorageResponse. 60 | :type name: str 61 | """ 62 | if name is None: 63 | raise ValueError("Invalid value for `name`, must not be `None`") # noqa: E501 64 | 65 | cls._name = name 66 | 67 | @property 68 | def uuidv4(cls) -> str: 69 | """Gets the uuidv4 of this StorageResponse. 70 | 71 | 72 | :return: The uuidv4 of this StorageResponse. 73 | :rtype: str 74 | """ 75 | return cls._uuidv4 76 | 77 | @uuidv4.setter 78 | def uuidv4(cls, uuidv4: str): 79 | """Sets the uuidv4 of this StorageResponse. 80 | 81 | 82 | :param uuidv4: The uuidv4 of this StorageResponse. 83 | :type uuidv4: str 84 | """ 85 | if uuidv4 is None: 86 | raise ValueError("Invalid value for `uuidv4`, must not be `None`") # noqa: E501 87 | 88 | cls._uuidv4 = uuidv4 89 | -------------------------------------------------------------------------------- /xumm/resource/types/meta/pong.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | from .application_details import ApplicationDetails 7 | 8 | 9 | class PongResponse(XummResource): 10 | """ 11 | Attributes: 12 | model_types (dict): The key is attribute name 13 | and the value is attribute type. 14 | attribute_map (dict): The key is attribute name 15 | and the value is json key in definition. 16 | """ 17 | required = { 18 | 'pong': True, 19 | 'auth': True 20 | } 21 | 22 | model_types = { 23 | 'pong': bool, 24 | 'auth': dict 25 | } 26 | 27 | attribute_map = { 28 | 'pong': 'pong', 29 | 'auth': 'auth', 30 | } 31 | 32 | def refresh_from(cls, **kwargs): 33 | """Returns the dict as a model 34 | 35 | :param dikt: A dict. 36 | :type: dict 37 | :return: The PongResponse of this PongResponse. # noqa: E501 38 | :rtype: PongResponse 39 | """ 40 | cls.sanity_check(kwargs) 41 | cls._pong = None 42 | cls._auth = None 43 | cls.pong = kwargs['pong'] 44 | cls.auth = ApplicationDetails(**kwargs['auth']) 45 | 46 | @property 47 | def pong(cls) -> bool: 48 | """Gets the pong of this PongResponse. 49 | 50 | 51 | :return: The pong of this PongResponse. 52 | :rtype: bool 53 | """ 54 | return cls._pong 55 | 56 | @pong.setter 57 | def pong(cls, pong: bool): 58 | """Sets the pong of this PongResponse. 59 | 60 | 61 | :param pong: The pong of this PongResponse. 62 | :type pong: bool 63 | """ 64 | if pong is None: 65 | raise ValueError("Invalid value for `pong`, must not be `None`") # noqa: E501 66 | 67 | cls._pong = pong 68 | 69 | @property 70 | def auth(cls) -> ApplicationDetails: 71 | """Gets the auth of this PongResponse. 72 | 73 | 74 | :return: The auth of this PongResponse. 75 | :rtype: ApplicationDetails 76 | """ 77 | return cls._auth 78 | 79 | @auth.setter 80 | def auth(cls, auth: ApplicationDetails): 81 | """Sets the auth of this PongResponse. 82 | 83 | 84 | :param auth: The auth of this PongResponse. 85 | :type auth: ApplicationDetails 86 | """ 87 | if auth is None: 88 | raise ValueError("Invalid value for `auth`, must not be `None`") # noqa: E501 89 | 90 | cls._auth = auth 91 | -------------------------------------------------------------------------------- /xumm/resource/types/meta/kyc_info_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class KycInfoResponse(XummResource): 8 | """ 9 | Attributes: 10 | model_types (dict): The key is attribute name 11 | and the value is attribute type. 12 | attribute_map (dict): The key is attribute name 13 | and the value is json key in definition. 14 | """ 15 | required = { 16 | 'account': True, 17 | 'kyc_approved': True, 18 | } 19 | 20 | model_types = { 21 | 'account': str, 22 | 'kyc_approved': bool 23 | } 24 | 25 | attribute_map = { 26 | 'account': 'account', 27 | 'kyc_approved': 'kycApproved' 28 | } 29 | 30 | def refresh_from(cls, **kwargs): 31 | """Returns the dict as a model 32 | 33 | :param kwargs: A dict. 34 | :type: dict 35 | :return: The KycInfoResponse of this KycInfoResponse. # noqa: E501 36 | :rtype: KycInfoResponse 37 | """ 38 | cls.sanity_check(kwargs) 39 | cls._account = None 40 | cls._kyc_approved = None 41 | cls.account = kwargs['account'] 42 | cls.kyc_approved = kwargs['kycApproved'] 43 | 44 | @property 45 | def account(cls) -> str: 46 | """Gets the account of this KycInfoResponse. 47 | 48 | 49 | :return: The account of this KycInfoResponse. 50 | :rtype: str 51 | """ 52 | return cls._account 53 | 54 | @account.setter 55 | def account(cls, account: str): 56 | """Sets the account of this KycInfoResponse. 57 | 58 | 59 | :param account: The account of this KycInfoResponse. 60 | :type account: str 61 | """ 62 | if account is None: 63 | raise ValueError("Invalid value for `account`, must not be `None`") # noqa: E501 64 | 65 | cls._account = account 66 | 67 | @property 68 | def kyc_approved(cls) -> bool: 69 | """Gets the kyc_approved of this KycInfoResponse. 70 | 71 | 72 | :return: The kyc_approved of this KycInfoResponse. 73 | :rtype: bool 74 | """ 75 | return cls._kyc_approved 76 | 77 | @kyc_approved.setter 78 | def kyc_approved(cls, kyc_approved: bool): 79 | """Sets the kyc_approved of this KycInfoResponse. 80 | 81 | 82 | :param kyc_approved: The kyc_approved of this KycInfoResponse. 83 | :type kyc_approved: bool 84 | """ 85 | if kyc_approved is None: 86 | raise ValueError("Invalid value for `kyc_approved`, must not be `None`") # noqa: E501 87 | 88 | cls._kyc_approved = kyc_approved 89 | -------------------------------------------------------------------------------- /xumm/resource/types/storage/storage_get_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | from typing import Dict 6 | 7 | from .storage_response import StorageResponse 8 | 9 | 10 | class StorageGetResponse(XummResource): 11 | """ 12 | Attributes: 13 | model_types (dict): The key is attribute name 14 | and the value is attribute type. 15 | attribute_map (dict): The key is attribute name 16 | and the value is json key in definition. 17 | """ 18 | required = { 19 | 'application': True, 20 | 'data': True, 21 | } 22 | 23 | model_types = { 24 | 'application': dict, 25 | 'data': dict, 26 | } 27 | 28 | attribute_map = { 29 | 'application': 'application', 30 | 'data': 'data', 31 | } 32 | 33 | def refresh_from(cls, **kwargs): 34 | """Returns the dict as a model 35 | 36 | :param kwargs: A dict. 37 | :type: dict 38 | :return: The StorageGetResponse of this StorageGetResponse. # noqa: E501 39 | :rtype: StorageGetResponse 40 | """ 41 | cls.sanity_check(kwargs) 42 | cls._application = None 43 | cls._data = None 44 | cls.application = StorageResponse(**kwargs['application']) 45 | cls.data = kwargs['data'] 46 | 47 | @property 48 | def application(self) -> StorageResponse: 49 | """Gets the application of this StorageGetResponse. 50 | 51 | 52 | :return: The application of this StorageGetResponse. 53 | :rtype: StorageResponse 54 | """ 55 | return self._application 56 | 57 | @application.setter 58 | def application(self, application: StorageResponse): 59 | """Sets the application of this StorageGetResponse. 60 | 61 | 62 | :param application: The application of this StorageGetResponse. 63 | :type application: StorageResponse 64 | """ 65 | if application is None: 66 | raise ValueError("Invalid value for `application`, must not be `None`") # noqa: E501 67 | 68 | self._application = application 69 | 70 | @property 71 | def data(self) -> Dict[str, object]: 72 | """Gets the data of this StorageGetResponse. 73 | 74 | 75 | :return: The data of this StorageGetResponse. 76 | :rtype: Dict[str, object] 77 | """ 78 | return self._data 79 | 80 | @data.setter 81 | def data(self, data: Dict[str, object]): 82 | """Sets the data of this StorageGetResponse. 83 | 84 | 85 | :param data: The data of this StorageGetResponse. 86 | :type data: Dict[str, object] 87 | """ 88 | self._data = data 89 | -------------------------------------------------------------------------------- /xumm/resource/oauth2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from typing import List, Dict, Any, Tuple # noqa: F401 5 | 6 | from xumm import client 7 | from xumm.resource import XummResource 8 | 9 | from requests_oauthlib import OAuth2Session 10 | 11 | from .types import OAuth2TokenResponse, OAuth2UserInfoResponse 12 | 13 | 14 | class OAuth2Resource(XummResource): 15 | 16 | @classmethod 17 | def auth_url(cls) -> str: 18 | """ 19 | Gets the AUTH url of this OAuth2Resource 20 | 21 | :return: The AUTH url of this OAuth2Resource. 22 | :rtype: str 23 | """ 24 | return super(OAuth2Resource, cls).oauth2_url() + 'auth' 25 | 26 | @classmethod 27 | def token_url(cls) -> str: 28 | """ 29 | Gets the TOKEN url of this OAuth2Resource 30 | 31 | :return: The TOKEN url of this OAuth2Resource. 32 | :rtype: str 33 | """ 34 | return super(OAuth2Resource, cls).oauth2_url() + 'token' 35 | 36 | @classmethod 37 | def userinfo_url(cls) -> str: 38 | """ 39 | Gets the USERINFO url of this OAuth2Resource 40 | 41 | :return: The USERINFO url of this OAuth2Resource. 42 | :rtype: str 43 | """ 44 | return super(OAuth2Resource, cls).oauth2_url() + 'userinfo' 45 | 46 | def refresh_from(cls, **kwargs) -> 'OAuth2Resource': 47 | return cls 48 | 49 | def auth(cls, redirect_uri: str) -> Tuple[str, str]: 50 | """Returns the dict as a model 51 | 52 | :return: The authorization url and the state. # noqa: E501 53 | :rtype: Tuple[str, str] 54 | """ 55 | from xumm import api_key 56 | oauth = OAuth2Session(api_key, redirect_uri=redirect_uri) 57 | return oauth.authorization_url(cls.auth_url()) 58 | 59 | def token(cls, redirect_uri: str, code: str) -> OAuth2TokenResponse: # noqa: E501 60 | """Returns the dict as a model 61 | 62 | :return: The OAuth2TokenResponse of this OAuth2TokenResponse. # noqa: E501 63 | :rtype: OAuth2TokenResponse 64 | """ 65 | 66 | from xumm import api_key, api_secret 67 | oauth = OAuth2Session(api_key, redirect_uri=redirect_uri) 68 | token = oauth.fetch_token( 69 | cls.token_url(), 70 | client_secret=api_secret, 71 | code=code 72 | ) 73 | return OAuth2TokenResponse(**token) 74 | 75 | def userinfo(cls, access_token: str) -> OAuth2UserInfoResponse: 76 | """Returns the dict as a model 77 | 78 | :return: The OAuth2UserInfoResponse of this OAuth2UserInfoResponse. # noqa: E501 79 | :rtype: OAuth2UserInfoResponse 80 | """ 81 | res = client.get(cls.userinfo_url(), access_token) 82 | return OAuth2UserInfoResponse(**res) 83 | -------------------------------------------------------------------------------- /testing_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import asyncio 5 | import contextlib 6 | import functools 7 | import logging 8 | 9 | from unittest import TestCase, defaultTestLoader 10 | 11 | from xumm.util import read_json 12 | 13 | 14 | class BaseTestConfig(TestCase): 15 | 16 | sdk = None 17 | json_fixtures = {} 18 | 19 | @classmethod 20 | def setUpClass(cls): 21 | print('Set Up Class Testing') 22 | cls.json_fixtures = read_json('./tests/fixtures/xumm_api.json') 23 | 24 | @classmethod 25 | def tearDownClass(cls): 26 | print('Tear Down Class Testing') 27 | 28 | 29 | class AsyncioTestCase(BaseTestConfig): 30 | """ 31 | Base class for tests that sets up an isolated event loop for each test. 32 | """ 33 | 34 | def __init_subclass__(cls, **kwargs): 35 | """ 36 | Convert test coroutines to test functions. 37 | This supports asychronous tests transparently. 38 | """ 39 | super().__init_subclass__(**kwargs) 40 | for name in defaultTestLoader.getTestCaseNames(cls): 41 | test = getattr(cls, name) 42 | if asyncio.iscoroutinefunction(test): 43 | setattr(cls, name, cls.convert_async_to_sync(test)) 44 | 45 | @staticmethod 46 | def convert_async_to_sync(test): 47 | """ 48 | Convert a test coroutine to a test function. 49 | """ 50 | 51 | @functools.wraps(test) 52 | def test_func(self, *args, **kwargs): 53 | return self.loop.run_until_complete(test(self, *args, **kwargs)) 54 | 55 | return test_func 56 | 57 | def setUp(self): 58 | super().setUp() 59 | self.loop = asyncio.new_event_loop() 60 | asyncio.set_event_loop(self.loop) 61 | 62 | def tearDown(self): 63 | self.loop.close() 64 | super().tearDown() 65 | 66 | def run_loop_once(self): 67 | # Process callbacks scheduled with call_soon by appending a callback 68 | # to stop the event loop then running it until it hits that callback. 69 | self.loop.call_soon(self.loop.stop) 70 | self.loop.run_forever() 71 | 72 | @contextlib.contextmanager 73 | def assertNoLogs(self, logger="websockets", level=logging.ERROR): 74 | """ 75 | No message is logged on the given logger with at least the given level. 76 | """ 77 | with self.assertLogs(logger, level) as logs: 78 | # We want to test that no log message is emitted 79 | # but assertLogs expects at least one log message. 80 | logging.getLogger(logger).log(level, "dummy") 81 | yield 82 | 83 | level_name = logging.getLevelName(level) 84 | self.assertEqual(logs.output, [f"{level_name}:{logger}:dummy"]) 85 | 86 | def assertDeprecationWarnings(self, recorded_warnings, expected_warnings): 87 | """ 88 | Check recorded deprecation warnings match a list of expected messages. 89 | """ 90 | for recorded in recorded_warnings: 91 | self.assertEqual(type(recorded.message), DeprecationWarning) 92 | self.assertEqual( 93 | set(str(recorded.message) for recorded in recorded_warnings), 94 | set(expected_warnings), 95 | ) 96 | -------------------------------------------------------------------------------- /tests/test_payload_cancel.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import pytest 5 | 6 | from testing_config import BaseTestConfig 7 | from tests.fixtures import xumm_api as test_fixtures 8 | from unittest.mock import Mock, patch 9 | 10 | import xumm 11 | 12 | 13 | class TestPayloadCancel(BaseTestConfig): 14 | 15 | @classmethod 16 | def setUp(cls): 17 | cls.sdk = xumm.XummSdk( 18 | cls.json_fixtures['api']['key'], 19 | cls.json_fixtures['api']['secret'] 20 | ) 21 | 22 | @patch('xumm.client.requests.delete') 23 | def test_payload_cancel(cls, mock_delete): 24 | print('should cancel a payload by UUID') 25 | payloadId = '00000000-0000-4839-af2f-f794874a80b0' 26 | 27 | mock_delete.return_value = Mock(status_code=200) 28 | mock_delete.return_value.json.return_value = cls.json_fixtures['payload']['cancelled'] 29 | 30 | cls.assertEqual(cls.sdk.payload.cancel(payloadId).to_dict(), cls.json_fixtures['payload']['cancelled']) 31 | 32 | @patch('xumm.client.requests.delete') 33 | def test_payload_not_found_errors(cls, mock_delete): 34 | print('should throw if payload not found with `returnErrors`') 35 | payloadId = '00000000-0000-4839-af2f-f794874a80b0' 36 | 37 | mock_delete.return_value = Mock(status_code=404) 38 | mock_delete.return_value.json.return_value = cls.json_fixtures['payload']['notfound'] 39 | 40 | with pytest.raises(xumm.error.InvalidRequestError, match=r"Error code 404, see XUMM Dev Console, reference: a61ba59a-0304-44ae-a86e-d74808bd5190"): 41 | cls.sdk.payload.cancel(payloadId) 42 | cls.fail("payload_cancel() raised Exception unexpectedly!") 43 | 44 | @patch('xumm.client.requests.delete') 45 | @patch('xumm.client.requests.post') 46 | def test_payload_create_cancel(cls, mock_post, mock_delete): 47 | print('should cancel a payload by Created Payload') 48 | 49 | mock_post.return_value = Mock(status_code=200) 50 | mock_post.return_value.json.return_value = cls.json_fixtures['payload']['created'] 51 | 52 | created_payload = cls.sdk.payload.create(test_fixtures.valid_payload()) 53 | if created_payload: 54 | mock_delete.return_value = Mock(status_code=200) 55 | mock_delete.return_value.json.return_value = cls.json_fixtures['payload']['cancelled'] 56 | cls.assertEqual(cls.sdk.payload.cancel(created_payload.uuid).to_dict(), cls.json_fixtures['payload']['cancelled']) 57 | 58 | @patch('xumm.client.requests.delete') 59 | @patch('xumm.client.requests.get') 60 | def test_payload_get_cancel(cls, mock_get, mock_delete): 61 | print('should cancel a payload by Fetched Payload') 62 | payloadId = '00000000-0000-4839-af2f-f794874a80b0' 63 | mock_get.return_value = Mock(status_code=200) 64 | mock_get.return_value.json.return_value = cls.json_fixtures['payload']['get'] 65 | 66 | get_payload = cls.sdk.payload.get(payloadId) 67 | if get_payload: 68 | mock_delete.return_value = Mock(status_code=200) 69 | mock_delete.return_value.json.return_value = cls.json_fixtures['payload']['cancelled'] 70 | cls.assertEqual(cls.sdk.payload.cancel(get_payload.meta.uuid).to_dict(), cls.json_fixtures['payload']['cancelled']) 71 | -------------------------------------------------------------------------------- /tests/types/meta/test_curated_asset_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import pytest 5 | from testing_config import BaseTestConfig 6 | 7 | from xumm.resource.types import ( 8 | CuratedAssetsResponse 9 | ) 10 | 11 | class TestCuratedAssetResponse(BaseTestConfig): 12 | 13 | def test_curated_asset_response(cls): 14 | print('should set curated asset response') 15 | 16 | dict = cls.json_fixtures['curatedAssets'] 17 | cls.assertEqual(CuratedAssetsResponse(**dict).to_dict(), dict) 18 | 19 | def test_curated_asset_response_fail(cls): 20 | print('should fail to set application details') 21 | 22 | dict = { 23 | "issuers": [ 24 | "Bitstamp", 25 | "Wietse" 26 | ], 27 | "currencies": [ 28 | "USD", 29 | "BTC", 30 | "ETH", 31 | "WIE" 32 | ], 33 | "details": { 34 | "Bitstamp": { 35 | "id": 185, 36 | "name": 1, # FAILS 37 | "domain": "bitstamp.net", 38 | "avatar": "https://xumm.app/assets/icons/currencies/ex-bitstamp.png", 39 | "shortlist": 1, 40 | "currencies": { 41 | "USD": { 42 | "id": 178, 43 | "issuer_id": 185, 44 | "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", 45 | "currency": "USD", 46 | "name": "US Dollar", 47 | "avatar": "https://xumm.app/assets/icons/currencies/fiat-dollar.png", 48 | "shortlist": 1 49 | }, 50 | "BTC": { 51 | "id": 492, 52 | "issuer_id": 185, 53 | "issuer": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B", 54 | "currency": "BTC", 55 | "name": "Bitcoin", 56 | "avatar": "https://xumm.app/assets/icons/currencies/crypto-btc.png", 57 | "shortlist": 1 58 | } 59 | } 60 | }, 61 | "Wietse": { 62 | "id": 17553, 63 | "name": "Wietse", 64 | "domain": "wietse.com", 65 | "avatar": "https://xumm.app/assets/icons/currencies/wietse.jpg", 66 | "shortlist": 0, 67 | "currencies": { 68 | "WIE": { 69 | "id": 17552, 70 | "issuer_id": 17553, 71 | "issuer": "rwietsevLFg8XSmG3bEZzFein1g8RBqWDZ", 72 | "currency": "WIE", 73 | "name": "Wietse", 74 | "avatar": "https://xumm.app/assets/icons/currencies/transparent.png", 75 | "shortlist": 0 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | with pytest.raises(ValueError, match=r"Invalid value: 1 for `name`, must be a `` found: "): 83 | CuratedAssetsResponse(**dict) 84 | cls.fail("CuratedAssetsResponse: raised Exception unexpectedly!") -------------------------------------------------------------------------------- /xumm/resource/types/storage/storage_set_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | from typing import Dict 6 | 7 | from .storage_response import StorageResponse 8 | 9 | 10 | class StorageSetResponse(XummResource): 11 | """ 12 | Attributes: 13 | model_types (dict): The key is attribute name 14 | and the value is attribute type. 15 | attribute_map (dict): The key is attribute name 16 | and the value is json key in definition. 17 | """ 18 | required = { 19 | 'application': True, 20 | 'stored': True, 21 | 'data': True, 22 | } 23 | 24 | model_types = { 25 | 'application': dict, 26 | 'stored': bool, 27 | 'data': dict, 28 | } 29 | 30 | attribute_map = { 31 | 'application': 'application', 32 | 'stored': 'stored', 33 | 'data': 'data', 34 | } 35 | 36 | def refresh_from(cls, **kwargs): 37 | """Returns the dict as a model 38 | 39 | :param kwargs: A dict. 40 | :type: dict 41 | :return: The StorageSetResponse of this StorageSetResponse. # noqa: E501 42 | :rtype: StorageSetResponse 43 | """ 44 | cls.sanity_check(kwargs) 45 | cls._application = None 46 | cls._stored = None 47 | cls._data = None 48 | cls.application = StorageResponse(**kwargs['application']) 49 | cls.stored = kwargs['stored'] 50 | cls.data = kwargs['data'] 51 | 52 | @property 53 | def application(self) -> StorageResponse: 54 | """Gets the application of this StorageSetResponse. 55 | 56 | 57 | :return: The application of this StorageSetResponse. 58 | :rtype: StorageResponse 59 | """ 60 | return self._application 61 | 62 | @application.setter 63 | def application(self, application: StorageResponse): 64 | """Sets the application of this StorageSetResponse. 65 | 66 | 67 | :param application: The application of this StorageSetResponse. 68 | :type application: StorageResponse 69 | """ 70 | if application is None: 71 | raise ValueError("Invalid value for `application`, must not be `None`") # noqa: E501 72 | 73 | self._application = application 74 | 75 | @property 76 | def stored(self) -> bool: 77 | """Gets the stored of this StorageSetResponse. 78 | 79 | 80 | :return: The stored of this StorageSetResponse. 81 | :rtype: bool 82 | """ 83 | return self._stored 84 | 85 | @stored.setter 86 | def stored(self, stored: bool): 87 | """Sets the stored of this StorageSetResponse. 88 | 89 | 90 | :param stored: The stored of this StorageSetResponse. 91 | :type stored: bool 92 | """ 93 | if stored is None: 94 | raise ValueError("Invalid value for `stored`, must not be `None`") # noqa: E501 95 | 96 | self._stored = stored 97 | 98 | @property 99 | def data(self) -> Dict[str, object]: 100 | """Gets the data of this StorageSetResponse. 101 | 102 | 103 | :return: The data of this StorageSetResponse. 104 | :rtype: Dict[str, object] 105 | """ 106 | return self._data 107 | 108 | @data.setter 109 | def data(self, data: Dict[str, object]): 110 | """Sets the data of this StorageSetResponse. 111 | 112 | 113 | :param data: The data of this StorageSetResponse. 114 | :type data: Dict[str, object] 115 | """ 116 | if data is None: 117 | raise ValueError("Invalid value for `data`, must not be `None`") # noqa: E501 118 | 119 | self._data = data 120 | -------------------------------------------------------------------------------- /xumm/resource/types/storage/storage_delete_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | from typing import Dict 6 | 7 | from .storage_response import StorageResponse 8 | 9 | 10 | class StorageDeleteResponse(XummResource): 11 | """ 12 | Attributes: 13 | model_types (dict): The key is attribute name 14 | and the value is attribute type. 15 | attribute_map (dict): The key is attribute name 16 | and the value is json key in definition. 17 | """ 18 | nullable = { 19 | 'data': True 20 | } 21 | 22 | required = { 23 | 'application': True, 24 | 'stored': True, 25 | 'data': True, 26 | } 27 | 28 | model_types = { 29 | 'application': dict, 30 | 'stored': bool, 31 | 'data': dict, 32 | } 33 | 34 | attribute_map = { 35 | 'application': 'application', 36 | 'stored': 'stored', 37 | 'data': 'data', 38 | } 39 | 40 | def refresh_from(cls, **kwargs): 41 | """Returns the dict as a model 42 | 43 | :param kwargs: A dict. 44 | :type: dict 45 | :return: The StorageDeleteResponse of this StorageDeleteResponse. # noqa: E501 46 | :rtype: StorageDeleteResponse 47 | """ 48 | cls.sanity_check(kwargs) 49 | cls._application = None 50 | cls._stored = None 51 | cls._data = None 52 | cls.application = StorageResponse(**kwargs['application']) 53 | cls.stored = kwargs['stored'] 54 | cls.data = kwargs['data'] 55 | 56 | @property 57 | def application(self) -> StorageResponse: 58 | """Gets the application of this StorageDeleteResponse. 59 | 60 | 61 | :return: The application of this StorageDeleteResponse. 62 | :rtype: StorageResponse 63 | """ 64 | return self._application 65 | 66 | @application.setter 67 | def application(self, application: StorageResponse): 68 | """Sets the application of this StorageDeleteResponse. 69 | 70 | 71 | :param application: The application of this StorageDeleteResponse. 72 | :type application: StorageResponse 73 | """ 74 | if application is None: 75 | raise ValueError("Invalid value for `application`, must not be `None`") # noqa: E501 76 | 77 | self._application = application 78 | 79 | @property 80 | def stored(self) -> bool: 81 | """Gets the stored of this StorageDeleteResponse. 82 | 83 | 84 | :return: The stored of this StorageDeleteResponse. 85 | :rtype: bool 86 | """ 87 | return self._stored 88 | 89 | @stored.setter 90 | def stored(self, stored: bool): 91 | """Sets the stored of this StorageDeleteResponse. 92 | 93 | 94 | :param stored: The stored of this StorageDeleteResponse. 95 | :type stored: bool 96 | """ 97 | if stored is None: 98 | raise ValueError("Invalid value for `stored`, must not be `None`") # noqa: E501 99 | 100 | self._stored = stored 101 | 102 | @property 103 | def data(self) -> Dict[str, object]: 104 | """Gets the data of this StorageDeleteResponse. 105 | 106 | 107 | :return: The data of this StorageDeleteResponse. 108 | :rtype: Dict[str, object] 109 | """ 110 | return self._data 111 | 112 | @data.setter 113 | def data(self, data: Dict[str, object]): 114 | """Sets the data of this StorageDeleteResponse. 115 | 116 | 117 | :param data: The data of this StorageDeleteResponse. 118 | :type data: Dict[str, object] 119 | """ 120 | # if data is None: 121 | # raise ValueError("Invalid value for `data`, must not be `None`") # noqa: E501 122 | 123 | self._data = data 124 | -------------------------------------------------------------------------------- /tests/fixtures/xumm_api.py: -------------------------------------------------------------------------------- 1 | 2 | #!/usr/bin/env python 3 | # coding: utf-8 4 | 5 | from typing import List, Dict, Any # noqa: F401 6 | 7 | def to_match_object(model_object: Any) -> bool: 8 | try: 9 | return model_object.model_check() 10 | except Exception as e: 11 | return False 12 | 13 | 14 | def pong_object(object: Dict[str, object]) -> Dict[str, object]: 15 | return { 16 | 'application': { 17 | 'disabled': isinstance(object['application']['disabled'], int), 18 | 'name': isinstance(object['application']['name'], str), 19 | 'uuidv4': isinstance(object['application']['uuidv4'], str), 20 | 'webhookurl': isinstance(object['application']['webhookurl'], str) 21 | }, 22 | 'call': { 23 | 'uuidv4': isinstance(object['call']['uuidv4'], str) 24 | } 25 | } 26 | 27 | def subscription_updates() -> Dict[str, object]: 28 | return { 29 | 'welcome': { 30 | 'message': 'Welcome aaaaaaaa-dddd-ffff-cccc-8207bd724e45' 31 | }, 32 | 'expire': { 33 | 'expires_in_seconds': 30000 34 | }, 35 | 'opened': { 36 | 'opened': True 37 | }, 38 | 'rejected': { 39 | 'payload_uuidv4': 'aaaaaaaa-dddd-ffff-cccc-8207bd724e45', 40 | 'reference_call_uuidv4': 'bbbbbbbb-eeee-aaaa-1111-8d192bd91f07', 41 | 'signed': False, 42 | 'user_token': True, 43 | 'return_url': { 44 | 'app': None, 45 | 'web': None 46 | }, 47 | 'custom_meta': {} 48 | } 49 | } 50 | 51 | # export const createPayloadResponseObject = { 52 | # uuid: expect.any(String), 53 | # next: { 54 | # always: expect.any(String) 55 | # }, 56 | # refs: { 57 | # qr_png: expect.any(String), 58 | # qr_matrix: expect.any(String), 59 | # qr_uri_quality_opts: expect.any(Array), 60 | # websocket_status: expect.any(String) 61 | # }, 62 | # pushed: expect.any(Boolean) 63 | # } 64 | 65 | # export const cancelPayloadResponseObject = { 66 | # result: { 67 | # cancelled: true, 68 | # reason: 'OK' 69 | # }, 70 | # meta: { 71 | # exists: true, 72 | # uuid: expect.any(String), 73 | # multisign: expect.any(Boolean), 74 | # submit: expect.any(Boolean), 75 | # destination: expect.any(String), 76 | # resolved_destination: expect.any(String), 77 | # resolved: expect.any(Boolean), 78 | # signed: expect.any(Boolean), 79 | # cancelled: expect.any(Boolean), 80 | # expired: true, 81 | # pushed: expect.any(Boolean), 82 | # app_opened: expect.any(Boolean), 83 | # opened_by_deeplink: expect.any(Boolean) || null, 84 | # return_url_app: expect.any(String), 85 | # return_url_web: expect.any(String), 86 | # is_xapp: expect.any(Boolean) 87 | # } 88 | # } 89 | 90 | def valid_payload() -> Dict[str, object]: 91 | return { 92 | 'txjson': { 93 | 'TransactionType': 'Payment', 94 | 'Destination': 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY', 95 | 'DestinationTag': 495 96 | }, 97 | 'options': { 98 | 'submit': True, 99 | 'expire': 3600, 100 | 'return_url': { 101 | 'app': 'https://xumm.dev/beta/test?payloadId={payloadId}' 102 | } 103 | } 104 | } 105 | 106 | def invalid_payload() -> Dict[str, object]: 107 | return { 108 | 'user_token': 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee', 109 | 'txblob': '1200002400000003614000000002FAF0806840000000000000C8732' + 110 | '08536F6D65547970657D08536F6D6544617461E1EA7C09446576656C6F706' + 111 | '5727D0B4057696574736557696E64E1F1', 112 | 'txjson': { 113 | 'TransactionType' : 'Payment', 114 | 'Destination' : 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY', 115 | 'DestinationTag': 495, 116 | 'Amount': '65000' 117 | } 118 | } -------------------------------------------------------------------------------- /xumm/resource/types/meta/kyc_status_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | from typing import Dict 6 | 7 | 8 | class KycStatusResponse(XummResource): 9 | """ 10 | Attributes: 11 | model_types (dict): The key is attribute name 12 | and the value is attribute type. 13 | attribute_map (dict): The key is attribute name 14 | and the value is json key in definition. 15 | """ 16 | required = { 17 | 'kyc_status': True, 18 | 'possible_statuses': True, 19 | } 20 | 21 | model_types = { 22 | 'kyc_status': str, 23 | 'possible_statuses': dict 24 | } 25 | 26 | attribute_map = { 27 | 'kyc_status': 'kycStatus', 28 | 'possible_statuses': 'possibleStatuses' 29 | } 30 | 31 | def refresh_from(cls, **kwargs): 32 | """Returns the dict as a model 33 | 34 | :param kwargs: A dict. 35 | :type: dict 36 | :return: The KycStatusResponse of this KycStatusResponse. # noqa: E501 37 | :rtype: KycStatusResponse 38 | """ 39 | cls.sanity_check(kwargs) 40 | cls._kyc_status = None 41 | cls._possible_statuses = None 42 | cls.kyc_status = kwargs['kycStatus'] 43 | cls.possible_statuses = kwargs['possibleStatuses'] 44 | 45 | @property 46 | def kyc_status(cls) -> str: 47 | """Gets the kyc_status of this KycStatusResponse. 48 | 49 | 50 | :return: The kyc_status of this KycStatusResponse. 51 | :rtype: str 52 | """ 53 | return cls._kyc_status 54 | 55 | @kyc_status.setter 56 | def kyc_status(cls, kyc_status: str): 57 | """Sets the kyc_status of this KycStatusResponse. 58 | 59 | 60 | :param kyc_status: The kyc_status of this KycStatusResponse. 61 | :type kyc_status: str 62 | """ 63 | if kyc_status is None: 64 | raise ValueError("Invalid value for `kyc_status`, must not be `None`") # noqa: E501 65 | 66 | allowed_values = [ 67 | "NONE", 68 | "IN_PROGRESS", 69 | "REJECTED", 70 | "SUCCESSFUL", 71 | ] # noqa: E501 72 | if kyc_status not in allowed_values: 73 | raise ValueError( 74 | "Invalid value for `kyc_status` ({0}), must be one of {1}" # noqa: E501 75 | .format(kyc_status, allowed_values) 76 | ) 77 | 78 | cls._kyc_status = kyc_status 79 | 80 | @property 81 | def possible_statuses(cls) -> Dict[str, object]: 82 | """Gets the possible_statuses of this KycStatusResponse. 83 | 84 | 85 | :return: The possible_statuses of this KycStatusResponse. 86 | :rtype: bool 87 | """ 88 | return cls._possible_statuses 89 | 90 | @possible_statuses.setter 91 | def possible_statuses(cls, possible_statuses: Dict[str, object]): 92 | """Sets the possible_statuses of this KycStatusResponse. 93 | 94 | 95 | :param possible_statuses: The possible_statuses of this KycStatusResponse. # noqa: E501 96 | :type possible_statuses: bool 97 | """ 98 | if possible_statuses is None: 99 | raise ValueError("Invalid value for `possible_statuses`, must not be `None`") # noqa: E501 100 | 101 | allowed_values = { 102 | "NONE": "No KYC attempt has been made", 103 | "IN_PROGRESS": "KYC flow has been started, but did not finish (yet)", # noqa: E501 104 | "REJECTED": "KYC flow has been started and rejected (NO SUCCESSFUL KYC)", # noqa: E501 105 | "SUCCESSFUL": "KYC flow has been started and was SUCCESSFUL :)" 106 | } 107 | if possible_statuses != allowed_values: 108 | raise ValueError( 109 | "Invalid value for `possible_statuses` ({0}), must be exactly {1}" # noqa: E501 110 | .format(possible_statuses, allowed_values) 111 | ) 112 | 113 | cls._possible_statuses = possible_statuses 114 | -------------------------------------------------------------------------------- /xumm/resource/types/payload/subscription_callback_params.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | from typing import Dict, Callable, Any 6 | 7 | from ..xumm_api import XummGetPayloadResponse as XummPayload 8 | 9 | 10 | class SubscriptionCallbackParams(XummResource): 11 | """ 12 | Attributes: 13 | model_types (dict): The key is attribute name 14 | and the value is attribute type. 15 | attribute_map (dict): The key is attribute name 16 | and the value is json key in definition. 17 | """ 18 | required = { 19 | 'uuid': True, 20 | 'data': True, 21 | 'payload': True, 22 | 'resolve': True, 23 | } 24 | 25 | model_types = { 26 | 'uuid': str, 27 | 'data': dict, 28 | 'payload': dict, 29 | 'resolve': Callable, 30 | } 31 | 32 | attribute_map = { 33 | 'uuid': 'uuid', 34 | 'data': 'data', 35 | 'payload': 'payload', 36 | 'resolve': 'resolve', 37 | } 38 | 39 | def refresh_from(cls, **kwargs): 40 | """Returns the dict as a model 41 | 42 | :param kwargs: A dict. 43 | :type: dict 44 | :return: The PayloadSubscription of this PayloadSubscription. # noqa: E501 45 | :rtype: PayloadSubscription 46 | """ 47 | cls.sanity_check(kwargs) 48 | cls._uuid = None 49 | cls._data = None 50 | cls._payload = None 51 | cls._resolve = None 52 | cls.uuid = kwargs['uuid'] 53 | cls.data = kwargs['data'] 54 | cls.payload = XummPayload(**kwargs['payload']) 55 | cls.resolve = kwargs['resolve'] 56 | 57 | @property 58 | def uuid(cls) -> str: 59 | """Gets the uuid of this XummPostPayloadResponse. 60 | 61 | 62 | :return: The uuid of this XummPostPayloadResponse. 63 | :rtype: str 64 | """ 65 | return cls._uuid 66 | 67 | @uuid.setter 68 | def uuid(cls, uuid: str): 69 | """Sets the uuid of this XummPostPayloadResponse. 70 | 71 | 72 | :param uuid: The uuid of this XummPostPayloadResponse. 73 | :type uuid: str 74 | """ 75 | if uuid is None: 76 | raise ValueError("Invalid value for `uuid`, must not be `None`") # noqa: E501 77 | 78 | cls._uuid = uuid 79 | 80 | @property 81 | def data(cls) -> Dict[str, object]: 82 | """Gets the data of this XummCustomMeta. 83 | 84 | 85 | :return: The data of this XummCustomMeta. 86 | :rtype: Dict[str, object] 87 | """ 88 | return cls._blob 89 | 90 | @data.setter 91 | def data(cls, data: Dict[str, object]): 92 | """Sets the data of this XummCustomMeta. 93 | 94 | 95 | :param data: The data of this XummCustomMeta. 96 | :type data: Dict[str, object] 97 | """ 98 | if data is None: 99 | raise ValueError("Invalid value for `data`, must not be `None`") # noqa: E501 100 | 101 | cls._blob = data 102 | 103 | @property 104 | def payload(cls) -> XummPayload: 105 | """Gets the payload of this PayloadSubscription. 106 | 107 | 108 | :return: The payload of this PayloadSubscription. 109 | :rtype: XummPayload 110 | """ 111 | return cls._payload 112 | 113 | @payload.setter 114 | def payload(cls, payload: XummPayload): 115 | """Sets the payload of this PayloadSubscription. 116 | 117 | 118 | :param payload: The payload of this PayloadSubscription. 119 | :type meta: CreatedPayload 120 | """ 121 | if payload is None: 122 | raise ValueError("Invalid value for `payload`, must not be `None`") # noqa: E501 123 | 124 | cls._payload = payload 125 | 126 | @property 127 | def resolve(cls) -> Callable[[Any], Any]: 128 | """Gets the resolve of this PayloadSubscription. 129 | 130 | 131 | :return: The resolve of this PayloadSubscription. 132 | :rtype: Callable 133 | """ 134 | return cls._resolve 135 | 136 | @resolve.setter 137 | def resolve(cls, resolve: Callable[[Any], Any]): 138 | """Sets the resolve of this PayloadSubscription. 139 | 140 | 141 | :param resolve: The resolve of this PayloadSubscription. 142 | :type meta: Callable 143 | """ 144 | if resolve is None: 145 | raise ValueError("Invalid value for `resolve`, must not be `None`") # noqa: E501 146 | 147 | cls._resolve = resolve 148 | -------------------------------------------------------------------------------- /xumm/resource/types/payload/payload_subscription.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | from typing import Callable, Any 6 | 7 | from xumm.ws_client import WSClient 8 | from ..xumm_api import XummGetPayloadResponse as XummPayload 9 | 10 | 11 | class PayloadSubscription(XummResource): 12 | """ 13 | Attributes: 14 | model_types (dict): The key is attribute name 15 | and the value is attribute type. 16 | attribute_map (dict): The key is attribute name 17 | and the value is json key in definition. 18 | """ 19 | required = { 20 | 'payload': True, 21 | 'resolved': True, 22 | 'resolve': True, 23 | 'websocket': True, 24 | } 25 | 26 | model_types = { 27 | 'payload': dict, 28 | 'resolved': Callable, 29 | 'resolve': Callable, 30 | 'websocket': WSClient, 31 | } 32 | 33 | attribute_map = { 34 | 'payload': 'payload', 35 | 'resolved': 'resolved', 36 | 'resolve': 'resolve', 37 | 'websocket': 'websocket', 38 | } 39 | 40 | def refresh_from(cls, **kwargs): 41 | """Returns the dict as a model 42 | 43 | :param kwargs: A dict. 44 | :type: dict 45 | :return: The PayloadSubscription of this PayloadSubscription. # noqa: E501 46 | :rtype: PayloadSubscription 47 | """ 48 | # cls.sanity_check(kwargs) 49 | cls._payload = None 50 | cls._resolved = None 51 | cls._resolve = None 52 | cls._websocket = None 53 | cls.payload = XummPayload(**kwargs['payload']) 54 | cls.resolved = kwargs['resolved'] 55 | cls.resolve = kwargs['resolve'] 56 | cls.websocket = kwargs['websocket'] 57 | 58 | @property 59 | def payload(cls) -> XummPayload: 60 | """Gets the payload of this PayloadSubscription. 61 | 62 | 63 | :return: The payload of this PayloadSubscription. 64 | :rtype: XummPayload 65 | """ 66 | return cls._payload 67 | 68 | @payload.setter 69 | def payload(cls, payload: XummPayload): 70 | """Sets the payload of this PayloadSubscription. 71 | 72 | 73 | :param payload: The payload of this PayloadSubscription. 74 | :type meta: CreatedPayload 75 | """ 76 | if payload is None: 77 | raise ValueError("Invalid value for `payload`, must not be `None`") # noqa: E501 78 | 79 | cls._payload = payload 80 | 81 | @property 82 | def resolved(cls) -> Callable[[Any], Any]: 83 | """Gets the resolved of this PayloadSubscription. 84 | 85 | 86 | :return: The resolved of this PayloadSubscription. 87 | :rtype: Callable 88 | """ 89 | return cls._resolved 90 | 91 | @resolved.setter 92 | def resolved(cls, resolved: Callable[[Any], Any]): 93 | """Sets the resolved of this PayloadSubscription. 94 | 95 | 96 | :param resolved: The resolved of this PayloadSubscription. 97 | :type meta: Payload 98 | """ 99 | if resolved is None: 100 | raise ValueError("Invalid value for `resolve`, must not be `None`") # noqa: E501 101 | 102 | cls._resolved = resolved 103 | 104 | @property 105 | def resolve(cls) -> Callable[[Any], Any]: 106 | """Gets the resolve of this PayloadSubscription. 107 | 108 | 109 | :return: The resolve of this PayloadSubscription. 110 | :rtype: Callable 111 | """ 112 | return cls._resolve 113 | 114 | @resolve.setter 115 | def resolve(cls, resolve: Callable[[Any], Any]): 116 | """Sets the resolve of this PayloadSubscription. 117 | 118 | 119 | :param resolve: The resolve of this PayloadSubscription. 120 | :type meta: Callable 121 | """ 122 | if resolve is None: 123 | raise ValueError("Invalid value for `resolve`, must not be `None`") # noqa: E501 124 | 125 | cls._resolve = resolve 126 | 127 | @property 128 | def websocket(cls) -> WSClient: 129 | """Gets the websocket of this PayloadSubscription. 130 | 131 | 132 | :return: The websocket of this PayloadSubscription. 133 | :rtype: WSClient 134 | """ 135 | return cls._websocket 136 | 137 | @websocket.setter 138 | def websocket(cls, websocket: WSClient): 139 | """Sets the websocket of this PayloadSubscription. 140 | 141 | 142 | :param websocket: The websocket of this PayloadSubscription. 143 | :type meta: WSClient 144 | """ 145 | if websocket is None: 146 | raise ValueError("Invalid value for `websocket`, must not be `None`") # noqa: E501 147 | 148 | cls._websocket = websocket 149 | -------------------------------------------------------------------------------- /xumm/resource/types/oauth2/oauth2_token_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class OAuth2TokenResponse(XummResource): 8 | """ 9 | Attributes: 10 | model_types (dict): The key is attribute name 11 | and the value is attribute type. 12 | attribute_map (dict): The key is attribute name 13 | and the value is json key in definition. 14 | """ 15 | required = { 16 | 'access_token': True, 17 | 'token_type': True, 18 | 'expires_in': True, 19 | 'refresh_token': False, 20 | 'expires_at': True, 21 | } 22 | 23 | model_types = { 24 | 'access_token': str, 25 | 'token_type': str, 26 | 'expires_in': int, 27 | 'refresh_token': str, 28 | 'expires_at': float, 29 | } 30 | 31 | attribute_map = { 32 | 'access_token': 'access_token', 33 | 'token_type': 'token_type', 34 | 'expires_in': 'expires_in', 35 | 'refresh_token': 'refresh_token', 36 | 'expires_at': 'expires_at', 37 | } 38 | 39 | def refresh_from(cls, **kwargs): 40 | """Returns the dict as a model 41 | 42 | :param kwargs: A dict. 43 | :type: dict 44 | :return: The OAuth2Response of this OAuth2Response. # noqa: E501 45 | :rtype: OAuth2Response 46 | """ 47 | cls.sanity_check(kwargs) 48 | cls._access_token = None 49 | cls._token_type = None 50 | cls._expires_in = None 51 | cls._refresh_token = None 52 | cls._expires_at = None 53 | cls.access_token = kwargs['access_token'] 54 | cls.token_type = kwargs['token_type'] 55 | cls.expires_in = kwargs['expires_in'] 56 | cls.refresh_token = kwargs['refresh_token'] 57 | cls.expires_at = kwargs['expires_at'] 58 | 59 | @property 60 | def access_token(cls) -> str: 61 | """Gets the access_token of this OAuth2Response. 62 | 63 | 64 | :return: The access_token of this OAuth2Response. 65 | :rtype: str 66 | """ 67 | return cls._access_token 68 | 69 | @access_token.setter 70 | def access_token(cls, access_token: str): 71 | """Sets the access_token of this OAuth2Response. 72 | 73 | 74 | :param access_token: The access_token of this OAuth2Response. 75 | :type access_token: str 76 | """ 77 | if access_token is None: 78 | raise ValueError("Invalid value for `access_token`, must not be `None`") # noqa: E501 79 | 80 | cls._access_token = access_token 81 | 82 | @property 83 | def token_type(cls) -> str: 84 | """Gets the token_type of this OAuth2Response. 85 | 86 | 87 | :return: The token_type of this OAuth2Response. 88 | :rtype: str 89 | """ 90 | return cls._token_type 91 | 92 | @token_type.setter 93 | def token_type(cls, token_type: str): 94 | """Sets the token_type of this OAuth2Response. 95 | 96 | 97 | :param token_type: The token_type of this OAuth2Response. 98 | :type token_type: str 99 | """ 100 | if token_type is None: 101 | raise ValueError("Invalid value for `token_type`, must not be `None`") # noqa: E501 102 | 103 | cls._token_type = token_type 104 | 105 | @property 106 | def expires_in(cls) -> int: 107 | """Gets the expires_in of this OAuth2Response. 108 | 109 | 110 | :return: The expires_in of this OAuth2Response. 111 | :rtype: int 112 | """ 113 | return cls._expires_in 114 | 115 | @expires_in.setter 116 | def expires_in(cls, expires_in: int): 117 | """Sets the expires_in of this OAuth2Response. 118 | 119 | 120 | :param expires_in: The expires_in of this OAuth2Response. 121 | :type expires_in: int 122 | """ 123 | if expires_in is None: 124 | raise ValueError("Invalid value for `expires_in`, must not be `None`") # noqa: E501 125 | 126 | cls._expires_in = expires_in 127 | 128 | @property 129 | def refresh_token(cls) -> int: 130 | """Gets the refresh_token of this OAuth2Response. 131 | 132 | 133 | :return: The refresh_token of this OAuth2Response. 134 | :rtype: int 135 | """ 136 | return cls._refresh_token 137 | 138 | @refresh_token.setter 139 | def refresh_token(cls, refresh_token: int): 140 | """Sets the refresh_token of this OAuth2Response. 141 | 142 | 143 | :param refresh_token: The refresh_token of this OAuth2Response. 144 | :type refresh_token: int 145 | """ 146 | if refresh_token is None: 147 | raise ValueError("Invalid value for `refresh_token`, must not be `None`") # noqa: E501 148 | 149 | cls._refresh_token = refresh_token 150 | 151 | @property 152 | def expires_at(cls) -> float: 153 | """Gets the expires_at of this OAuth2Response. 154 | 155 | 156 | :return: The expires_at of this OAuth2Response. 157 | :rtype: float 158 | """ 159 | return cls._expires_at 160 | 161 | @expires_at.setter 162 | def expires_at(cls, expires_at: float): 163 | """Sets the expires_at of this OAuth2Response. 164 | 165 | 166 | :param expires_at: The expires_at of this OAuth2Response. 167 | :type expires_at: float 168 | """ 169 | if expires_at is None: 170 | raise ValueError("Invalid value for `expires_at`, must not be `None`") # noqa: E501 171 | 172 | cls._expires_at = expires_at 173 | -------------------------------------------------------------------------------- /xumm/resource/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import re 5 | from typing import List, Union 6 | import xumm 7 | from xumm import ( 8 | client, 9 | error 10 | ) 11 | from xumm.resource import XummResource 12 | 13 | from .types import ( 14 | KycInfoResponse, 15 | KycStatusResponse, 16 | PongResponse, 17 | CuratedAssetsResponse, 18 | XrplTransaction, 19 | RatesResponse, 20 | UserTokenResponse 21 | ) 22 | 23 | from xumm.resource.ping import PingResource 24 | from xumm.resource.kyc_status import KycStatusResource 25 | from xumm.resource.curated_assets import CuratedAssetsResource 26 | from xumm.resource.xrpl_tx import XrplTxResource 27 | from xumm.resource.rates import RatesResource 28 | from xumm.resource.payload import PayloadResource 29 | from xumm.resource.storage import StorageResource 30 | from xumm.resource.user_tokens import UserTokensResource 31 | from xumm.resource.push import PushResource 32 | from xumm.resource.oauth2 import OAuth2Resource 33 | 34 | 35 | class XummSdk(XummResource): 36 | 37 | def __init__(cls, *args) -> 'XummSdk': 38 | 39 | if len(args) == 2 and isinstance(args[0], str): 40 | xumm.api_key = args[0] 41 | 42 | if len(args) == 2 and isinstance(args[1], str): 43 | xumm.api_secret = args[1] 44 | 45 | if xumm.api_key is None: 46 | raise error.AuthenticationError( 47 | 'No API key provided. (HINT: set your API key using ' 48 | '"xumm.api_key = "). You can generate API keys ' 49 | 'from the Xumm web interface.' 50 | ) 51 | if xumm.api_secret is None: 52 | raise error.AuthenticationError( 53 | 'No API secret provided. (HINT: set your API key using ' 54 | '"xumm.api_secret = "). You can generate API keys ' 55 | 'from the Xumm web interface.' 56 | ) 57 | 58 | pattern = r'[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}' # noqa: E501 59 | if not re.match(pattern, xumm.api_key) or not re.match(pattern, xumm.api_secret): # noqa: E501 60 | raise error.AuthenticationError( 61 | 'Invalid API secret provided. (HINT: XXXXXXXX-XXXX-' 62 | 'XXXX-XXXX-XXXXXXXXXXXX).' 63 | ) 64 | 65 | cls.payload = PayloadResource() 66 | cls.storage = StorageResource() 67 | cls.push = PushResource() 68 | cls.oauth2 = OAuth2Resource() 69 | 70 | def refresh_from(cls, **kwargs): 71 | return super().refresh_from(**kwargs) 72 | 73 | def ping(cls) -> PongResponse: 74 | """Returns the dict as a model 75 | 76 | :return: The PongResponse of this PongResponse. # noqa: E501 77 | :rtype: PongResponse 78 | """ 79 | 80 | res = client.get(PingResource.get_url()) 81 | return PongResponse(**res).auth 82 | 83 | def get_kyc_status( 84 | cls, 85 | id: str = None 86 | ) -> Union[KycInfoResponse, KycStatusResponse]: # noqa: E501 87 | """Returns the dict as a model 88 | 89 | :return: The kyc_status of this kyc_status. # noqa: E501 90 | :rtype: kyc_status 91 | """ 92 | if re.match('^r', id.strip()): 93 | res = client.get(KycStatusResource.get_url(id)) 94 | return 'SUCCESSFUL' if KycInfoResponse(**res).kyc_approved else 'NONE' # noqa: E501 95 | else: 96 | res = client.post( 97 | KycStatusResource.post_url(), 98 | {'user_token': id} 99 | ) 100 | return KycStatusResponse(**res).kyc_status or 'NONE' 101 | 102 | def get_curated_assets(cls) -> CuratedAssetsResponse: 103 | """Returns the dict as a model 104 | 105 | :return: The CuratedAssetsResponse of this CuratedAssetsResponse. # noqa: E501 106 | :rtype: CuratedAssetsResponse 107 | """ 108 | 109 | res = client.get(CuratedAssetsResource.get_url()) 110 | return CuratedAssetsResponse(**res) 111 | 112 | def get_transaction(cls, id: str = None) -> XrplTransaction: 113 | """Returns the dict as a model 114 | 115 | :return: The XrplTransaction of this XrplTransaction. # noqa: E501 116 | :rtype: XrplTransaction 117 | """ 118 | 119 | res = client.get(XrplTxResource.get_url(id)) 120 | return XrplTransaction(**res) 121 | 122 | def get_rates(cls, id: str = None) -> RatesResponse: 123 | """Returns the dict as a model 124 | 125 | :return: The RatesResponse of this RatesResponse. # noqa: E501 126 | :rtype: RatesResponse 127 | """ 128 | 129 | res = client.get(RatesResource.get_url(id)) 130 | return RatesResponse(**res) 131 | 132 | def verify_user_tokens(cls, tokens: List[str] = None) -> UserTokenResponse: 133 | """Returns the dict as a model 134 | 135 | :return: The UserTokenResponse of this UserTokenResponse. # noqa: E501 136 | :rtype: UserTokenResponse 137 | """ 138 | 139 | res = client.post(UserTokensResource.post_url(), tokens) 140 | return UserTokenResponse(**res).tokens 141 | 142 | def verify_user_token(cls, token: str = None) -> UserTokenResponse: 143 | """Returns the dict as a model 144 | 145 | :return: The UserTokenResponse of this UserTokenResponse. # noqa: E501 146 | :rtype: UserTokenResponse 147 | """ 148 | 149 | token_results = cls.verify_user_tokens([token]) 150 | return token_results[0] if isinstance(token_results, list) and len(token_results) == 1 else None # noqa: E501 151 | -------------------------------------------------------------------------------- /xumm/resource/types/payload/payload_and_subscription.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | from typing import Callable, Any 6 | 7 | from xumm.ws_client import WSClient 8 | from ..xumm_api import ( 9 | XummGetPayloadResponse as XummPayload, 10 | XummPostPayloadResponse as CreatedPayload, 11 | ) 12 | 13 | 14 | class PayloadAndSubscription(XummResource): 15 | """ 16 | Attributes: 17 | model_types (dict): The key is attribute name 18 | and the value is attribute type. 19 | attribute_map (dict): The key is attribute name 20 | and the value is json key in definition. 21 | """ 22 | required = { 23 | 'created': True, 24 | 'payload': True, 25 | 'resolve': True, 26 | 'resolved': True, 27 | 'websocket': True, 28 | } 29 | 30 | model_types = { 31 | 'created': dict, 32 | 'payload': dict, 33 | 'resolve': Callable, 34 | 'resolved': Callable, 35 | 'websocket': WSClient, 36 | } 37 | 38 | attribute_map = { 39 | 'created': 'created', 40 | 'payload': 'payload', 41 | 'resolve': 'resolve', 42 | 'resolved': 'resolved', 43 | 'websocket': 'websocket', 44 | } 45 | 46 | def refresh_from(cls, **kwargs): 47 | """Returns the dict as a model 48 | 49 | :param kwargs: A dict. 50 | :type: dict 51 | :return: The PayloadAndSubscription of this PayloadAndSubscription. # noqa: E501 52 | :rtype: PayloadAndSubscription 53 | """ 54 | cls.sanity_check(kwargs) 55 | cls._created = None 56 | cls._payload = None 57 | cls._resolve = None 58 | cls._resolved = None 59 | cls._websocket = None 60 | cls.created = CreatedPayload(**kwargs['created']) 61 | cls.payload = XummPayload(**kwargs['payload']) 62 | cls.resolve = kwargs['resolve'] 63 | cls.resolved = kwargs['resolved'] 64 | cls.websocket = kwargs['websocket'] 65 | 66 | @property 67 | def created(cls) -> CreatedPayload: 68 | """Gets the created of this PayloadAndSubscription. 69 | 70 | 71 | :return: The created of this PayloadAndSubscription. 72 | :rtype: PostPayloadResponse 73 | """ 74 | return cls._created 75 | 76 | @created.setter 77 | def created(cls, created: CreatedPayload): 78 | """Sets the created of this PayloadAndSubscription. 79 | 80 | 81 | :param created: The created of this PayloadAndSubscription. 82 | :type meta: PostPayloadResponse 83 | """ 84 | if created is None: 85 | raise ValueError("Invalid value for `created`, must not be `None`") # noqa: E501 86 | 87 | cls._created = created 88 | 89 | @property 90 | def payload(cls) -> XummPayload: 91 | """Gets the payload of this PayloadAndSubscription. 92 | 93 | 94 | :return: The payload of this PayloadAndSubscription. 95 | :rtype: GetPayloadResponse 96 | """ 97 | return cls._payload 98 | 99 | @payload.setter 100 | def payload(cls, payload: XummPayload): 101 | """Sets the payload of this PayloadAndSubscription. 102 | 103 | 104 | :param payload: The payload of this PayloadAndSubscription. 105 | :type payload: XummPayload 106 | """ 107 | if payload is None: 108 | raise ValueError("Invalid value for `payload`, must not be `None`") # noqa: E501 109 | 110 | cls._payload = payload 111 | 112 | @property 113 | def resolve(cls) -> Callable[[Any], Any]: 114 | """Gets the resolve of this PayloadAndSubscription. 115 | 116 | 117 | :return: The resolve of this PayloadAndSubscription. 118 | :rtype: Callable 119 | """ 120 | return cls._resolve 121 | 122 | @resolve.setter 123 | def resolve(cls, resolve: Callable[[Any], Any]): 124 | """Sets the resolve of this PayloadAndSubscription. 125 | 126 | 127 | :param resolve: The resolve of this PayloadAndSubscription. 128 | :type meta: Callable 129 | """ 130 | if resolve is None: 131 | raise ValueError("Invalid value for `resolve`, must not be `None`") # noqa: E501 132 | 133 | cls._resolve = resolve 134 | 135 | @property 136 | def resolved(cls) -> Callable[[Any], Any]: 137 | """Gets the resolved of this PayloadAndSubscription. 138 | 139 | 140 | :return: The resolved of this PayloadAndSubscription. 141 | :rtype: Callable 142 | """ 143 | return cls._resolved 144 | 145 | @resolved.setter 146 | def resolved(cls, resolved: Callable[[Any], Any]): 147 | """Sets the resolved of this PayloadAndSubscription. 148 | 149 | 150 | :param resolved: The resolved of this PayloadAndSubscription. 151 | :type meta: Payload 152 | """ 153 | if resolved is None: 154 | raise ValueError("Invalid value for `resolve`, must not be `None`") # noqa: E501 155 | 156 | cls._resolved = resolved 157 | 158 | @property 159 | def websocket(cls) -> WSClient: 160 | """Gets the websocket of this PayloadAndSubscription. 161 | 162 | 163 | :return: The websocket of this PayloadAndSubscription. 164 | :rtype: WSClient 165 | """ 166 | return cls._websocket 167 | 168 | @websocket.setter 169 | def websocket(cls, websocket: WSClient): 170 | """Sets the websocket of this PayloadAndSubscription. 171 | 172 | 173 | :param websocket: The websocket of this PayloadAndSubscription. 174 | :type meta: WSClient 175 | """ 176 | if websocket is None: 177 | raise ValueError("Invalid value for `websocket`, must not be `None`") # noqa: E501 178 | 179 | cls._websocket = websocket 180 | -------------------------------------------------------------------------------- /xumm/resource/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from __future__ import unicode_literals 5 | 6 | from typing import Dict 7 | import pprint 8 | import six 9 | 10 | from xumm import client 11 | from xumm import oauth2_base 12 | 13 | # import logging 14 | # logging.basicConfig(level=logging.ERROR) 15 | 16 | 17 | class BaseResource(object): 18 | 19 | # nullable: The key is attribute name and the 20 | # value is a bool. 21 | nullable = {} 22 | 23 | # required: The key is attribute name and the 24 | # value is a bool. 25 | required = {} 26 | 27 | # modelTypes: The key is attribute name and the 28 | # value is attribute type. 29 | model_types = {} 30 | 31 | # attributeMap: The key is attribute name and the 32 | # value is json key in definition. 33 | attribute_map = {} 34 | 35 | def sanity_check(cls, kwargs) -> None: 36 | """Runs a sanity check on the model""" 37 | 38 | for _attr, is_type in six.iteritems(cls.model_types): 39 | 40 | # Use the attribute map for RECEIVING json data (Camelcase) 41 | attr = cls.attribute_map[_attr] 42 | 43 | # Error if attribute not in json 44 | # and attribute in required 45 | # * 'not in' isnt correct as False may get tripped here... 46 | if attr not in kwargs and attr in cls.required: 47 | raise ValueError( 48 | "Invalid value for `{}`, " 49 | "must not be `None`".format(attr) 50 | ) 51 | 52 | # Skip option attributes if non exists in json 53 | if attr not in kwargs and attr not in cls.required: 54 | continue 55 | 56 | # set value for attribute 57 | value = kwargs[attr] 58 | 59 | # Skip nullable attributes 60 | # if empty json, list or None 61 | if attr in cls.nullable and value == {} or value == [] or value is None or value == '': # noqa: E501 62 | continue 63 | 64 | # Error if value is not instance of attribute type 65 | if not isinstance(value, is_type): 66 | raise ValueError( 67 | "Invalid value: {} for `{}`, " 68 | "must be a `{}` found: {}".format( 69 | value, 70 | attr, 71 | is_type, 72 | type(value) 73 | ) 74 | ) 75 | 76 | # Error if attribute is required and value is 77 | # None: 2x of ^^^ Delete in final 78 | if attr in cls.nullable and value is None and cls.nullable[attr] is True: # noqa: E501 79 | continue 80 | 81 | if attr in cls.required and value is None: 82 | raise ValueError( 83 | "Invalid value for `{}`, " 84 | "must not be `None`".format(attr) 85 | ) 86 | 87 | # def from_dict(self): 88 | # """Returns the model properties as a dict 89 | 90 | # :rtype: dict 91 | # """ 92 | # return self.from_dict() 93 | 94 | def to_dict(cls) -> Dict[str, object]: 95 | """Returns the model properties as a dict""" 96 | result = {} 97 | 98 | for attr, _ in six.iteritems(cls.attribute_map): 99 | value = getattr(cls, attr) 100 | attr = cls.attribute_map[attr] 101 | if isinstance(value, list): 102 | result[attr] = list(map( 103 | lambda x: x.to_dict() if hasattr(x, "to_dict") else x, 104 | value 105 | )) 106 | elif hasattr(value, "to_dict"): 107 | result[attr] = value.to_dict() 108 | elif isinstance(value, dict): 109 | result[attr] = dict(map( 110 | lambda item: (item[0], item[1].to_dict()) 111 | if hasattr(item[1], "to_dict") else item, 112 | value.items() 113 | )) 114 | else: 115 | result[attr] = value 116 | 117 | if issubclass(cls.__class__, dict): 118 | for key, value in cls.items(): 119 | result[key] = value 120 | 121 | return { 122 | k: v for k, v in result.items() 123 | if v is not None or k in 124 | cls.required and k in cls.nullable 125 | } 126 | 127 | # def to_dict(self) -> Dict[str, object]: 128 | # """Returns the model properties as a dict 129 | 130 | # :rtype: dict 131 | # """ 132 | # return self.to_dict() 133 | 134 | def to_str(self) -> str: 135 | """Returns the string representation of the model 136 | 137 | :rtype: str 138 | """ 139 | return pprint.pformat(self.__class__) 140 | 141 | def __repr__(self) -> str: 142 | """For `print` and `pprint`""" 143 | return self.to_str() 144 | 145 | def __eq__(self, other) -> bool: 146 | """Returns true if both objects are equal""" 147 | 148 | if type(other) != type(self): 149 | return False 150 | 151 | if isinstance(self, list): 152 | return self == other 153 | 154 | return self.__dict__ == other.__dict__ 155 | 156 | def __ne__(self, other) -> bool: 157 | """Returns true if both objects are not equal""" 158 | return not self == other 159 | 160 | 161 | class XummResource(BaseResource): 162 | 163 | @classmethod 164 | def platform_url(cls) -> str: 165 | return client.build_url() + 'platform' + '/' 166 | 167 | @classmethod 168 | def oauth2_url(cls) -> str: 169 | return oauth2_base + '/' 170 | 171 | def __init__(cls, *args, **kwargs) -> 'XummResource': 172 | cls.refresh_from(**kwargs) 173 | 174 | def refresh_from(cls, **kwargs): 175 | raise NotImplementedError 176 | -------------------------------------------------------------------------------- /xumm/resource/types/meta/user_tokens.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | from typing import List 6 | 7 | 8 | class UserTokenValidity(XummResource): 9 | """ 10 | Attributes: 11 | model_types (dict): The key is attribute name 12 | and the value is attribute type. 13 | attribute_map (dict): The key is attribute name 14 | and the value is json key in definition. 15 | """ 16 | required = { 17 | 'user_token': True, 18 | 'active': True, 19 | 'token_issued': True, 20 | 'token_expiration': True 21 | } 22 | 23 | model_types = { 24 | 'user_token': str, 25 | 'active': bool, 26 | 'token_issued': int, 27 | 'token_expiration': int 28 | } 29 | 30 | attribute_map = { 31 | 'user_token': 'user_token', 32 | 'active': 'active', 33 | 'token_issued': 'token_issued', 34 | 'token_expiration': 'token_expiration' 35 | } 36 | 37 | def refresh_from(cls, **kwargs): 38 | """Returns the dict as a model 39 | 40 | :param kwargs: A dict. 41 | :type: dict 42 | :return: The UserToken of this UserToken. # noqa: E501 43 | :rtype: UserToken 44 | """ 45 | cls.sanity_check(kwargs) 46 | cls._user_token = None 47 | cls._active = None 48 | cls._token_issued = None 49 | cls._token_expiration = None 50 | cls.user_token = kwargs['user_token'] 51 | cls.active = kwargs['active'] 52 | cls.token_issued = kwargs['token_issued'] 53 | cls.token_expiration = kwargs['token_expiration'] 54 | return cls 55 | 56 | @property 57 | def user_token(cls) -> str: 58 | """Gets the user_token of this UserTokenValidity. 59 | 60 | 61 | :return: The user_token of this UserTokenValidity. 62 | :rtype: str 63 | """ 64 | return cls._user_token 65 | 66 | @user_token.setter 67 | def user_token(cls, user_token: str): 68 | """Sets the user_token of this UserTokenValidity. 69 | 70 | 71 | :param user_token: The user_token of this UserTokenValidity. 72 | :type user_token: str 73 | """ 74 | if user_token is None: 75 | raise ValueError("Invalid value for `user_token`, must not be `None`") # noqa: E501 76 | 77 | cls._user_token = user_token 78 | 79 | @property 80 | def active(cls) -> str: 81 | """Gets the active of this UserTokenValidity. 82 | 83 | 84 | :return: The active of this UserTokenValidity. 85 | :rtype: str 86 | """ 87 | return cls._active 88 | 89 | @active.setter 90 | def active(cls, active: str): 91 | """Sets the active of this UserTokenValidity. 92 | 93 | 94 | :param active: The active of this UserTokenValidity. 95 | :type active: str 96 | """ 97 | if active is None: 98 | raise ValueError("Invalid value for `active`, must not be `None`") # noqa: E501 99 | 100 | cls._active = active 101 | 102 | @property 103 | def token_issued(cls) -> int: 104 | """Gets the token_issued of this UserTokenValidity. 105 | 106 | 107 | :return: The token_issued of this UserTokenValidity. 108 | :rtype: int 109 | """ 110 | return cls._token_issued 111 | 112 | @token_issued.setter 113 | def token_issued(cls, token_issued: int): 114 | """Sets the token_issued of this UserTokenValidity. 115 | 116 | 117 | :param token_issued: The token_issued of this UserTokenValidity. 118 | :type token_issued: int 119 | """ 120 | if token_issued is None: 121 | raise ValueError("Invalid value for `token_issued`, must not be `None`") # noqa: E501 122 | 123 | cls._token_issued = token_issued 124 | 125 | @property 126 | def token_expiration(cls) -> int: 127 | """Gets the token_expiration of this UserTokenValidity. 128 | 129 | 130 | :return: The token_expiration of this UserTokenValidity. 131 | :rtype: int 132 | """ 133 | return cls._token_expiration 134 | 135 | @token_expiration.setter 136 | def token_expiration(cls, token_expiration: int): 137 | """Sets the token_expiration of this UserTokenValidity. 138 | 139 | 140 | :param token_expiration: The token_expiration of this UserTokenValidity. # noqa: E501 141 | :type token_expiration: int 142 | """ 143 | if token_expiration is None: 144 | raise ValueError("Invalid value for `token_expiration`, must not be `None`") # noqa: E501 145 | 146 | cls._token_expiration = token_expiration 147 | 148 | 149 | class UserTokenResponse(XummResource): 150 | """ 151 | Attributes: 152 | model_types (dict): The key is attribute name 153 | and the value is attribute type. 154 | attribute_map (dict): The key is attribute name 155 | and the value is json key in definition. 156 | """ 157 | required = { 158 | 'tokens': True, 159 | } 160 | 161 | model_types = { 162 | 'tokens': list, 163 | } 164 | 165 | attribute_map = { 166 | 'tokens': 'tokens', 167 | } 168 | 169 | def refresh_from(cls, **kwargs): 170 | """Returns the dict as a model 171 | 172 | :param dikt: A dict. 173 | :type: dict 174 | :return: The UserTokenResponse of this UserTokenResponse. # noqa: E501 175 | :rtype: UserTokenResponse 176 | """ 177 | cls.sanity_check(kwargs) 178 | cls._tokens = None 179 | cls.tokens = [UserTokenValidity(**t) for t in kwargs['tokens']] 180 | 181 | @property 182 | def tokens(cls) -> List[UserTokenValidity]: 183 | """Gets the tokens of this UserTokenResponse. 184 | 185 | 186 | :return: The tokens of this UserTokenResponse. 187 | :rtype: List[UserTokenValidity] 188 | """ 189 | return cls._tokens 190 | 191 | @tokens.setter 192 | def tokens(cls, tokens: List[UserTokenValidity]): 193 | """Sets the tokens of this UserTokenResponse. 194 | 195 | 196 | :param tokens: The tokens of this UserTokenResponse. 197 | :type tokens: List[UserTokenValidity] 198 | """ 199 | if tokens is None: 200 | raise ValueError("Invalid value for `tokens`, must not be `None`") # noqa: E501 201 | 202 | cls._tokens = tokens 203 | -------------------------------------------------------------------------------- /tests/test_common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | import pytest 4 | from testing_config import BaseTestConfig 5 | from unittest.mock import Mock, patch 6 | from dotenv import dotenv_values 7 | 8 | import xumm 9 | 10 | 11 | class TestCommon(BaseTestConfig): 12 | 13 | def test_xumm_dotenv(cls): 14 | print('should construct based on dotenv') 15 | configs = { **dotenv_values(".env.sample") } 16 | cls.assertEqual(configs['XUMM_APIKEY'], 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') 17 | cls.assertEqual(configs['XUMM_APISECRET'], 'XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX') 18 | try: 19 | sdk = xumm.XummSdk() 20 | except Exception: 21 | cls.fail("XummSdk() raised Exception unexpectedly!") 22 | 23 | def test_xumm_construct(cls): 24 | print('should construct based on provided api key & secret') 25 | try: 26 | sdk = xumm.XummSdk( 27 | cls.json_fixtures['api']['key'], 28 | cls.json_fixtures['api']['secret'] 29 | ) 30 | except Exception: 31 | cls.fail("XummSdk() raised Exception unexpectedly!") 32 | 33 | def test_xumm_invalid_keys(cls): 34 | print('should get error results on invalid api key / secret') 35 | try: 36 | sdk = xumm.XummSdk( 37 | 'xxxxxx', 38 | 'yyyyyyy' 39 | ) 40 | cls.fail("XummSdk() raised Exception unexpectedly!") 41 | except Exception as e: 42 | cls.assertEqual(str(e), 'Invalid API secret provided. (HINT: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX).') 43 | 44 | @patch('xumm.client.requests.get') 45 | def test_xumm_ping(cls, mock_get): 46 | print('should get app name on valid credentials') 47 | sdk = xumm.XummSdk( 48 | cls.json_fixtures['api']['key'], 49 | cls.json_fixtures['api']['secret'] 50 | ) 51 | 52 | mock_get.return_value = Mock(status_code=200) 53 | mock_get.return_value.json.return_value = cls.json_fixtures['ping']['pong'] 54 | 55 | cls.assertEqual(sdk.ping().to_dict(), cls.json_fixtures['ping']['pong']['auth']) 56 | 57 | @patch('xumm.client.requests.get') 58 | def test_invalid_credentials(cls, mock_get): 59 | print('should get auth error on invalid credentials') 60 | sdk = xumm.XummSdk( 61 | cls.json_fixtures['api']['key'], 62 | cls.json_fixtures['api']['secret'] 63 | ) 64 | 65 | mock_get.return_value = Mock(status_code=403) 66 | mock_get.return_value.json.return_value = cls.json_fixtures['invalidCredentials'] 67 | 68 | with pytest.raises(xumm.error.APIError, match=r"Error code 813, see XUMM Dev Console, reference: 26279bfe-c7e1-4b12-a680-26119d8f5062"): 69 | sdk.ping() 70 | cls.fail("ping() raised Exception unexpectedly!") 71 | 72 | @patch('xumm.client.requests.get') 73 | def test_fetch_curated_assets(cls, mock_get): 74 | print('should fetch curated assets') 75 | sdk = xumm.XummSdk( 76 | cls.json_fixtures['api']['key'], 77 | cls.json_fixtures['api']['secret'] 78 | ) 79 | 80 | mock_get.return_value = Mock(status_code=200) 81 | mock_get.return_value.json.return_value = cls.json_fixtures['curatedAssets'] 82 | cls.assertEqual(sdk.get_curated_assets().to_dict(), cls.json_fixtures['curatedAssets']) 83 | 84 | @patch('xumm.client.requests.get') 85 | def test_fetch_rates(cls, mock_get): 86 | print('should fetch rates') 87 | sdk = xumm.XummSdk( 88 | cls.json_fixtures['api']['key'], 89 | cls.json_fixtures['api']['secret'] 90 | ) 91 | 92 | mock_get.return_value = Mock(status_code=200) 93 | mock_get.return_value.json.return_value = cls.json_fixtures['rates'] 94 | cls.assertEqual(sdk.get_rates('usd').to_dict(), cls.json_fixtures['rates']) 95 | 96 | @patch('xumm.client.requests.get') 97 | def test_fetch_kyc_status(cls, mock_get): 98 | print('should fetch user KYC status') 99 | sdk = xumm.XummSdk( 100 | cls.json_fixtures['api']['key'], 101 | cls.json_fixtures['api']['secret'] 102 | ) 103 | 104 | user_token = 'rPEPPER7kfTD9w2To4CQk6UCfuHM9c6GDY' 105 | mock_get.return_value = Mock(status_code=200) 106 | mock_get.return_value.json.return_value = cls.json_fixtures['kycStatus']['get'] 107 | cls.assertEqual(sdk.get_kyc_status(user_token), 'NONE') 108 | 109 | @patch('xumm.client.requests.post') 110 | def test_create_kyc_status(cls, mock_post): 111 | print('should create user KYC status') 112 | sdk = xumm.XummSdk( 113 | cls.json_fixtures['api']['key'], 114 | cls.json_fixtures['api']['secret'] 115 | ) 116 | 117 | user_token = '2557f69c-6617-40dc-9d1e-a34487cb3f90' 118 | 119 | mock_post.return_value = Mock(status_code=200) 120 | mock_post.return_value.json.return_value = cls.json_fixtures['kycStatus']['post'] 121 | 122 | cls.assertEqual(sdk.get_kyc_status(user_token), 'IN_PROGRESS') 123 | 124 | @patch('xumm.client.requests.get') 125 | def test_fetch_tx(cls, mock_get): 126 | print('should fetch an XRPL tx') 127 | sdk = xumm.XummSdk( 128 | cls.json_fixtures['api']['key'], 129 | cls.json_fixtures['api']['secret'] 130 | ) 131 | 132 | mock_get.return_value = Mock(status_code=200) 133 | mock_get.return_value.json.return_value = cls.json_fixtures['xrplTx'] 134 | 135 | cls.assertEqual(sdk.get_transaction(cls.json_fixtures['xrplTx']['txid']).to_dict(), cls.json_fixtures['xrplTx']) 136 | 137 | @patch('xumm.client.requests.post') 138 | def test_validate_user_tokens(cls, mock_post): 139 | print('should fetch user tokens') 140 | sdk = xumm.XummSdk( 141 | cls.json_fixtures['api']['key'], 142 | cls.json_fixtures['api']['secret'] 143 | ) 144 | 145 | mock_post.return_value = Mock(status_code=200) 146 | mock_post.return_value.json.return_value = cls.json_fixtures['userTokens'] 147 | 148 | user_token = cls.json_fixtures['userTokens']['tokens'][0]['user_token'] 149 | 150 | cls.assertEqual([token.to_dict() for token in sdk.verify_user_tokens([user_token])], cls.json_fixtures['userTokens']['tokens']) 151 | 152 | @patch('xumm.client.requests.post') 153 | def test_validate_user_token(cls, mock_post): 154 | print('should fetch user token') 155 | sdk = xumm.XummSdk( 156 | cls.json_fixtures['api']['key'], 157 | cls.json_fixtures['api']['secret'] 158 | ) 159 | 160 | mock_post.return_value = Mock(status_code=200) 161 | mock_post.return_value.json.return_value = cls.json_fixtures['userTokens'] 162 | 163 | user_token = cls.json_fixtures['userTokens']['tokens'][0]['user_token'] 164 | 165 | cls.assertEqual(sdk.verify_user_token(user_token).to_dict(), cls.json_fixtures['userTokens']['tokens'][0]) -------------------------------------------------------------------------------- /xumm/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from __future__ import unicode_literals 5 | 6 | import requests 7 | import textwrap 8 | 9 | from socket import gethostname 10 | 11 | from requests.exceptions import ConnectionError 12 | from requests import Response 13 | 14 | from typing import List, Dict, Any # noqa: F401 15 | 16 | from xumm import ( 17 | api_base, 18 | error, 19 | ) 20 | 21 | 22 | def build_url(endpoint: str = None) -> str: 23 | """ 24 | Returns the base url + endpoint 25 | 26 | :param endpoint: A string endpoint. 27 | :type: str 28 | :return: str 29 | """ 30 | url = api_base 31 | 32 | if endpoint: 33 | url += endpoint 34 | 35 | return url 36 | 37 | 38 | def get_env() -> str: 39 | """ 40 | Returns the sdk env 41 | :return: str 42 | """ 43 | from xumm import env 44 | return env 45 | 46 | 47 | def get_headers() -> Dict[str, object]: 48 | """ 49 | Returns the sdk headers + Authentication 50 | :return: Dict[str, object] 51 | """ 52 | from xumm import api_key, api_secret 53 | 54 | if api_key is None: 55 | raise error.AuthenticationError( 56 | 'No API key provided. (HINT: set your API key using ' 57 | '"xumm.api_key = "). You can generate API keys ' 58 | 'from the Xumm web interface.' 59 | ) 60 | if api_secret is None: 61 | raise error.AuthenticationError( 62 | 'No API secret provided. (HINT: set your API key using ' 63 | '"xumm.api_secret = "). You can generate API keys ' 64 | 'from the Xumm web interface.' 65 | ) 66 | 67 | return { 68 | 'X-API-Key': api_key, 69 | 'X-API-Secret': api_secret, 70 | 'content-type': 'application/json', 71 | 'accept': 'application/json', 72 | 'user-agent': "xumm-sdk/python ({}) requests".format(gethostname(),) 73 | } 74 | 75 | 76 | def get( 77 | url: str, 78 | access_token: str = None 79 | ) -> Dict[str, object]: 80 | """ 81 | Returns the sdk GET response 82 | 83 | :param url: A string url endpoint. 84 | :type: str 85 | :return: Dict[str, object] 86 | """ 87 | try: 88 | headers = get_headers() 89 | if isinstance(access_token, str): 90 | headers['Authorization'] = f'Bearer {access_token}' 91 | res = requests.get(url, headers=headers) 92 | except Exception as e: 93 | handle_request_error(e) 94 | return handle_response(res) 95 | 96 | 97 | def post( 98 | url: str, 99 | data: Dict[str, object] 100 | ) -> Dict[str, object]: 101 | """ 102 | Returns the sdk POST response 103 | 104 | :param url: A string url endpoint. 105 | :type: str 106 | :param data: A dictionary. 107 | :type: Dict[str, object] 108 | :return: Dict[str, object] 109 | """ 110 | try: 111 | res = requests.post(url, headers=get_headers(), json=data) 112 | except Exception as e: 113 | handle_request_error(e) 114 | return handle_response(res) 115 | 116 | 117 | def delete( 118 | url: str 119 | ) -> Dict[str, object]: 120 | """ 121 | Returns the sdk DELETE response 122 | 123 | :param url: A string url endpoint. 124 | :type: str 125 | :return: Dict[str, object] 126 | """ 127 | try: 128 | res = requests.delete(url, headers=get_headers()) 129 | except Exception as e: 130 | handle_request_error(e) 131 | return handle_response(res) 132 | 133 | 134 | def handle_response(res: Response) -> Dict[str, object]: 135 | """ 136 | Returns the sdk JSON response 137 | 138 | :param res: A string url endpoint. 139 | :type: str 140 | :return: Dict[str, object] 141 | """ 142 | try: 143 | json = res.json() 144 | except ValueError as e: 145 | handle_parse_error(e) 146 | 147 | if not (200 <= res.status_code < 300): 148 | handle_error_code(json, res.status_code, res.headers) 149 | 150 | return json 151 | 152 | 153 | def handle_request_error(e: ConnectionError): 154 | """ 155 | Throws the sdk REQUEST error responses 156 | 157 | :param e: A connection error (Network | Server). 158 | :type: ConnectionError 159 | :return: throws 160 | """ 161 | if isinstance(e, requests.exceptions.RequestException): 162 | msg = 'Unexpected error communicating with Xumm.' 163 | err = '{}: {}'.format(type(e).__name__, str(e)) 164 | else: 165 | msg = ('Unexpected error communicating with Xumm. ' 166 | 'It looks like there\'s probably a configuration ' 167 | 'issue locally.') 168 | err = 'A {} was raised'.format(type(e).__name__) 169 | if u'%s' % e: 170 | err += ' with error message {}'.format(e) 171 | else: 172 | err += ' with no error message' 173 | 174 | msg = textwrap.fill(msg) + '\n\n(Network error: {})'.format(err) 175 | raise error.APIConnectionError(msg) 176 | 177 | 178 | def handle_error_code( 179 | json: Dict[str, object], 180 | status_code: int, 181 | headers: Dict[str, object] 182 | ): 183 | """ 184 | Throws the sdk API error responses 185 | 186 | :param json: A json response object. 187 | :type: Dict[str, object] 188 | :param status_code: An integer status code. 189 | :type: int 190 | :param headers: A headers response dictionary. 191 | :type: Dict[str, object] 192 | :return: throws 193 | """ 194 | 195 | # invalid response from api 196 | if 'error' not in json: 197 | raise ValueError('Error parsing Xumm JSON response: error') 198 | 199 | # hard error 200 | if isinstance(json['error'], bool): 201 | if 'code' not in json or 'message' not in json: 202 | raise ValueError('Error parsing Xumm JSON response: code/message') 203 | 204 | err = 'Error code {}, message: {}'.format( 205 | json['code'], 206 | json['message'], 207 | ) 208 | # soft error 209 | elif isinstance(json['error'], dict): 210 | if 'code' not in json['error'] or 'reference' not in json['error']: 211 | raise ValueError( 212 | 'Error parsing Xumm JSON response: code/reference' 213 | ) 214 | 215 | err = 'Error code {}, see XUMM Dev Console, reference: {}'.format( 216 | json['error']['code'], 217 | json['error']['reference'], 218 | ) 219 | # this should neven happen 220 | else: 221 | err = 'Unexpected error' 222 | 223 | if status_code == 400: 224 | raise error.InvalidRequestError(err, status_code, headers) 225 | elif status_code == 401: 226 | raise error.AuthenticationError(err, status_code, headers) 227 | elif status_code == 404: 228 | raise error.InvalidRequestError(err, status_code, headers) 229 | elif status_code == 500: 230 | raise error.APIError(err, status_code, headers) 231 | else: 232 | raise error.APIError(err, status_code, headers) 233 | 234 | 235 | def handle_parse_error( 236 | e: ValueError, 237 | status_code: int, 238 | headers: Dict[str, object] 239 | ): 240 | """ 241 | Throws the sdk JSON error response 242 | 243 | :param e: A value error (JSON | KeyError). 244 | :type: ValueError 245 | :param status_code: An integer status code. 246 | :type: int 247 | :param headers: A headers response dictionary. 248 | :type: Dict[str, object] 249 | :return: throws 250 | """ 251 | err = '{}: {}'.format(type(e).__name__, e) 252 | msg = 'Error parsing Xumm JSON response. \n\n{}'.format(err) 253 | raise error.APIError(msg, status_code, headers) 254 | -------------------------------------------------------------------------------- /xumm/resource/types/meta/xrpl_transaction.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | from typing import Dict, Any 6 | 7 | 8 | class BalanceChange(XummResource): 9 | """ 10 | Attributes: 11 | model_types (dict): The key is attribute name 12 | and the value is attribute type. 13 | attribute_map (dict): The key is attribute name 14 | and the value is json key in definition. 15 | """ 16 | required = { 17 | 'counterparty': True, 18 | 'currency': True, 19 | 'value': True, 20 | } 21 | 22 | model_types = { 23 | 'counterparty': str, 24 | 'currency': str, 25 | 'value': str 26 | } 27 | 28 | attribute_map = { 29 | 'counterparty': 'counterparty', 30 | 'currency': 'currency', 31 | 'value': 'value' 32 | } 33 | 34 | def refresh_from(cls, **kwargs): 35 | """Returns the dict as a model 36 | 37 | :param kwargs: A dict. 38 | :type: dict 39 | :return: The BalanceChange of this BalanceChange. # noqa: E501 40 | :rtype: BalanceChange 41 | """ 42 | cls.sanity_check(kwargs) 43 | cls._counterparty = None 44 | cls._currency = None 45 | cls._value = None 46 | cls.counterparty = kwargs['counterparty'] 47 | cls.currency = kwargs['currency'] 48 | cls.value = kwargs['value'] 49 | 50 | @property 51 | def counterparty(cls) -> str: 52 | """Gets the counterparty of this BalanceChange. 53 | 54 | 55 | :return: The counterparty of this BalanceChange. 56 | :rtype: str 57 | """ 58 | return cls._counterparty 59 | 60 | @counterparty.setter 61 | def counterparty(cls, counterparty: str): 62 | """Sets the counterparty of this BalanceChange. 63 | 64 | 65 | :param counterparty: The counterparty of this BalanceChange. 66 | :type counterparty: str 67 | """ 68 | 69 | cls._counterparty = counterparty 70 | 71 | @property 72 | def currency(cls) -> str: 73 | """Gets the currency of this BalanceChange. 74 | 75 | 76 | :return: The currency of this BalanceChange. 77 | :rtype: str 78 | """ 79 | return cls._currency 80 | 81 | @currency.setter 82 | def currency(cls, currency: str): 83 | """Sets the currency of this BalanceChange. 84 | 85 | 86 | :param currency: The currency of this BalanceChange. 87 | :type currency: str 88 | """ 89 | 90 | cls._currency = currency 91 | 92 | @property 93 | def value(cls) -> str: 94 | """Gets the value of this BalanceChange. 95 | 96 | 97 | :return: The value of this BalanceChange. 98 | :rtype: str 99 | """ 100 | return cls._value 101 | 102 | @value.setter 103 | def value(cls, value: str): 104 | """Sets the value of this BalanceChange. 105 | 106 | 107 | :param value: The value of this BalanceChange. 108 | :type value: str 109 | """ 110 | 111 | cls._value = value 112 | 113 | 114 | class XrplTransaction(XummResource): 115 | """ 116 | Attributes: 117 | model_types (dict): The key is attribute name 118 | and the value is attribute type. 119 | attribute_map (dict): The key is attribute name 120 | and the value is json key in definition. 121 | """ 122 | required = { 123 | 'txid': True, 124 | 'node': True, 125 | 'transaction': True, 126 | } 127 | 128 | model_types = { 129 | 'txid': str, 130 | 'balance_changes': dict, 131 | 'node': str, 132 | 'transaction': dict 133 | } 134 | 135 | attribute_map = { 136 | 'txid': 'txid', 137 | 'balance_changes': 'balanceChanges', 138 | 'node': 'node', 139 | 'transaction': 'transaction' 140 | } 141 | 142 | def refresh_from(cls, **kwargs): 143 | """Returns the dict as a model 144 | 145 | :param kwargs: A dict. 146 | :type: dict 147 | :return: The XrplTransaction of this XrplTransaction. # noqa: E501 148 | :rtype: XrplTransaction 149 | """ 150 | cls.sanity_check(kwargs) 151 | cls._txid = None 152 | cls._balance_changes = None 153 | cls._node = None 154 | cls._transaction = None 155 | cls.txid = kwargs['txid'] 156 | cls.balance_changes = {k: [BalanceChange(**b).to_dict() for b in v] for k, v in kwargs['balanceChanges'].items()} # noqa: E501 157 | cls.node = kwargs['node'] 158 | cls.transaction = kwargs['transaction'] 159 | 160 | @property 161 | def txid(cls) -> str: 162 | """Gets the txid of this XrplTransaction. 163 | 164 | 165 | :return: The txid of this XrplTransaction. 166 | :rtype: str 167 | """ 168 | return cls._txid 169 | 170 | @txid.setter 171 | def txid(cls, txid: str): 172 | """Sets the txid of this XrplTransaction. 173 | 174 | 175 | :param txid: The txid of this XrplTransaction. 176 | :type txid: str 177 | """ 178 | if txid is None: 179 | raise ValueError("Invalid value for `txid`, must not be `None`") # noqa: E501 180 | 181 | cls._txid = txid 182 | 183 | @property 184 | def balance_changes(cls) -> Dict[str, BalanceChange]: 185 | """Gets the balance_changes of this XrplTransaction. 186 | 187 | 188 | :return: The balance_changes of this XrplTransaction. 189 | :rtype: Dict[str, BalanceChange] 190 | """ 191 | return cls._balance_changes 192 | 193 | @balance_changes.setter 194 | def balance_changes(cls, balance_changes: Dict[str, BalanceChange]): 195 | """Sets the balance_changes of this XrplTransaction. 196 | 197 | 198 | :param balance_changes: The balance_changes of this XrplTransaction. 199 | :type balance_changes: Dict[str, BalanceChange] 200 | """ 201 | if balance_changes is None: 202 | raise ValueError("Invalid value for `balance_changes`, must not be `None`") # noqa: E501 203 | 204 | cls._balance_changes = balance_changes 205 | 206 | @property 207 | def node(cls) -> str: 208 | """Gets the node of this XrplTransaction. 209 | 210 | 211 | :return: The node of this XrplTransaction. 212 | :rtype: str 213 | """ 214 | return cls._node 215 | 216 | @node.setter 217 | def node(cls, node: str): 218 | """Sets the node of this XrplTransaction. 219 | 220 | 221 | :param node: The node of this XrplTransaction. 222 | :type node: str 223 | """ 224 | if node is None: 225 | raise ValueError("Invalid value for `node`, must not be `None`") # noqa: E501 226 | 227 | cls._node = node 228 | 229 | @property 230 | def transaction(cls) -> Dict[str, Any]: 231 | """Gets the transaction of this XrplTransaction. 232 | 233 | 234 | :return: The transaction of this XrplTransaction. 235 | :rtype: Dict[str, Any] 236 | """ 237 | return cls._transaction 238 | 239 | @transaction.setter 240 | def transaction(cls, transaction: Dict[str, Any]): 241 | """Sets the transaction of this XrplTransaction. 242 | 243 | 244 | :param transaction: The transaction of this XrplTransaction. 245 | :type transaction: Dict[str, Any] 246 | """ 247 | if transaction is None: 248 | raise ValueError("Invalid value for `transaction`, must not be `None`") # noqa: E501 249 | 250 | cls._transaction = transaction 251 | -------------------------------------------------------------------------------- /xumm/resource/types/meta/rates_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class RateCurrency(XummResource): 8 | """ 9 | Attributes: 10 | model_types (dict): The key is attribute name 11 | and the value is attribute type. 12 | attribute_map (dict): The key is attribute name 13 | and the value is json key in definition. 14 | """ 15 | required = { 16 | # 'en': True, 17 | 'code': True, 18 | 'symbol': True, 19 | 'iso_decimals': True, 20 | } 21 | 22 | model_types = { 23 | 'en': str, 24 | 'code': str, 25 | 'symbol': str, 26 | 'iso_decimals': int, 27 | } 28 | 29 | attribute_map = { 30 | 'en': 'en', 31 | 'code': 'code', 32 | 'symbol': 'symbol', 33 | 'iso_decimals': 'isoDecimals', 34 | } 35 | 36 | def refresh_from(cls, **kwargs): 37 | """Returns the dict as a model 38 | 39 | :param dikt: A dict. 40 | :type: dict 41 | :return: The RateCurrency of this RateCurrency. # noqa: E501 42 | :rtype: RateCurrency 43 | """ 44 | cls.sanity_check(kwargs) 45 | cls._en = None 46 | cls._code = None 47 | cls._symbol = None 48 | cls._iso_decimals = None 49 | if 'en' in kwargs: 50 | cls.en = kwargs['en'] 51 | cls.code = kwargs['code'] 52 | cls.symbol = kwargs['symbol'] 53 | cls.iso_decimals = kwargs['isoDecimals'] 54 | 55 | @property 56 | def en(cls) -> str: 57 | """Gets the en of this RateCurrency. 58 | 59 | 60 | :return: The en of this RateCurrency. 61 | :rtype: str 62 | """ 63 | return cls._en 64 | 65 | @en.setter 66 | def en(cls, en: str): 67 | """Sets the en of this RateCurrency. 68 | 69 | 70 | :param en: The en of this RateCurrency. 71 | :type en: str 72 | """ 73 | if en is None: 74 | raise ValueError("Invalid value for `en`, must not be `None`") # noqa: E501 75 | 76 | cls._en = en 77 | 78 | @property 79 | def code(cls) -> str: 80 | """Gets the code of this RateCurrency. 81 | 82 | 83 | :return: The code of this RateCurrency. 84 | :rtype: str 85 | """ 86 | return cls._code 87 | 88 | @code.setter 89 | def code(cls, code: str): 90 | """Sets the code of this RateCurrency. 91 | 92 | 93 | :param code: The code of this RateCurrency. 94 | :type code: str 95 | """ 96 | if code is None: 97 | raise ValueError("Invalid value for `code`, must not be `None`") # noqa: E501 98 | 99 | cls._code = code 100 | 101 | @property 102 | def symbol(cls) -> str: 103 | """Gets the symbol of this RateCurrency. 104 | 105 | 106 | :return: The symbol of this RateCurrency. 107 | :rtype: str 108 | """ 109 | return cls._symbol 110 | 111 | @symbol.setter 112 | def symbol(cls, symbol: str): 113 | """Sets the symbol of this RateCurrency. 114 | 115 | 116 | :param symbol: The symbol of this RateCurrency. 117 | :type symbol: str 118 | """ 119 | if symbol is None: 120 | raise ValueError("Invalid value for `symbol`, must not be `None`") # noqa: E501 121 | 122 | cls._symbol = symbol 123 | 124 | @property 125 | def iso_decimals(cls) -> int: 126 | """Gets the iso_decimals of this RateCurrency. 127 | 128 | 129 | :return: The iso_decimals of this RateCurrency. 130 | :rtype: int 131 | """ 132 | return cls._iso_decimals 133 | 134 | @iso_decimals.setter 135 | def iso_decimals(cls, iso_decimals: int): 136 | """Sets the iso_decimals of this RateCurrency. 137 | 138 | 139 | :param iso_decimals: The iso_decimals of this RateCurrency. 140 | :type iso_decimals: int 141 | """ 142 | if iso_decimals is None: 143 | raise ValueError("Invalid value for `iso_decimals`, must not be `None`") # noqa: E501 144 | 145 | cls._iso_decimals = iso_decimals 146 | 147 | 148 | class CurrencyRef(XummResource): 149 | """ 150 | Attributes: 151 | model_types (dict): The key is attribute name 152 | and the value is attribute type. 153 | attribute_map (dict): The key is attribute name 154 | and the value is json key in definition. 155 | """ 156 | required = { 157 | 'currency': True, 158 | } 159 | 160 | model_types = { 161 | 'currency': dict, 162 | } 163 | 164 | attribute_map = { 165 | 'currency': 'currency', 166 | } 167 | 168 | def refresh_from(cls, **kwargs): 169 | """Returns the dict as a model 170 | 171 | :param dikt: A dict. 172 | :type: dict 173 | :return: The CurrencyRef of this CurrencyRef. # noqa: E501 174 | :rtype: CurrencyRef 175 | """ 176 | cls.sanity_check(kwargs) 177 | cls._currency = None 178 | cls.currency = RateCurrency(**kwargs['currency']) 179 | 180 | @property 181 | def currency(cls) -> RateCurrency: 182 | """Gets the currency of this CurrencyRef. 183 | 184 | 185 | :return: The currency of this CurrencyRef. 186 | :rtype: RateCurrency 187 | """ 188 | return cls._currency 189 | 190 | @currency.setter 191 | def currency(cls, currency: RateCurrency): 192 | """Sets the currency of this CurrencyRef. 193 | 194 | 195 | :param currency: The currency of this CurrencyRef. 196 | :type currency: RateCurrency 197 | """ 198 | if currency is None: 199 | raise ValueError("Invalid value for `currency`, must not be `None`") # noqa: E501 200 | 201 | cls._currency = currency 202 | 203 | 204 | class RatesResponse(XummResource): 205 | """ 206 | Attributes: 207 | model_types (dict): The key is attribute name 208 | and the value is attribute type. 209 | attribute_map (dict): The key is attribute name 210 | and the value is json key in definition. 211 | """ 212 | required = { 213 | 'USD': True, 214 | 'XRP': True, 215 | 'meta': True, 216 | } 217 | 218 | model_types = { 219 | 'USD': int, 220 | 'XRP': float, 221 | 'meta': dict 222 | } 223 | 224 | attribute_map = { 225 | 'USD': 'USD', 226 | 'XRP': 'XRP', 227 | 'meta': '__meta', 228 | } 229 | 230 | def refresh_from(cls, **kwargs): 231 | """Returns the dict as a model 232 | 233 | :param dikt: A dict. 234 | :type: dict 235 | :return: The RatesResponse of this RatesResponse. # noqa: E501 236 | :rtype: RatesResponse 237 | """ 238 | # cls.sanity_check(kwargs) 239 | cls._USD = None 240 | cls._XRP = None 241 | cls._meta = None 242 | cls.USD = kwargs['USD'] 243 | cls.XRP = kwargs['XRP'] 244 | cls.meta = CurrencyRef(**kwargs['__meta']) 245 | 246 | @property 247 | def USD(cls) -> int: 248 | """Gets the USD of this RatesResponse. 249 | 250 | 251 | :return: The USD of this RatesResponse. 252 | :rtype: int 253 | """ 254 | return cls._USD 255 | 256 | @USD.setter 257 | def USD(cls, USD: int): 258 | """Sets the USD of this RatesResponse. 259 | 260 | 261 | :param USD: The USD of this RatesResponse. 262 | :type USD: int 263 | """ 264 | if USD is None: 265 | raise ValueError("Invalid value for `USD`, must not be `None`") # noqa: E501 266 | 267 | cls._USD = USD 268 | 269 | @property 270 | def XRP(cls) -> float: 271 | """Gets the XRP of this RatesResponse. 272 | 273 | 274 | :return: The XRP of this RatesResponse. 275 | :rtype: float 276 | """ 277 | return cls._XRP 278 | 279 | @XRP.setter 280 | def XRP(cls, XRP: float): 281 | """Sets the XRP of this RatesResponse. 282 | 283 | 284 | :param XRP: The XRP of this RatesResponse. 285 | :type XRP: float 286 | """ 287 | if XRP is None: 288 | raise ValueError("Invalid value for `XRP`, must not be `None`") # noqa: E501 289 | 290 | cls._XRP = XRP 291 | 292 | @property 293 | def meta(cls) -> CurrencyRef: 294 | """Gets the meta of this RatesResponse. 295 | 296 | 297 | :return: The meta of this RatesResponse. 298 | :rtype: CurrencyRef 299 | """ 300 | return cls._meta 301 | 302 | @meta.setter 303 | def meta(cls, meta: CurrencyRef): 304 | """Sets the XRP of this RatesResponse. 305 | 306 | 307 | :param meta: The meta of this RatesResponse. 308 | :type meta: CurrencyRef 309 | """ 310 | if meta is None: 311 | raise ValueError("Invalid value for `meta`, must not be `None`") # noqa: E501 312 | 313 | cls._meta = meta 314 | -------------------------------------------------------------------------------- /xumm/resource/payload.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import logging 5 | from typing import Any, Union, Callable 6 | from xumm import ( 7 | client, 8 | error 9 | ) 10 | from xumm.resource import XummResource 11 | 12 | from .types import ( 13 | XummPostPayloadBodyJson, 14 | XummPostPayloadBodyBlob, 15 | XummJsonTransaction, 16 | XummPostPayloadResponse, 17 | XummDeletePayloadResponse, 18 | XummGetPayloadResponse, 19 | XummPayload, 20 | CreatedPayload, 21 | PayloadSubscription, 22 | PayloadAndSubscription, 23 | on_payload_event 24 | ) 25 | 26 | from xumm.ws_client import WSClient 27 | 28 | logger = logging.getLogger('app') 29 | 30 | 31 | class CallbackPromise: 32 | 33 | resolved = None 34 | error = None 35 | data = None 36 | 37 | def in_res(cls, args): 38 | # print('RES') 39 | cls.data = args 40 | return args 41 | 42 | def in_rej(cls, error): 43 | # print('REJ') 44 | cls.error = error 45 | return error 46 | 47 | def __init__(cls): 48 | cls.resolve_fn = cls.in_res 49 | cls.reject_fn = cls.in_rej 50 | cls.data = None 51 | 52 | def _resolve(cls, arg: Any): 53 | if not cls.data and not cls.error: 54 | cls.resolve_fn(arg) 55 | 56 | def _reject(cls, error): 57 | if not cls.data and not cls.error: 58 | cls.reject_fn(error) 59 | 60 | async def _resolved(cls): 61 | while not cls.data and not cls.error: 62 | continue 63 | 64 | if cls.error: 65 | return None 66 | 67 | return cls.data 68 | 69 | 70 | class PayloadResource(XummResource): 71 | 72 | _conn: WSClient = None 73 | _callback: Callable = None 74 | 75 | @classmethod 76 | def post_url(cls) -> str: 77 | """ 78 | Gets the POST url of this PayloadResource 79 | 80 | :param id: A string id. 81 | :type: str 82 | :return: The POST url of this PayloadResource. 83 | :rtype: str 84 | """ 85 | return super(PayloadResource, cls).platform_url() + 'payload' + '/' 86 | 87 | @classmethod 88 | def get_url(cls, id: str = None) -> str: 89 | """ 90 | Gets the GET url of this PayloadResource 91 | 92 | :param id: A string id. 93 | :type: str 94 | :return: The GET url of this PayloadResource. 95 | :rtype: str 96 | """ 97 | return super(PayloadResource, cls).platform_url() + 'payload' + '/' + id # noqa: E501 98 | 99 | @classmethod 100 | def delete_url(cls, id: str = None) -> str: 101 | """ 102 | Gets the DELETE url of this PayloadResource 103 | 104 | :param id: A string id. 105 | :type: str 106 | :return: The DELETE url of this PayloadResource. 107 | :rtype: str 108 | """ 109 | return super(PayloadResource, cls).platform_url() + 'payload' + '/' + id # noqa: E501 110 | 111 | def refresh_from(cls, **kwargs) -> 'PayloadResource': 112 | return cls 113 | 114 | @classmethod 115 | def resolve_payload( 116 | cls, 117 | payload: Union[str, XummPayload, CreatedPayload] 118 | ) -> XummPayload: 119 | """ 120 | Returns the XummPayload 121 | 122 | :param payload: A payload string or json object. 123 | :type: Union[str, XummPayload, CreatedPayload] 124 | :return: The XummPayload json dictionary 125 | :rtype: XummPayload 126 | """ 127 | if isinstance(payload, str): 128 | return cls.get(payload) 129 | if isinstance(payload, CreatedPayload): 130 | return cls.get(payload.uuid) 131 | if isinstance(payload, XummPayload): 132 | return payload 133 | 134 | raise error.APIError('Could not resolve payload (not found)') 135 | 136 | @classmethod 137 | def create( 138 | cls, 139 | payload: Union[ 140 | XummPostPayloadBodyJson, 141 | XummPostPayloadBodyBlob, 142 | XummJsonTransaction 143 | ], 144 | ) -> XummPostPayloadResponse: 145 | """Returns the dict as a model 146 | 147 | :param payload: The payload of this payload_create. 148 | :type payload: 149 | XummPostPayloadBodyJson | 150 | XummPostPayloadBodyBlob | 151 | XummJsonTransaction 152 | 153 | :return: The XummPostPayloadResponse of this XummPostPayloadResponse. # noqa: E501 154 | :rtype: XummPostPayloadResponse 155 | """ 156 | direct_tx = 'TransactionType' in payload and 'txjson' not in payload 157 | clone_payload = {'txjson': payload} if direct_tx else payload 158 | 159 | res = client.post(cls.post_url(), clone_payload) 160 | return XummPostPayloadResponse(**res) 161 | 162 | @classmethod 163 | def cancel( 164 | cls, 165 | id: str = None, 166 | ) -> XummDeletePayloadResponse: 167 | """Returns the dict as a model 168 | 169 | :return: The XummDeletePayloadResponse of this XummDeletePayloadResponse. # noqa: E501 170 | :rtype: XummDeletePayloadResponse 171 | """ 172 | 173 | res = client.delete(cls.delete_url(id)) 174 | return XummDeletePayloadResponse(**res) 175 | 176 | @classmethod 177 | def get( 178 | cls, 179 | id: str = None, 180 | ) -> XummGetPayloadResponse: 181 | """Returns the dict as a model 182 | 183 | :return: The XummGetPayloadResponse of this XummGetPayloadResponse. # noqa: E501 184 | :rtype: XummGetPayloadResponse 185 | """ 186 | res = client.get(cls.get_url(id)) 187 | return XummGetPayloadResponse(**res) 188 | 189 | @classmethod 190 | async def subscribe( 191 | cls, 192 | payload: Union[str, XummPayload, CreatedPayload], 193 | callback: on_payload_event 194 | ) -> PayloadSubscription: 195 | """Subscribe to a channel 196 | :returns: PayloadSubscription 197 | """ 198 | from xumm import env 199 | 200 | callback_promise = CallbackPromise() 201 | payload_details = cls.resolve_payload(payload) 202 | 203 | if payload_details: 204 | 205 | def on_open(connection): 206 | logger.debug( 207 | 'Payload {}: Subscription active (WebSocket opened)' 208 | .format(payload_details.meta.uuid) 209 | ) 210 | 211 | def on_message(json_data): 212 | if json_data and cls._callback and 'devapp_fetched' not in json_data: # noqa: E501 213 | try: 214 | callback_result = cls._callback({ 215 | 'uuid': payload_details.meta.uuid, 216 | 'data': json_data, 217 | 'resolve': callback_promise._resolve, 218 | 'payload': payload_details 219 | }) 220 | 221 | if callback_result: 222 | callback_promise._resolve(callback_result) 223 | 224 | except Exception as e: 225 | logger.debug( 226 | 'Payload {}: Callback error: {}' 227 | .format(payload_details.meta.uuid, e) 228 | ) 229 | 230 | def on_error(error): 231 | logger.debug( 232 | 'Payload {}: Subscription error: {}' 233 | .format(payload_details.meta.uuid, error) 234 | ) 235 | cls._conn.disconnect() 236 | callback_promise._reject(error) 237 | 238 | def on_close(): 239 | logger.debug( 240 | 'Payload {}: Subscription ended (WebSocket closed)' 241 | .format(payload_details.meta.uuid) 242 | ) 243 | callback_promise._reject('closed') 244 | 245 | cls._callback = callback 246 | cls._conn = WSClient( 247 | log_level=logging.DEBUG if env == 'sandbox' else logging.ERROR, 248 | server='ws://127.0.0.1:8765' if env == 'sandbox' else 'wss://xumm.app/sign/{}'.format(payload_details.meta.uuid), # noqa: E501 249 | on_response=on_message, 250 | on_error=on_error, 251 | on_close=on_close, 252 | on_open=on_open 253 | ) 254 | cls._conn.connect(nowait=False) 255 | 256 | resp = { 257 | 'payload': payload_details.to_dict(), 258 | 'resolve': callback_promise._resolve, 259 | 'resolved': callback_promise._resolved, 260 | 'websocket': cls._conn 261 | } 262 | return PayloadSubscription(**resp) 263 | 264 | raise ValueError('Could not subscribe: could not fetch payload') 265 | 266 | @classmethod 267 | async def create_and_subscribe( 268 | cls, 269 | payload: CreatedPayload, 270 | callback: on_payload_event 271 | ) -> PayloadAndSubscription: 272 | """Create payload and subscribe to a channel 273 | :returns: PayloadAndSubscription 274 | """ 275 | created_payload = cls.create(payload) 276 | if created_payload: 277 | subscription = await cls.subscribe(created_payload, callback) 278 | resp = { 279 | 'created': created_payload.to_dict(), 280 | 'payload': subscription.payload.to_dict(), 281 | 'resolve': subscription.resolve, 282 | 'resolved': subscription.resolved, 283 | 'websocket': subscription.websocket 284 | } 285 | return PayloadAndSubscription(**resp) 286 | raise ValueError('Error creating payload or subscribing to created payload') # noqa: E501 287 | 288 | @classmethod 289 | def unsubscribe(cls): 290 | """Unsubscribe to a channel 291 | :returns: None 292 | """ 293 | cls._conn.disconnect() 294 | -------------------------------------------------------------------------------- /xumm/resource/types/oauth2/oauth2_user_info_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | from xumm.resource import XummResource 5 | 6 | 7 | class OAuth2UserInfoResponse(XummResource): 8 | """ 9 | Attributes: 10 | model_types (dict): The key is attribute name 11 | and the value is attribute type. 12 | attribute_map (dict): The key is attribute name 13 | and the value is json key in definition. 14 | """ 15 | required = { 16 | 'sub': True, 17 | 'email': True, 18 | 'picture': True, 19 | 'account': True, 20 | 'name': True, 21 | 'domain': True, 22 | 'blocked': True, 23 | 'source': True, 24 | 'force_dtag': True, 25 | 'kyc_approved': True, 26 | 'pro_subscription': True, 27 | 'network_type': True, 28 | 'network_endpoint': True, 29 | } 30 | 31 | model_types = { 32 | 'sub': str, 33 | 'email': str, 34 | 'picture': str, 35 | 'account': str, 36 | 'name': str, 37 | 'domain': str, 38 | 'blocked': bool, 39 | 'source': str, 40 | 'force_dtag': bool, 41 | 'kyc_approved': bool, 42 | 'pro_subscription': bool, 43 | 'network_type': str, 44 | 'network_endpoint': str, 45 | } 46 | 47 | attribute_map = { 48 | 'sub': 'sub', 49 | 'email': 'email', 50 | 'picture': 'picture', 51 | 'account': 'account', 52 | 'name': 'name', 53 | 'domain': 'domain', 54 | 'blocked': 'blocked', 55 | 'source': 'source', 56 | 'force_dtag': 'force_dtag', 57 | 'kyc_approved': 'kycApproved', 58 | 'pro_subscription': 'proSubscription', 59 | 'network_type': 'networkType', 60 | 'network_endpoint': 'networkEndpoint', 61 | } 62 | 63 | def refresh_from(cls, **kwargs): 64 | """Returns the dict as a model 65 | 66 | :param kwargs: A dict. 67 | :type: dict 68 | :return: The XappUserAccountInfo of this XappUserAccountInfo. # noqa: E501 69 | :rtype: XappUserAccountInfo 70 | """ 71 | cls.sanity_check(kwargs) 72 | cls._sub = None 73 | cls._email = None 74 | cls._picture = None 75 | cls._account = None 76 | cls._name = None 77 | cls._domain = None 78 | cls._blocked = None 79 | cls._source = None 80 | cls._force_dtag = None 81 | cls._kyc_approved = None 82 | cls._pro_subscription = None 83 | cls._network_type = None 84 | cls._network_endpoint = None 85 | cls.sub = kwargs['sub'] 86 | cls.email = kwargs['email'] 87 | cls.picture = kwargs['picture'] 88 | cls.account = kwargs['account'] 89 | cls.name = kwargs['name'] 90 | cls.domain = kwargs['domain'] 91 | cls.blocked = kwargs['blocked'] 92 | cls.source = kwargs['source'] 93 | cls.force_dtag = kwargs['force_dtag'] 94 | cls.kyc_approved = kwargs['kycApproved'] 95 | cls.pro_subscription = kwargs['proSubscription'] 96 | cls.network_type = kwargs['networkType'] 97 | cls.network_endpoint = kwargs['networkEndpoint'] 98 | 99 | @property 100 | def sub(self) -> str: 101 | """Gets the sub of this XappUserAccountInfo. 102 | 103 | 104 | :return: The sub of this XappUserAccountInfo. 105 | :rtype: str 106 | """ 107 | return self._sub 108 | 109 | @sub.setter 110 | def sub(self, sub: str): 111 | """Sets the sub of this XappUserAccountInfo. 112 | 113 | 114 | :param sub: The sub of this XappUserAccountInfo. 115 | :type sub: str 116 | """ 117 | self._sub = sub 118 | 119 | @property 120 | def email(self) -> str: 121 | """Gets the email of this XappUserAccountInfo. 122 | 123 | 124 | :return: The email of this XappUserAccountInfo. 125 | :rtype: str 126 | """ 127 | return self._email 128 | 129 | @email.setter 130 | def email(self, email: str): 131 | """Sets the email of this XappUserAccountInfo. 132 | 133 | 134 | :param email: The email of this XappUserAccountInfo. 135 | :type email: str 136 | """ 137 | self._email = email 138 | 139 | @property 140 | def picture(self) -> str: 141 | """Gets the picture of this XappUserAccountInfo. 142 | 143 | 144 | :return: The picture of this XappUserAccountInfo. 145 | :rtype: str 146 | """ 147 | return self._picture 148 | 149 | @picture.setter 150 | def picture(self, picture: str): 151 | """Sets the picture of this XappUserAccountInfo. 152 | 153 | 154 | :param picture: The picture of this XappUserAccountInfo. 155 | :type picture: str 156 | """ 157 | self._picture = picture 158 | 159 | @property 160 | def account(self) -> str: 161 | """Gets the account of this XappUserAccountInfo. 162 | 163 | 164 | :return: The account of this XappUserAccountInfo. 165 | :rtype: str 166 | """ 167 | return self._account 168 | 169 | @account.setter 170 | def account(self, account: str): 171 | """Sets the account of this XappUserAccountInfo. 172 | 173 | 174 | :param account: The account of this XappUserAccountInfo. 175 | :type account: str 176 | """ 177 | self._account = account 178 | 179 | @property 180 | def name(self) -> str: 181 | """Gets the name of this XappUserAccountInfo. 182 | 183 | 184 | :return: The name of this XappUserAccountInfo. 185 | :rtype: str 186 | """ 187 | return self._name 188 | 189 | @name.setter 190 | def name(self, name: str): 191 | """Sets the name of this XappUserAccountInfo. 192 | 193 | 194 | :param name: The name of this XappUserAccountInfo. 195 | :type name: str 196 | """ 197 | self._name = name 198 | 199 | @property 200 | def domain(self) -> str: 201 | """Gets the domain of this XappUserAccountInfo. 202 | 203 | 204 | :return: The domain of this XappUserAccountInfo. 205 | :rtype: str 206 | """ 207 | return self._domain 208 | 209 | @domain.setter 210 | def domain(self, domain: str): 211 | """Sets the domain of this XappUserAccountInfo. 212 | 213 | 214 | :param domain: The domain of this XappUserAccountInfo. 215 | :type domain: str 216 | """ 217 | self._domain = domain 218 | 219 | @property 220 | def blocked(self) -> bool: 221 | """Gets the blocked of this XappUserAccountInfo. 222 | 223 | 224 | :return: The blocked of this XappUserAccountInfo. 225 | :rtype: bool 226 | """ 227 | return self._blocked 228 | 229 | @blocked.setter 230 | def blocked(self, blocked: bool): 231 | """Sets the blocked of this XappUserAccountInfo. 232 | 233 | 234 | :param blocked: The blocked of this XappUserAccountInfo. 235 | :type blocked: bool 236 | """ 237 | self._blocked = blocked 238 | 239 | @property 240 | def source(self) -> str: 241 | """Gets the source of this XappUserAccountInfo. 242 | 243 | 244 | :return: The source of this XappUserAccountInfo. 245 | :rtype: str 246 | """ 247 | return self._source 248 | 249 | @source.setter 250 | def source(self, source: str): 251 | """Sets the source of this XappUserAccountInfo. 252 | 253 | 254 | :param source: The source of this XappUserAccountInfo. 255 | :type source: str 256 | """ 257 | self._source = source 258 | 259 | @property 260 | def force_dtag(self) -> bool: 261 | """Gets the force_dtag of this XappUserAccountInfo. 262 | 263 | 264 | :return: The force_dtag of this XappUserAccountInfo. 265 | :rtype: bool 266 | """ 267 | return self._force_dtag 268 | 269 | @force_dtag.setter 270 | def force_dtag(self, force_dtag: bool): 271 | """Sets the force_dtag of this XappUserAccountInfo. 272 | 273 | 274 | :param force_dtag: The force_dtag of this XappUserAccountInfo. 275 | :type force_dtag: bool 276 | """ 277 | self._force_dtag = force_dtag 278 | 279 | @property 280 | def kyc_approved(self) -> bool: 281 | """Gets the kyc_approved of this XappUserAccountInfo. 282 | 283 | 284 | :return: The kyc_approved of this XappUserAccountInfo. 285 | :rtype: bool 286 | """ 287 | return self._kyc_approved 288 | 289 | @kyc_approved.setter 290 | def kyc_approved(self, kyc_approved: bool): 291 | """Sets the kyc_approved of this XappUserAccountInfo. 292 | 293 | 294 | :param kyc_approved: The kyc_approved of this XappUserAccountInfo. 295 | :type kyc_approved: bool 296 | """ 297 | self._kyc_approved = kyc_approved 298 | 299 | @property 300 | def pro_subscription(self) -> bool: 301 | """Gets the pro_subscription of this XappUserAccountInfo. 302 | 303 | 304 | :return: The pro_subscription of this XappUserAccountInfo. 305 | :rtype: bool 306 | """ 307 | return self._pro_subscription 308 | 309 | @pro_subscription.setter 310 | def pro_subscription(self, pro_subscription: bool): 311 | """Sets the pro_subscription of this XappUserAccountInfo. 312 | 313 | 314 | :param pro_subscription: The pro_subscription of this XappUserAccountInfo. # noqa: E501 315 | :type pro_subscription: bool 316 | """ 317 | self._pro_subscription = pro_subscription 318 | 319 | @property 320 | def network_type(self) -> str: 321 | """Gets the network_type of this XappUserAccountInfo. 322 | 323 | 324 | :return: The network_type of this XappUserAccountInfo. 325 | :rtype: str 326 | """ 327 | return self._network_type 328 | 329 | @network_type.setter 330 | def network_type(self, network_type: str): 331 | """Sets the network_type of this XappUserAccountInfo. 332 | 333 | 334 | :param network_type: The network_type of this XappUserAccountInfo. # noqa: E501 335 | :type network_type: str 336 | """ 337 | self._network_type = network_type 338 | 339 | @property 340 | def network_endpoint(self) -> str: 341 | """Gets the network_endpoint of this XappUserAccountInfo. 342 | 343 | 344 | :return: The network_endpoint of this XappUserAccountInfo. 345 | :rtype: str 346 | """ 347 | return self._network_endpoint 348 | 349 | @network_endpoint.setter 350 | def network_endpoint(self, network_endpoint: str): 351 | """Sets the network_endpoint of this XappUserAccountInfo. 352 | 353 | 354 | :param network_endpoint: The network_endpoint of this XappUserAccountInfo. # noqa: E501 355 | :type network_endpoint: str 356 | """ 357 | self._network_endpoint = network_endpoint 358 | -------------------------------------------------------------------------------- /tests/test_payload_subscribe.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import sys 5 | import time 6 | import json 7 | import asyncio 8 | import websockets 9 | 10 | from multiprocessing.sharedctypes import Value 11 | from threading import Thread, main_thread 12 | 13 | from testing_config import BaseTestConfig, AsyncioTestCase 14 | from tests.fixtures import ( 15 | xumm_api as test_fixtures, 16 | ) 17 | from unittest.mock import Mock, patch 18 | 19 | 20 | import xumm 21 | from xumm.ws_client import WSClient 22 | from xumm.error import APIError 23 | from xumm.resource.payload import CallbackPromise 24 | 25 | 26 | COUNT = 0 27 | 28 | # import pytest 29 | # @pytest.mark.skip(reason="Using Prod Cert") 30 | class TestPayloadSubscribe(BaseTestConfig): 31 | 32 | server = None 33 | thread = None 34 | 35 | @classmethod 36 | async def start_server(cls, ws, path): 37 | try: 38 | print('MOCK SOCKET OPEN: {}'.format(ws.open)) 39 | 40 | await ws.send(json.dumps(test_fixtures.subscription_updates()['expire'])) # noqa: E501 41 | print('SENT EXPIRE') 42 | print(test_fixtures.subscription_updates()['expire']) 43 | 44 | await ws.send(json.dumps(test_fixtures.subscription_updates()['opened'])) # noqa: E501 45 | print('SENT OPENED') 46 | print(test_fixtures.subscription_updates()['opened']) 47 | 48 | await ws.send(json.dumps(test_fixtures.subscription_updates()['rejected'])) # noqa: E501 49 | print('SENT REJECTED') 50 | print(test_fixtures.subscription_updates()['rejected']) 51 | 52 | await asyncio.sleep(1) 53 | 54 | except KeyboardInterrupt: 55 | ws.close() 56 | 57 | except Exception as e: 58 | print('on_open Error: {}'.format(e)) 59 | ws.close() 60 | 61 | @classmethod 62 | def startup_server(cls): 63 | loop = asyncio.new_event_loop() 64 | asyncio.set_event_loop(loop) 65 | start_server = websockets.serve(cls.start_server, "127.0.0.1", 8765) 66 | asyncio.get_event_loop().run_until_complete(start_server) 67 | asyncio.get_event_loop().run_forever() 68 | print('WS RUNNING') 69 | 70 | @classmethod 71 | def setUp(cls): 72 | print('SET UP TEST') 73 | 74 | thread = Thread( 75 | target=cls.startup_server, 76 | daemon=True 77 | ) 78 | thread.start() 79 | 80 | xumm.env = 'sandbox' 81 | cls.sdk = xumm.XummSdk( 82 | cls.json_fixtures['api']['key'], 83 | cls.json_fixtures['api']['secret'] 84 | ) 85 | 86 | print('SDK INIT') 87 | 88 | def tearDown(cls): 89 | print('TEAR DOWN TEST') 90 | # print(ee) 91 | 92 | def _test_callback_promise_result(cls): 93 | callback_promise = CallbackPromise() 94 | callback_promise._resolve({'data': 'something'}) 95 | cls.assertEqual(callback_promise.data, {'data': 'something'}) 96 | cls.assertEqual(callback_promise.error, None) 97 | 98 | def _test_callback_promise_reject(cls): 99 | callback_promise = CallbackPromise() 100 | callback_promise._reject(APIError('Invalid Rejection')) 101 | cls.assertEqual(callback_promise.data, None) 102 | cls.assertEqual(str(callback_promise.error), 'Invalid Rejection') 103 | 104 | def _test_callback_promise_await(cls): 105 | callback_promise = CallbackPromise() 106 | 107 | async def run_pass(cb: CallbackPromise = None): 108 | result = await cb._resolved() 109 | cls.assertEqual(result, {'data': 'something'}) 110 | 111 | callback_promise._resolve({'data': 'something'}) 112 | asyncio.run(run_pass(callback_promise)) 113 | 114 | async def run_fail(cb: CallbackPromise = None): 115 | result = await cb._resolved() 116 | cls.assertEqual(result, None) 117 | 118 | callback_promise._reject(APIError('Invalid Rejection')) 119 | asyncio.run(run_fail(callback_promise)) 120 | 121 | @patch('xumm.client.requests.get') 122 | async def _test_payload_subscribe_inner(cls, mock_get): 123 | print('should susbscribe & resolve using inner resolve method @ callback') 124 | 125 | mock_get.return_value = Mock(status_code=200) 126 | mock_get.return_value.json.return_value = cls.json_fixtures['payload']['get'] 127 | def callback_func(event): 128 | if 'signed' in event['data']: 129 | return event['resolve'](event['data']) 130 | 131 | subscription_socket = await cls.sdk.payload.subscribe( 132 | '0ef943e9-18ae-4fa2-952a-6f55dce363f0', 133 | callback_func, 134 | ) 135 | 136 | test_dict = { 137 | 'payload': subscription_socket.payload.to_dict(), 138 | 'resolve': type(subscription_socket.resolve).__name__, 139 | 'resolved': type(subscription_socket.resolved).__name__, 140 | 'websocket': type(subscription_socket.websocket).__name__, 141 | } 142 | 143 | cls.assertEqual(test_dict, { 144 | 'payload': cls.json_fixtures['payload']['get'], 145 | 'resolve': 'method', 146 | 'resolved': 'method', 147 | 'websocket': WSClient.__name__, 148 | }) 149 | 150 | cls.assertEqual(subscription_socket.websocket.status(), 101) 151 | cls.assertEqual(await subscription_socket.resolved(), test_fixtures.subscription_updates()['rejected']) 152 | # expect(wsEol).toEqual(expect.arrayContaining([subscriptionSocket.websocket.readyState])) 153 | time.sleep(3) 154 | 155 | 156 | @patch('xumm.client.requests.get') 157 | async def _test_payload_subscribe_return(cls, mock_get): 158 | print('should susbscribe & resolve using return @ callback') 159 | mock_get.return_value = Mock(status_code=200) 160 | mock_get.return_value.json.return_value = cls.json_fixtures['payload']['get'] 161 | 162 | def received_ws_message(event): 163 | global COUNT 164 | COUNT = COUNT + 1 165 | 166 | subscription_socket = await cls.sdk.payload.subscribe( 167 | '0ef943e9-18ae-4fa2-952a-6f55dce363f0', 168 | received_ws_message, 169 | ) 170 | 171 | test_dict = { 172 | 'payload': subscription_socket.payload.to_dict(), 173 | 'resolve': type(subscription_socket.resolve).__name__, 174 | 'resolved': type(subscription_socket.resolved).__name__, 175 | 'websocket': type(subscription_socket.websocket).__name__, 176 | } 177 | 178 | cls.assertEqual(test_dict, { 179 | 'payload': cls.json_fixtures['payload']['get'], 180 | 'resolve': 'method', 181 | 'resolved': 'method', 182 | 'websocket': WSClient.__name__, 183 | }) 184 | 185 | cls.assertEqual(subscription_socket.websocket.status(), 101) 186 | # cls.assertEqual(COUNT, 3) 187 | subscription_socket.resolve({'dummyObject': True}) 188 | cls.assertEqual(await subscription_socket.resolved(), {'dummyObject': True}) 189 | # expect(wsEol).toEqual(expect.arrayContaining([subscriptionSocket.websocket.readyState])) 190 | time.sleep(3) 191 | 192 | 193 | @patch('xumm.client.requests.get') 194 | @patch('xumm.client.requests.post') 195 | async def _test_payload_create_subscribe_inner(cls, mock_post, mock_get): 196 | print('should create, susbscribe & resolve using inner @ callback') 197 | mock_post.return_value = Mock(status_code=200) 198 | mock_post.return_value.json.return_value = cls.json_fixtures['payload']['created'] 199 | mock_get.return_value = Mock(status_code=200) 200 | mock_get.return_value.json.return_value = cls.json_fixtures['payload']['get'] 201 | 202 | def callback_func(event): 203 | if 'signed' in event['data']: 204 | return event['resolve'](event['data']) # FIX 205 | 206 | subscription_socket = await cls.sdk.payload.subscribe( 207 | '0ef943e9-18ae-4fa2-952a-6f55dce363f0', 208 | callback_func, 209 | ) 210 | 211 | test_dict = { 212 | 'payload': subscription_socket.payload.to_dict(), 213 | 'resolve': type(subscription_socket.resolve).__name__, 214 | 'resolved': type(subscription_socket.resolved).__name__, 215 | 'websocket': type(subscription_socket.websocket).__name__, 216 | } 217 | 218 | cls.assertEqual(test_dict, { 219 | 'payload': cls.json_fixtures['payload']['get'], 220 | 'resolve': 'method', 221 | 'resolved': 'method', 222 | 'websocket': WSClient.__name__, 223 | }) 224 | 225 | cls.assertEqual(subscription_socket.websocket.status(), 101) 226 | cls.assertEqual(await subscription_socket.resolved(), test_fixtures.subscription_updates()['rejected']) 227 | # expect(wsEol).toEqual(expect.arrayContaining([subscriptionSocket.websocket.readyState])) 228 | time.sleep(3) 229 | 230 | 231 | @patch('xumm.client.requests.get') 232 | @patch('xumm.client.requests.post') 233 | async def _test_payload_create_subscribe_return(cls, mock_post, mock_get): 234 | print('should create, susbscribe & resolve using return @ callback') 235 | 236 | mock_post.return_value = Mock(status_code=200) 237 | mock_post.return_value.json.return_value = cls.json_fixtures['payload']['created'] 238 | mock_get.return_value = Mock(status_code=200) 239 | mock_get.return_value.json.return_value = cls.json_fixtures['payload']['get'] 240 | 241 | def callback_func(event): 242 | if 'signed' in event['data']: 243 | return event['resolve'](event['data']) 244 | 245 | subscription_socket = await cls.sdk.payload.subscribe( 246 | '0ef943e9-18ae-4fa2-952a-6f55dce363f0', 247 | callback_func, 248 | ) 249 | 250 | test_dict = { 251 | 'payload': subscription_socket.payload.to_dict(), 252 | 'resolve': type(subscription_socket.resolve).__name__, 253 | 'resolved': type(subscription_socket.resolved).__name__, 254 | 'websocket': type(subscription_socket.websocket).__name__, 255 | } 256 | 257 | cls.assertEqual(test_dict, { 258 | 'payload': cls.json_fixtures['payload']['get'], 259 | 'resolve': 'method', 260 | 'resolved': 'method', 261 | 'websocket': WSClient.__name__, 262 | }) 263 | 264 | cls.assertEqual(subscription_socket.websocket.status(), 101) 265 | cls.assertEqual(await subscription_socket.resolved(), test_fixtures.subscription_updates()['rejected']) 266 | # expect(wsEol).toEqual(expect.arrayContaining([subscriptionSocket.websocket.readyState])) 267 | time.sleep(3) 268 | 269 | async def _test_payload_subscribe(cls): 270 | 271 | await asyncio.sleep(3) 272 | 273 | await cls._test_payload_subscribe_inner() 274 | 275 | await cls._test_payload_subscribe_return() 276 | 277 | await cls._test_payload_create_subscribe_inner() 278 | 279 | await cls._test_payload_create_subscribe_return() 280 | 281 | return True 282 | 283 | 284 | def test_ws_tests(cls): 285 | loop = asyncio.new_event_loop() 286 | asyncio.set_event_loop(loop) 287 | loop.run_until_complete(cls._test_payload_subscribe()) 288 | loop.close() 289 | 290 | cls._test_callback_promise_result() 291 | cls._test_callback_promise_reject() 292 | # cls._test_callback_promise_await() 293 | -------------------------------------------------------------------------------- /xumm/ws_client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | 4 | import logging 5 | import time 6 | import json 7 | import sys 8 | from multiprocessing import Queue 9 | from threading import Thread, Event, Timer 10 | from typing import Dict, Any 11 | 12 | from websocket import ( 13 | enableTrace, 14 | WebSocketApp 15 | ) 16 | 17 | 18 | class WSClient(Thread): 19 | """ 20 | Higher level of APIs are provided. 21 | The interface is like JavaScript WebSocket object. 22 | """ 23 | def __init__( 24 | cls, 25 | server: str = None, 26 | timeout: int = None, 27 | log_level: str = None, 28 | *args, 29 | **kwargs 30 | ): 31 | """ 32 | Args: 33 | server: xumm node url. 34 | timeout: connection timeout seconds 35 | log_level: loggin level 36 | on_open: callable object which is called at opening websocket. 37 | on_reconnect: callable object which is called at reconnecting 38 | on_error: callable object which is called when we get error. 39 | on_close: callable object which is called when closed the connection. # noqa: E501 40 | on_response: callback object which is called when we recieve message # noqa: E501 41 | """ 42 | 43 | # assing any callback method 44 | available_callbacks = [ 45 | 'on_open', 46 | 'on_reconnect', 47 | 'on_close', 48 | 'on_error', 49 | 'on_response' 50 | ] 51 | 52 | for key, value in kwargs.items(): 53 | if(key in available_callbacks): 54 | setattr(cls, key, value) 55 | 56 | cls.socket: WebSocketApp = None 57 | cls.server = server or 'wss://xumm.app/sign' 58 | cls.responseEvents = dict() 59 | cls.q = Queue() 60 | 61 | # ledger status 62 | cls._ledgerVersion = None 63 | cls._fee_base = None 64 | cls._fee_ref = None 65 | 66 | # Connection Handling Attributes 67 | cls.connected = Event() 68 | cls.disconnect_called = Event() 69 | cls.reconnect_required = Event() 70 | cls.reconnect_interval = 10 71 | cls.paused = Event() 72 | 73 | # Setup Timer attributes 74 | # Tracks API Connection & Responses 75 | cls.ping_timer = None 76 | cls.ping_interval = 10 77 | 78 | # Tracks Websocket Connection 79 | cls.connection_timer = None 80 | cls.connection_timeout = timeout if timeout else 30 81 | cls.response_timeout = timeout if timeout else 30 82 | 83 | # Tracks responses from send_ping() 84 | cls.pong_timer = None 85 | cls.pong_received = False 86 | cls.pong_timeout = 30 87 | 88 | # Logging stuff 89 | cls.log = logging.getLogger(cls.__module__) 90 | logging.basicConfig(stream=sys.stdout, format="[%(filename)s:%(lineno)s - %(funcName)10s() : %(message)s") # noqa: E501 91 | if log_level == logging.DEBUG: 92 | enableTrace(True) 93 | cls.log.setLevel(level=log_level if log_level else logging.ERROR) 94 | 95 | # Call init of Thread and pass remaining args and kwargs 96 | Thread.__init__(cls) 97 | cls.daemon = False 98 | 99 | def connect(cls, nowait: bool = True) -> None: 100 | """ 101 | Simulate cls.start(), run the main thread 102 | :return: 103 | """ 104 | cls.start() 105 | 106 | if not nowait: 107 | return cls.connected.wait() 108 | 109 | def disconnect(cls) -> None: 110 | """ 111 | Disconnects from the websocket connection and joins the Thread. 112 | :return: 113 | """ 114 | cls.log.debug("Disconnecting from API..") 115 | cls.reconnect_required.clear() 116 | cls.disconnect_called.set() 117 | if cls.socket: 118 | cls.socket.close() 119 | 120 | # stop timers 121 | cls._stop_timers() 122 | 123 | cls.join(timeout=1) 124 | 125 | def status(cls) -> int: 126 | """ 127 | Get socket status. 128 | :return: 129 | """ 130 | if cls.connected.is_set(): 131 | return 101 132 | return 500 133 | 134 | def reconnect(cls) -> None: 135 | """ 136 | Issues a reconnection by setting the reconnect_required event. 137 | :return: 138 | """ 139 | # Reconnect attempt at cls.reconnect_interval 140 | cls.log.debug("Initiation reconnect sequence..") 141 | cls.connected.clear() 142 | cls.reconnect_required.set() 143 | if cls.socket: 144 | cls.socket.close() 145 | 146 | def _connect(cls) -> None: 147 | """ 148 | Creates a websocket connection. 149 | :return: 150 | """ 151 | cls.log.debug("Initializing Connection..") 152 | cls.socket = WebSocketApp( 153 | cls.server, 154 | on_open=cls._on_open, 155 | on_message=cls._on_message, 156 | on_error=cls._on_error, 157 | on_close=cls._on_close 158 | ) 159 | 160 | cls.log.debug("Starting Connection..") 161 | cls.socket.run_forever() 162 | 163 | while cls.reconnect_required.is_set(): 164 | if not cls.disconnect_called.is_set(): 165 | cls.log.info("Attempting to connect again in {} seconds.".format(cls.reconnect_interval)) # noqa: E501 166 | cls.state = "unavailable" 167 | time.sleep(cls.reconnect_interval) 168 | 169 | # We need to set this flag since closing the socket will 170 | # set it to False 171 | cls.socket.keep_running = True 172 | cls.socket.run_forever() 173 | 174 | def run(cls) -> None: 175 | """ 176 | Main method of Thread. 177 | :return: 178 | """ 179 | cls.log.debug("Starting up..") 180 | cls._connect() 181 | 182 | def _on_message(cls, ws, message) -> None: 183 | """ 184 | Handles and passes received data to the appropriate handlers. 185 | :return: 186 | """ 187 | 188 | # ignore income messages if we are disconnecting 189 | if cls.disconnect_called.is_set(): 190 | return 191 | 192 | raw, received_at = message, time.time() 193 | cls.log.debug("Received new message %s at %s", raw, received_at) 194 | 195 | data: Dict[str, Any] = None 196 | 197 | try: 198 | data = json.loads(raw) 199 | except json.JSONDecodeError: 200 | cls.log.info("Unknown Response.") 201 | return 202 | 203 | if isinstance(data, dict): 204 | # This is a valid message 205 | cls._data_handler(data, received_at) 206 | 207 | # We've received data, reset timers 208 | cls._start_timers() 209 | 210 | def _on_close(cls, *args) -> None: 211 | cls.log.info("Connection closed") 212 | cls.connected.clear() 213 | cls._stop_timers() 214 | 215 | cls._callback('on_close') 216 | 217 | def _on_open(cls, ws) -> None: 218 | cls.log.info("Connection opened") 219 | cls.connected.set() 220 | cls.send_ping() 221 | cls._start_timers() 222 | if cls.reconnect_required.is_set(): 223 | cls.log.info("Connection reconnected.") 224 | 225 | cls._callback('on_open', cls) 226 | 227 | def _on_error(cls, ws, error) -> None: 228 | cls.log.info("Connection Error - %s", error) 229 | 230 | # ignore errors if we are disconnecting 231 | if cls.disconnect_called.is_set(): 232 | return 233 | 234 | cls.reconnect_required.set() 235 | cls.connected.clear() 236 | 237 | cls._callback('on_error', error) 238 | 239 | def _stop_timers(cls) -> None: 240 | """ 241 | Stops ping, pong and connection timers. 242 | :return: 243 | """ 244 | if cls.ping_timer: 245 | cls.ping_timer.cancel() 246 | 247 | if cls.connection_timer: 248 | cls.connection_timer.cancel() 249 | 250 | if cls.pong_timer: 251 | cls.pong_timer.cancel() 252 | cls.log.debug("Timers stopped.") 253 | 254 | def _start_timers(cls) -> None: 255 | """ 256 | Resets and starts timers for API data and connection. 257 | :return: 258 | """ 259 | cls._stop_timers() 260 | 261 | # Sends a ping at ping_interval to see if API still responding 262 | cls.ping_timer = Timer(cls.ping_interval, cls.send_ping) 263 | cls.ping_timer.start() 264 | 265 | # Automatically reconnect if we did not receive data 266 | cls.connection_timer = Timer( 267 | cls.connection_timeout, 268 | cls._connection_timed_out 269 | ) 270 | cls.connection_timer.start() 271 | 272 | def send_ping(cls) -> None: 273 | """ 274 | Sends a ping message to the API and starts pong timers. 275 | :return: 276 | """ 277 | cls.log.debug("Sending ping to API..") 278 | cls.socket.send(json.dumps(dict(command='ping', id='ping'))) 279 | cls.pong_timer = Timer(cls.pong_timeout, cls._check_pong) 280 | cls.pong_timer.start() 281 | 282 | def _check_pong(cls) -> None: 283 | """ 284 | Checks if a Pong message was received. 285 | :return: 286 | """ 287 | cls.pong_timer.cancel() 288 | if cls.pong_received: 289 | cls.log.debug("Pong received in time.") 290 | cls.pong_received = False 291 | else: 292 | # reconnect 293 | cls.log.debug("Pong not received in time. Issuing reconnect..") # noqa: E501 294 | cls.reconnect() 295 | 296 | def _connection_timed_out(cls) -> None: 297 | """ 298 | Issues a reconnection if the connection timed out. 299 | :return: 300 | """ 301 | cls.log.debug("Timeout, Issuing reconnect..") 302 | cls.reconnect() 303 | 304 | def _pause(cls) -> None: 305 | """ 306 | Pauses the connection. 307 | :return: 308 | """ 309 | cls.log.debug("Setting paused() Flag!") 310 | cls.paused.set() 311 | 312 | def _unpause(cls) -> None: 313 | """ 314 | Unpauses the connection. 315 | Send a message up to client that he should re-subscribe to all 316 | channels. 317 | :return: 318 | """ 319 | cls.log.debug("Clearing paused() Flag!") 320 | cls.paused.clear() 321 | 322 | def _data_handler( 323 | cls, 324 | data: Dict[str, object], 325 | timestamp: str 326 | ) -> None: 327 | """ 328 | Distributes system messages to the appropriate handler. 329 | System messages include everything that arrives as a dict, 330 | :param data: 331 | :param ts: 332 | :return: 333 | """ 334 | cls.log.debug("On Response: {}".format(data)) 335 | cls._callback('on_response', data) 336 | 337 | def _callback(cls, callback, *args) -> None: 338 | """Emit a callback in a thread 339 | :param callback: 340 | :param *args: 341 | :return: 342 | """ 343 | if callback: 344 | try: 345 | _callback = getattr(cls, callback, None) 346 | if _callback is not None and callable(_callback): 347 | t = Thread(target=_callback, args=args) 348 | t.setDaemon(True) 349 | t.start() 350 | except Exception as e: 351 | cls.log.error("error from callback {}: {}".format(_callback, e)) # noqa: E501 352 | --------------------------------------------------------------------------------