├── .github ├── actions │ └── ssh-agent │ │ └── action.yml └── workflows │ ├── build.yml │ └── update_schema_flow.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── Pipfile ├── Pipfile.lock ├── README.md ├── deriv_api ├── __init__.py ├── cache.py ├── deriv_api.py ├── deriv_api_calls.py ├── easy_future.py ├── errors.py ├── in_memory.py ├── middlewares.py ├── streams_list.py ├── subscription_manager.py └── utils.py ├── docs ├── templates │ ├── config.mako │ └── html.mako └── usage_examples.md ├── examples ├── simple_bot1.py ├── simple_bot2.py ├── simple_bot3.py └── simple_bot4.py ├── pyproject.toml ├── pytest.ini ├── scripts ├── regen-py.pl └── templates │ ├── api-call-py.tt2 │ └── streams_list.py.tt2 ├── setup.py └── tests ├── test_cache.py ├── test_custom_future.py ├── test_deriv_api.py ├── test_deriv_api_calls.py ├── test_errors.py ├── test_in_memory.py ├── test_middlewares.py ├── test_subscription_manager.py └── test_utils.py /.github/actions/ssh-agent/action.yml: -------------------------------------------------------------------------------- 1 | name: SSH agent setup 2 | description: "Sets up ssh agent, add read ssh key and write ssh key" 3 | inputs: 4 | read_github_ssh_key: 5 | description: "ssh key to read other repos" 6 | required: false 7 | write_github_ssh_key: 8 | description: "ssh key to write other repos" 9 | required: false 10 | runs: 11 | using: composite 12 | steps: 13 | - name: set env 14 | shell: bash -e {0} 15 | working-directory: /tmp 16 | run: | 17 | eval $(ssh-agent) 18 | echo SSH_AUTH_SOCK=$SSH_AUTH_SOCK | sudo tee -a $GITHUB_ENV 19 | echo SSH_AGENT_PID=$SSH_AGENT_PID | sudo tee -a $GITHUB_ENV 20 | - name: create .ssh directory 21 | shell: bash -e {0} 22 | working-directory: /tmp 23 | run: | 24 | mkdir ~/.ssh 25 | chmod 700 ~/.ssh 26 | - name: setup read ssh key file 27 | shell: bash -e {0} 28 | working-directory: /tmp 29 | env: 30 | READ_SSH_KEY: ${{ inputs.read_github_ssh_key }} 31 | if: inputs.read_github_ssh_key != '' 32 | run: | 33 | echo "$READ_SSH_KEY" >> ~/.ssh/github.com.rsa 34 | chmod 600 ~/.ssh/github.com.rsa 35 | ssh-add ~/.ssh/github.com.rsa 36 | 37 | cat << EOF >> ~/.ssh/config 38 | Host github.com 39 | HostName github.com 40 | IdentitiesOnly yes 41 | IdentityFile HOME/.ssh/github.com.rsa 42 | EOF 43 | sed -i "s@HOME@$HOME/@" ~/.ssh/config 44 | chmod 600 ~/.ssh/config 45 | - name: setup write ssh key file 46 | shell: bash -e {0} 47 | working-directory: /tmp 48 | env: 49 | WRITE_SSH_KEY: ${{ inputs.write_github_ssh_key }} 50 | if: inputs.write_github_ssh_key != '' 51 | run: | 52 | echo "$WRITE_SSH_KEY" >> ~/.ssh/write.github.com.rsa 53 | chmod 600 ~/.ssh/write.github.com.rsa 54 | ssh-add ~/.ssh/write.github.com.rsa 55 | 56 | cat << EOF >> ~/.ssh/config 57 | Host push.github.com 58 | HostName github.com 59 | IdentitiesOnly yes 60 | IdentityFile HOME/.ssh/write.github.com.rsa 61 | EOF 62 | sed -i "s@HOME@$HOME/@" ~/.ssh/config 63 | chmod 600 ~/.ssh/config 64 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | run-name: Build 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - master 8 | pull_request: 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.ref }} 11 | cancel-in-progress: true 12 | jobs: 13 | test: 14 | runs-on: ubuntu-20.04 15 | strategy: 16 | matrix: 17 | python-version: 18 | - 3.9.6 19 | - 3.9.8 20 | - 3.9.9 21 | - 3.9.10 22 | - 3.9.11 23 | - 3.9.12 24 | - 3.9.13 25 | - 3.9.16 26 | - 3.10.0 27 | - 3.10.1 28 | - 3.10.2 29 | - 3.10.3 30 | - 3.10.4 31 | - 3.10.10 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: actions/setup-python@v4 35 | with: 36 | python-version: ${{ matrix.python-version }} 37 | cache: 'pipenv' 38 | - run: make setup 39 | - run: make test 40 | - run: make coverage 41 | release: 42 | if: github.ref == 'refs/heads/master' 43 | needs: test 44 | runs-on: ubuntu-20.04 45 | env: 46 | PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: actions/setup-python@v4 50 | with: 51 | python-version: "3.9.6" 52 | - name: setup pypi 53 | run: | 54 | echo "[pypi]" >> ~/.pypirc 55 | echo "username=__token__" >> ~/.pypirc 56 | echo "password=$PYPI_TOKEN" >> ~/.pypirc 57 | - name: release 58 | run: | 59 | python3 -m pip install --upgrade setuptools wheel build twine 60 | make build 61 | python3 -m twine upload --repository pypi dist/* 62 | echo "deployed to pypi" 63 | docs-build-deploy: 64 | if: github.ref == 'refs/heads/master' 65 | needs: release 66 | runs-on: ubuntu-20.04 67 | steps: 68 | - uses: actions/checkout@v4 69 | - uses: ./.github/actions/ssh-agent 70 | with: 71 | write_github_ssh_key: ${{ secrets.WRITE_GITHUB_SSH_KEY }} 72 | - uses: actions/setup-python@v4 73 | with: 74 | python-version: "3.9.6" 75 | cache: 'pipenv' 76 | - run: make setup 77 | - run: | 78 | git config --local user.email "sysadmin@binary.com" 79 | git config --local user.name "gh-pages deploy bot" 80 | make gh-pages 81 | -------------------------------------------------------------------------------- /.github/workflows/update_schema_flow.yml: -------------------------------------------------------------------------------- 1 | name: Update schema flow 2 | run-name: Update schema flow 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | jobs: 8 | update_schema: 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: ./.github/actions/ssh-agent 13 | with: 14 | write_github_ssh_key: ${{ secrets.WRITE_GITHUB_SSH_KEY }} 15 | - uses: actions/setup-python@v4 16 | with: 17 | python-version: "3.9.6" 18 | - name: config git 19 | run: | 20 | git config --global user.email "nobody@deriv.com" 21 | git config --global user.name "Nobody" 22 | - name: update schema 23 | run: | 24 | git clone https://github.com/binary-com/deriv-developers-portal.git /tmp/deriv-developers-portal 25 | curl -L https://cpanmin.us | perl - --sudo App::cpanminus 26 | sudo cpanm -n Dir::Self File::Basename JSON::MaybeXS Log::Any Path::Tiny Template Syntax::Keyword::Try 27 | BINARYCOM_API_SCHEMA_PATH=/tmp/deriv-developers-portal/config/v3 perl scripts/regen-py.pl 28 | if [[ $(git diff --shortstat) == ' 2 files changed, 2 insertions(+), 2 deletions(-)' ]] 29 | then 30 | echo 'Schema no change' 31 | exit 0 32 | fi 33 | echo "Schama updated" 34 | pip3 install bump 35 | NEXT_VER=$(bump) 36 | sed -i '/# Changelog/{s/$/\n\n## NEXTVER\n\nSync API/}' CHANGELOG.md 37 | sed -i "s/NEXTVER/$NEXT_VER/g" CHANGELOG.md 38 | git add . 39 | git commit -m 'update schema automatically' 40 | git push origin HEAD:master 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache_/ 2 | *.py[cod] 3 | .pytest_cache/ 4 | .eggs/ 5 | venv/ 6 | *.egg-info/ 7 | .idea 8 | dist 9 | .coverage 10 | build 11 | tags 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.1.7 4 | 5 | Sync API 6 | 7 | ## 0.1.6 8 | 9 | Sync API 10 | 11 | ## 0.1.5 12 | 13 | Change default ws server 14 | 15 | ## 0.1.4 16 | 17 | Sync API 18 | 19 | ## 0.1.3 20 | 21 | Fix a typo, which cause ws connection no response 22 | 23 | ## 0.1.2 24 | 25 | Added middleware support 26 | 27 | ## 0.1.1 28 | 29 | ### Fixed: 30 | 31 | Fixed a PyPI constraint where the package can only be installed on python ==3.9.6 32 | 33 | ## 0.1.0 34 | 35 | Initial version. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Deriv Group Services Ltd 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. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all setup test doc gh-pages build 2 | all: setup test 3 | setup: 4 | pip3 install pipenv && pipenv install --dev 5 | test: 6 | pipenv run pytest 7 | doc: 8 | pipenv run pdoc deriv_api --force --html -o docs/html --template-dir docs/templates 9 | build: 10 | pip3 install --upgrade setuptools wheel build && python3 -m build 11 | coverage: 12 | pipenv run coverage run --source deriv_api -m pytest && pipenv run coverage report -m 13 | gh-pages: 14 | pipenv run pdoc deriv_api --force --html -o /tmp/python-deriv-api-docs --template-dir docs/templates && git add -A . && git stash && git checkout gh-pages && cp -r /tmp/python-deriv-api-docs/deriv_api/* . && git add -A . && git commit -m 'Update docs' && git push origin gh-pages && git checkout - 15 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [packages] 2 | websockets = "==10.3" 3 | reactivex = "==4.0.*" 4 | deriv-api = {editable = true, path = "."} 5 | mako = ">=1.3.6" 6 | 7 | [dev-packages] 8 | pytest = "*" 9 | pytest-runner = "*" 10 | pytest-mock = "*" 11 | pytest-asyncio = "*" 12 | pdoc3 = "*" 13 | coverage = "===4.5.4" 14 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "8bfd793999eca2fad61949b5b3077645e6735a5cd35253cd58c13b2fc59daacb" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": {}, 8 | "sources": [ 9 | { 10 | "name": "pypi", 11 | "url": "https://pypi.org/simple", 12 | "verify_ssl": true 13 | } 14 | ] 15 | }, 16 | "default": { 17 | "deriv-api": { 18 | "editable": true, 19 | "path": "." 20 | }, 21 | "mako": { 22 | "hashes": [ 23 | "sha256:9ec3a1583713479fae654f83ed9fa8c9a4c16b7bb0daba0e6bbebff50c0d983d", 24 | "sha256:a91198468092a2f1a0de86ca92690fb0cfc43ca90ee17e15d93662b4c04b241a" 25 | ], 26 | "index": "pypi", 27 | "markers": "python_version >= '3.8'", 28 | "version": "==1.3.6" 29 | }, 30 | "markupsafe": { 31 | "hashes": [ 32 | "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", 33 | "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", 34 | "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0", 35 | "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", 36 | "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", 37 | "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13", 38 | "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", 39 | "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", 40 | "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", 41 | "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", 42 | "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0", 43 | "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", 44 | "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", 45 | "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", 46 | "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", 47 | "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff", 48 | "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", 49 | "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", 50 | "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", 51 | "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", 52 | "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", 53 | "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", 54 | "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", 55 | "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", 56 | "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a", 57 | "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", 58 | "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", 59 | "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", 60 | "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", 61 | "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144", 62 | "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f", 63 | "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", 64 | "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", 65 | "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", 66 | "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", 67 | "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", 68 | "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", 69 | "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", 70 | "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", 71 | "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", 72 | "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", 73 | "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", 74 | "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", 75 | "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", 76 | "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", 77 | "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", 78 | "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", 79 | "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", 80 | "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29", 81 | "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", 82 | "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", 83 | "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", 84 | "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", 85 | "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", 86 | "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", 87 | "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a", 88 | "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178", 89 | "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", 90 | "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", 91 | "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", 92 | "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50" 93 | ], 94 | "markers": "python_version >= '3.9'", 95 | "version": "==3.0.2" 96 | }, 97 | "python-deriv-api": { 98 | "editable": true, 99 | "path": "." 100 | }, 101 | "reactivex": { 102 | "hashes": [ 103 | "sha256:04d17b55652caf8b6c911f7588b4fe8fa69b6fc48e312e0e3462597fb93bb588", 104 | "sha256:e4db0f7b1646c2198fb7cceade05be7d2e1bd8c0284ae5dffdae449055400310" 105 | ], 106 | "index": "pypi", 107 | "version": "==4.0.2" 108 | }, 109 | "typing-extensions": { 110 | "hashes": [ 111 | "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708", 112 | "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376" 113 | ], 114 | "markers": "python_version >= '3.7'", 115 | "version": "==4.2.0" 116 | }, 117 | "websockets": { 118 | "hashes": [ 119 | "sha256:07cdc0a5b2549bcfbadb585ad8471ebdc7bdf91e32e34ae3889001c1c106a6af", 120 | "sha256:210aad7fdd381c52e58777560860c7e6110b6174488ef1d4b681c08b68bf7f8c", 121 | "sha256:28dd20b938a57c3124028680dc1600c197294da5db4292c76a0b48efb3ed7f76", 122 | "sha256:2f94fa3ae454a63ea3a19f73b95deeebc9f02ba2d5617ca16f0bbdae375cda47", 123 | "sha256:31564a67c3e4005f27815634343df688b25705cccb22bc1db621c781ddc64c69", 124 | "sha256:347974105bbd4ea068106ec65e8e8ebd86f28c19e529d115d89bd8cc5cda3079", 125 | "sha256:379e03422178436af4f3abe0aa8f401aa77ae2487843738542a75faf44a31f0c", 126 | "sha256:3eda1cb7e9da1b22588cefff09f0951771d6ee9fa8dbe66f5ae04cc5f26b2b55", 127 | "sha256:51695d3b199cd03098ae5b42833006a0f43dc5418d3102972addc593a783bc02", 128 | "sha256:54c000abeaff6d8771a4e2cef40900919908ea7b6b6a30eae72752607c6db559", 129 | "sha256:5b936bf552e4f6357f5727579072ff1e1324717902127ffe60c92d29b67b7be3", 130 | "sha256:6075fd24df23133c1b078e08a9b04a3bc40b31a8def4ee0b9f2c8865acce913e", 131 | "sha256:661f641b44ed315556a2fa630239adfd77bd1b11cb0b9d96ed8ad90b0b1e4978", 132 | "sha256:6ea6b300a6bdd782e49922d690e11c3669828fe36fc2471408c58b93b5535a98", 133 | "sha256:6ed1d6f791eabfd9808afea1e068f5e59418e55721db8b7f3bfc39dc831c42ae", 134 | "sha256:7934e055fd5cd9dee60f11d16c8d79c4567315824bacb1246d0208a47eca9755", 135 | "sha256:7ab36e17af592eec5747c68ef2722a74c1a4a70f3772bc661079baf4ae30e40d", 136 | "sha256:7f6d96fdb0975044fdd7953b35d003b03f9e2bcf85f2d2cf86285ece53e9f991", 137 | "sha256:83e5ca0d5b743cde3d29fda74ccab37bdd0911f25bd4cdf09ff8b51b7b4f2fa1", 138 | "sha256:85506b3328a9e083cc0a0fb3ba27e33c8db78341b3eb12eb72e8afd166c36680", 139 | "sha256:8af75085b4bc0b5c40c4a3c0e113fa95e84c60f4ed6786cbb675aeb1ee128247", 140 | "sha256:8b1359aba0ff810d5830d5ab8e2c4a02bebf98a60aa0124fb29aa78cfdb8031f", 141 | "sha256:8fbd7d77f8aba46d43245e86dd91a8970eac4fb74c473f8e30e9c07581f852b2", 142 | "sha256:907e8247480f287aa9bbc9391bd6de23c906d48af54c8c421df84655eef66af7", 143 | "sha256:93d5ea0b5da8d66d868b32c614d2b52d14304444e39e13a59566d4acb8d6e2e4", 144 | "sha256:97bc9d41e69a7521a358f9b8e44871f6cdeb42af31815c17aed36372d4eec667", 145 | "sha256:994cdb1942a7a4c2e10098d9162948c9e7b235df755de91ca33f6e0481366fdb", 146 | "sha256:a141de3d5a92188234afa61653ed0bbd2dde46ad47b15c3042ffb89548e77094", 147 | "sha256:a1e15b230c3613e8ea82c9fc6941b2093e8eb939dd794c02754d33980ba81e36", 148 | "sha256:aad5e300ab32036eb3fdc350ad30877210e2f51bceaca83fb7fef4d2b6c72b79", 149 | "sha256:b529fdfa881b69fe563dbd98acce84f3e5a67df13de415e143ef053ff006d500", 150 | "sha256:b9c77f0d1436ea4b4dc089ed8335fa141e6a251a92f75f675056dac4ab47a71e", 151 | "sha256:bb621ec2dbbbe8df78a27dbd9dd7919f9b7d32a73fafcb4d9252fc4637343582", 152 | "sha256:c7250848ce69559756ad0086a37b82c986cd33c2d344ab87fea596c5ac6d9442", 153 | "sha256:c8d1d14aa0f600b5be363077b621b1b4d1eb3fbf90af83f9281cda668e6ff7fd", 154 | "sha256:d1655a6fc7aecd333b079d00fb3c8132d18988e47f19740c69303bf02e9883c6", 155 | "sha256:d6353ba89cfc657a3f5beabb3b69be226adbb5c6c7a66398e17809b0ce3c4731", 156 | "sha256:da4377904a3379f0c1b75a965fff23b28315bcd516d27f99a803720dfebd94d4", 157 | "sha256:e49ea4c1a9543d2bd8a747ff24411509c29e4bdcde05b5b0895e2120cb1a761d", 158 | "sha256:e4e08305bfd76ba8edab08dcc6496f40674f44eb9d5e23153efa0a35750337e8", 159 | "sha256:e6fa05a680e35d0fcc1470cb070b10e6fe247af54768f488ed93542e71339d6f", 160 | "sha256:e7e6f2d6fd48422071cc8a6f8542016f350b79cc782752de531577d35e9bd677", 161 | "sha256:e904c0381c014b914136c492c8fa711ca4cced4e9b3d110e5e7d436d0fc289e8", 162 | "sha256:ec2b0ab7edc8cd4b0eb428b38ed89079bdc20c6bdb5f889d353011038caac2f9", 163 | "sha256:ef5ce841e102278c1c2e98f043db99d6755b1c58bde475516aef3a008ed7f28e", 164 | "sha256:f351c7d7d92f67c0609329ab2735eee0426a03022771b00102816a72715bb00b", 165 | "sha256:fab7c640815812ed5f10fbee7abbf58788d602046b7bb3af9b1ac753a6d5e916", 166 | "sha256:fc06cc8073c8e87072138ba1e431300e2d408f054b27047d047b549455066ff4" 167 | ], 168 | "index": "pypi", 169 | "version": "==10.3" 170 | } 171 | }, 172 | "develop": { 173 | "attrs": { 174 | "hashes": [ 175 | "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", 176 | "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" 177 | ], 178 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 179 | "version": "==21.4.0" 180 | }, 181 | "coverage": { 182 | "hashes": [ 183 | "sha256:08907593569fe59baca0bf152c43f3863201efb6113ecb38ce7e97ce339805a6", 184 | "sha256:0be0f1ed45fc0c185cfd4ecc19a1d6532d72f86a2bac9de7e24541febad72650", 185 | "sha256:141f08ed3c4b1847015e2cd62ec06d35e67a3ac185c26f7635f4406b90afa9c5", 186 | "sha256:19e4df788a0581238e9390c85a7a09af39c7b539b29f25c89209e6c3e371270d", 187 | "sha256:23cc09ed395b03424d1ae30dcc292615c1372bfba7141eb85e11e50efaa6b351", 188 | "sha256:245388cda02af78276b479f299bbf3783ef0a6a6273037d7c60dc73b8d8d7755", 189 | "sha256:331cb5115673a20fb131dadd22f5bcaf7677ef758741312bee4937d71a14b2ef", 190 | "sha256:386e2e4090f0bc5df274e720105c342263423e77ee8826002dcffe0c9533dbca", 191 | "sha256:3a794ce50daee01c74a494919d5ebdc23d58873747fa0e288318728533a3e1ca", 192 | "sha256:60851187677b24c6085248f0a0b9b98d49cba7ecc7ec60ba6b9d2e5574ac1ee9", 193 | "sha256:63a9a5fc43b58735f65ed63d2cf43508f462dc49857da70b8980ad78d41d52fc", 194 | "sha256:6b62544bb68106e3f00b21c8930e83e584fdca005d4fffd29bb39fb3ffa03cb5", 195 | "sha256:6ba744056423ef8d450cf627289166da65903885272055fb4b5e113137cfa14f", 196 | "sha256:7494b0b0274c5072bddbfd5b4a6c6f18fbbe1ab1d22a41e99cd2d00c8f96ecfe", 197 | "sha256:826f32b9547c8091679ff292a82aca9c7b9650f9fda3e2ca6bf2ac905b7ce888", 198 | "sha256:93715dffbcd0678057f947f496484e906bf9509f5c1c38fc9ba3922893cda5f5", 199 | "sha256:9a334d6c83dfeadae576b4d633a71620d40d1c379129d587faa42ee3e2a85cce", 200 | "sha256:af7ed8a8aa6957aac47b4268631fa1df984643f07ef00acd374e456364b373f5", 201 | "sha256:bf0a7aed7f5521c7ca67febd57db473af4762b9622254291fbcbb8cd0ba5e33e", 202 | "sha256:bf1ef9eb901113a9805287e090452c05547578eaab1b62e4ad456fcc049a9b7e", 203 | "sha256:c0afd27bc0e307a1ffc04ca5ec010a290e49e3afbe841c5cafc5c5a80ecd81c9", 204 | "sha256:dd579709a87092c6dbee09d1b7cfa81831040705ffa12a1b248935274aee0437", 205 | "sha256:df6712284b2e44a065097846488f66840445eb987eb81b3cc6e4149e7b6982e1", 206 | "sha256:e07d9f1a23e9e93ab5c62902833bf3e4b1f65502927379148b6622686223125c", 207 | "sha256:e2ede7c1d45e65e209d6093b762e98e8318ddeff95317d07a27a2140b80cfd24", 208 | "sha256:e4ef9c164eb55123c62411f5936b5c2e521b12356037b6e1c2617cef45523d47", 209 | "sha256:eca2b7343524e7ba246cab8ff00cab47a2d6d54ada3b02772e908a45675722e2", 210 | "sha256:eee64c616adeff7db37cc37da4180a3a5b6177f5c46b187894e633f088fb5b28", 211 | "sha256:ef824cad1f980d27f26166f86856efe11eff9912c4fed97d3804820d43fa550c", 212 | "sha256:efc89291bd5a08855829a3c522df16d856455297cf35ae827a37edac45f466a7", 213 | "sha256:fa964bae817babece5aa2e8c1af841bebb6d0b9add8e637548809d040443fee0", 214 | "sha256:ff37757e068ae606659c28c3bd0d923f9d29a85de79bf25b2b34b148473b5025" 215 | ], 216 | "index": "pypi", 217 | "version": "===4.5.4" 218 | }, 219 | "importlib-metadata": { 220 | "hashes": [ 221 | "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670", 222 | "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23" 223 | ], 224 | "markers": "python_version < '3.10'", 225 | "version": "==4.12.0" 226 | }, 227 | "iniconfig": { 228 | "hashes": [ 229 | "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3", 230 | "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32" 231 | ], 232 | "version": "==1.1.1" 233 | }, 234 | "mako": { 235 | "hashes": [ 236 | "sha256:3724869b363ba630a272a5f89f68c070352137b8fd1757650017b7e06fda163f", 237 | "sha256:8efcb8004681b5f71d09c983ad5a9e6f5c40601a6ec469148753292abc0da534" 238 | ], 239 | "index": "pypi", 240 | "markers": "python_version >= '3.7'", 241 | "version": "==1.2.2" 242 | }, 243 | "markdown": { 244 | "hashes": [ 245 | "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874", 246 | "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621" 247 | ], 248 | "markers": "python_version >= '3.6'", 249 | "version": "==3.3.7" 250 | }, 251 | "markupsafe": { 252 | "hashes": [ 253 | "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003", 254 | "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88", 255 | "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5", 256 | "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7", 257 | "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a", 258 | "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603", 259 | "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1", 260 | "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135", 261 | "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247", 262 | "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6", 263 | "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601", 264 | "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77", 265 | "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02", 266 | "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e", 267 | "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63", 268 | "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f", 269 | "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980", 270 | "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b", 271 | "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812", 272 | "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff", 273 | "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96", 274 | "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1", 275 | "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925", 276 | "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a", 277 | "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6", 278 | "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e", 279 | "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f", 280 | "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4", 281 | "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f", 282 | "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3", 283 | "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c", 284 | "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a", 285 | "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417", 286 | "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a", 287 | "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a", 288 | "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37", 289 | "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452", 290 | "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933", 291 | "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a", 292 | "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7" 293 | ], 294 | "markers": "python_version >= '3.7'", 295 | "version": "==2.1.1" 296 | }, 297 | "packaging": { 298 | "hashes": [ 299 | "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb", 300 | "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522" 301 | ], 302 | "markers": "python_version >= '3.6'", 303 | "version": "==21.3" 304 | }, 305 | "pdoc3": { 306 | "hashes": [ 307 | "sha256:5f22e7bcb969006738e1aa4219c75a32f34c2d62d46dc9d2fb2d3e0b0287e4b7", 308 | "sha256:ba45d1ada1bd987427d2bf5cdec30b2631a3ff5fb01f6d0e77648a572ce6028b" 309 | ], 310 | "index": "pypi", 311 | "version": "==0.10.0" 312 | }, 313 | "pluggy": { 314 | "hashes": [ 315 | "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159", 316 | "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3" 317 | ], 318 | "markers": "python_version >= '3.6'", 319 | "version": "==1.0.0" 320 | }, 321 | "py": { 322 | "hashes": [ 323 | "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", 324 | "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" 325 | ], 326 | "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", 327 | "version": "==1.11.0" 328 | }, 329 | "pyparsing": { 330 | "hashes": [ 331 | "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb", 332 | "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc" 333 | ], 334 | "markers": "python_full_version >= '3.6.8'", 335 | "version": "==3.0.9" 336 | }, 337 | "pytest": { 338 | "hashes": [ 339 | "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c", 340 | "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45" 341 | ], 342 | "index": "pypi", 343 | "version": "==7.1.2" 344 | }, 345 | "pytest-asyncio": { 346 | "hashes": [ 347 | "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213", 348 | "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91", 349 | "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84" 350 | ], 351 | "index": "pypi", 352 | "version": "==0.18.3" 353 | }, 354 | "pytest-mock": { 355 | "hashes": [ 356 | "sha256:2c6d756d5d3bf98e2e80797a959ca7f81f479e7d1f5f571611b0fdd6d1745240", 357 | "sha256:d989f11ca4a84479e288b0cd1e6769d6ad0d3d7743dcc75e460d1416a5f2135a" 358 | ], 359 | "index": "pypi", 360 | "version": "==3.8.1" 361 | }, 362 | "pytest-runner": { 363 | "hashes": [ 364 | "sha256:4c059cf11cf4306e369c0f8f703d1eaf8f32fad370f41deb5f007044656aca6b", 365 | "sha256:b4d85362ed29b4c348678de797df438f0f0509497ddb8c647096c02a6d87b685" 366 | ], 367 | "index": "pypi", 368 | "version": "==6.0.0" 369 | }, 370 | "tomli": { 371 | "hashes": [ 372 | "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", 373 | "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f" 374 | ], 375 | "markers": "python_version >= '3.7'", 376 | "version": "==2.0.1" 377 | }, 378 | "zipp": { 379 | "hashes": [ 380 | "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad", 381 | "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099" 382 | ], 383 | "markers": "python_version >= '3.7'", 384 | "version": "==3.8.0" 385 | } 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-deriv-api 2 | A python implementation of deriv api library. 3 | 4 | [![PyPI](https://img.shields.io/pypi/v/python_deriv_api.svg?style=flat-square)](https://pypi.org/project/python_deriv_api/) 5 | [![Python 3.9.6](https://img.shields.io/badge/python-3.9.6-blue.svg)](https://www.python.org/download/releases/3.9.6/) 6 | [![Test status](https://github.com/deriv-com/python-deriv-api/actions/workflows/build.yml/badge.svg)](https://github.com/deriv-com/python-deriv-api) 7 | 8 | Go through [api.deriv.com](https://api.deriv.com/) to know simple easy steps on how to register and get access. 9 | Use this all-in-one python library to set up and make your app running or you can extend it. 10 | 11 | ### Requirement 12 | Python (3.9.6 or higher is recommended) and pip3 13 | 14 | Note: There is bug in 'websockets' package with python 3.9.7, hope that will be fixed in 3.9.8 as mentioned in 15 | https://github.com/aaugustin/websockets/issues/1051. Please exclude python 3.9.7. 16 | 17 | # Installation 18 | 19 | `python3 -m pip install python_deriv_api` 20 | 21 | # Usage 22 | This is basic deriv-api python library which helps to make websockets connection and 23 | deal the API calls (including subscription). 24 | 25 | Import the module 26 | 27 | ``` 28 | from deriv_api import DerivAPI 29 | ``` 30 | 31 | Access 32 | 33 | ``` 34 | api = DerivAPI(endpoint='ws://...', app_id=1234); 35 | response = await api.ping({'ping': 1}) 36 | print(response) 37 | ``` 38 | 39 | ## Creating a websockets connection and API instantiation 40 | You can either create an instance of websockets and pass it as connection 41 | or 42 | pass the endpoint and app_id to the constructor to create the connection for you. 43 | 44 | If you pass the connection it's up to you to reconnect in case the connection drops (cause API doesn't know how to create the same connection). 45 | 46 | 47 | - Pass the arguments needed to create a connection: 48 | ``` 49 | api = DerivAPI(endpoint='ws://...', app_id=1234); 50 | ``` 51 | 52 | - create and use a previously opened connection: 53 | ``` 54 | connection = await websockets.connect('ws://...') 55 | api = DerivAPI(connection=connection) 56 | ``` 57 | 58 | # Documentation 59 | 60 | #### API reference 61 | The complete API reference is hosted [here](https://deriv-com.github.io/python-deriv-api/) 62 | 63 | Examples [here](https://github.com/deriv-com/python-deriv-api/tree/master/examples) 64 | 65 | # Development 66 | ``` 67 | git clone https://github.com/deriv-com/python-deriv-api 68 | cd python-deriv-api 69 | ``` 70 | Setup environment 71 | ``` 72 | make setup 73 | ``` 74 | 75 | Setup environment and run test 76 | ``` 77 | make all 78 | ``` 79 | 80 | #### Run test 81 | 82 | ``` 83 | python setup.py pytest 84 | ``` 85 | 86 | or 87 | 88 | ``` 89 | pytest 90 | ``` 91 | 92 | or 93 | 94 | ``` 95 | make test 96 | ``` 97 | #### Generate documentations 98 | 99 | Generate html version of the docs and publish it to gh-pages 100 | 101 | ``` 102 | make gh-pages 103 | ``` 104 | 105 | #### Build the package 106 | ``` 107 | make build 108 | ``` 109 | #### Run examples 110 | 111 | set token and run example 112 | 113 | ``` 114 | export DERIV_TOKEN=xxxTokenxxx 115 | PYTHONPATH=. python3 examples/simple_bot1.py 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /deriv_api/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | .. include:: ../README.md 3 | .. include:: ../docs/usage_examples.md 4 | """ 5 | 6 | __pdoc__ = { 7 | 'deriv_api.errors': False, 8 | 'deriv_api.utils': False, 9 | 'deriv_api.easy_future': False 10 | } 11 | 12 | from .deriv_api import DerivAPI 13 | from .errors import AddedTaskError, APIError, ConstructionError, ResponseError 14 | 15 | -------------------------------------------------------------------------------- /deriv_api/cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | from typing import TYPE_CHECKING, Union 3 | 4 | if TYPE_CHECKING: 5 | from deriv_api import DerivAPI 6 | from deriv_api.deriv_api_calls import DerivAPICalls 7 | from deriv_api.errors import ConstructionError 8 | from deriv_api.utils import dict_to_cache_key 9 | from deriv_api.in_memory import InMemory 10 | 11 | __pdoc__ = { 12 | 'deriv_api.cache.Cache.get': False, 13 | 'deriv_api.cache.Cache.get_by_msg_type': False, 14 | 'deriv_api.cache.Cache.has': False, 15 | 'deriv_api.cache.Cache.send': False, 16 | 'deriv_api.cache.Cache.set': False 17 | } 18 | 19 | 20 | class Cache(DerivAPICalls): 21 | """ 22 | Cache - A class for implementing in-memory and persistent cache 23 | 24 | The real implementation of the underlying cache is delegated to the storage 25 | object (See the params). 26 | 27 | The storage object needs to implement the API. 28 | 29 | Examples 30 | -------- 31 | - Read the latest active symbols 32 | >>> symbols = await api.active_symbols() 33 | 34 | - Read the data from cache if available 35 | >>> cached_symbols = await api.cache.active_symbols() 36 | 37 | Parameters 38 | ---------- 39 | api : deriv_api.DerivAPI 40 | API instance to get data that is not cached 41 | storage : Object 42 | A storage instance to use for caching 43 | """ 44 | 45 | def __init__(self, api: Union[DerivAPI, Cache], storage: Union[InMemory, Cache]) -> None: 46 | if not api: 47 | raise ConstructionError('Cache object needs an API to work') 48 | 49 | super().__init__() 50 | self.api = api 51 | self.storage = storage 52 | 53 | async def send(self, request: dict) -> dict: 54 | """Check if there is a cache for the request. If so then return that value. 55 | Otherwise send the request by the api 56 | 57 | Parameters 58 | ---------- 59 | request : dict 60 | API request 61 | 62 | Returns 63 | ------- 64 | API Response 65 | """ 66 | if await self.has(request): 67 | return await self.get(request) 68 | 69 | response = await self.api.send(request) 70 | self.set(request, response) 71 | return response 72 | 73 | async def has(self, request: dict) -> bool: 74 | """Redirected to the method defined by the storage 75 | 76 | Parameters 77 | ---------- 78 | request : dict 79 | API request 80 | 81 | Returns 82 | ------- 83 | Returns true if the request exists 84 | """ 85 | return self.storage.has(dict_to_cache_key(request)) 86 | 87 | async def get(self, request: dict) -> dict: 88 | """Redirected to the method defined by the storage 89 | 90 | Parameters 91 | ---------- 92 | request : dict 93 | API request 94 | 95 | Returns 96 | ------- 97 | API response stored in 98 | """ 99 | return self.storage.get(dict_to_cache_key(request)) 100 | 101 | async def get_by_msg_type(self, msg_type: str) -> dict: 102 | """Redirected to the method defined by the storage 103 | 104 | Parameters 105 | ---------- 106 | msg_type : str 107 | Request msg_type 108 | 109 | Returns 110 | ------- 111 | Returns response stored in 112 | """ 113 | return self.storage.get_by_msg_type(msg_type) 114 | 115 | def set(self, request: dict, response: dict) -> None: 116 | """Redirected to the method defined by the storage 117 | 118 | Parameters 119 | ---------- 120 | request : dict 121 | API request 122 | response : dict 123 | API response 124 | """ 125 | return self.storage.set(dict_to_cache_key(request), response) 126 | -------------------------------------------------------------------------------- /deriv_api/deriv_api.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import json 3 | import logging 4 | import re 5 | from asyncio import Future 6 | from typing import Dict, Optional, Union, Coroutine 7 | 8 | import websockets 9 | from reactivex import operators as op 10 | from reactivex.subject import Subject 11 | from reactivex import Observable 12 | from websockets.legacy.client import WebSocketClientProtocol 13 | from websockets.exceptions import ConnectionClosedOK, ConnectionClosed 14 | from websockets.frames import Close 15 | 16 | from deriv_api.cache import Cache 17 | from deriv_api.easy_future import EasyFuture 18 | from deriv_api.deriv_api_calls import DerivAPICalls 19 | from deriv_api.errors import APIError, ConstructionError, ResponseError, AddedTaskError 20 | from deriv_api.in_memory import InMemory 21 | from deriv_api.subscription_manager import SubscriptionManager 22 | from deriv_api.utils import is_valid_url 23 | from deriv_api.middlewares import MiddleWares 24 | 25 | # TODO NEXT subscribe is not calling deriv_api_calls. that's , args not verified. can we improve it ? 26 | 27 | logging.basicConfig( 28 | format="%(asctime)s %(message)s", 29 | level=logging.ERROR 30 | ) 31 | 32 | __pdoc__ = { 33 | 'deriv_api.deriv_api.DerivAPI.send_and_get_source': False, 34 | 'deriv_api.deriv_api.DerivAPI.api_connect': False, 35 | 'deriv_api.deriv_api.DerivAPI.get_url': False, 36 | 'deriv_api.deriv_api.DerivAPI.add_task': False, 37 | 'deriv_api.deriv_api.DerivAPI.delete_from_expect_response': False, 38 | 'deriv_api.deriv_api.DerivAPI.disconnect': False, 39 | 'deriv_api.deriv_api.DerivAPI.send': False, 40 | 'deriv_api.deriv_api.DerivAPI.wsconnection': False, 41 | 'deriv_api.deriv_api.DerivAPI.storage': False, 42 | } 43 | 44 | 45 | class DerivAPI(DerivAPICalls): 46 | """ 47 | The minimum functionality provided by DerivAPI, provides direct calls to the API. 48 | `api.cache` is available if you want to use the cached data 49 | 50 | Examples 51 | -------- 52 | - Pass the arguments needed to create a connection: 53 | >>> api = DerivAPI(endpoint='ws://...', app_id=1234) 54 | 55 | - create and use a previously opened connection: 56 | >>> connection = await websockets.connect('ws://...') 57 | >>> api = DerivAPI(connection=connection) 58 | 59 | Parameters 60 | ---------- 61 | options : dict with following keys 62 | connection : websockets.WebSocketClientProtocol 63 | A ready to use connection 64 | endpoint : String 65 | API server to connect to 66 | app_id : String 67 | Application ID of the API user 68 | lang : String 69 | Language of the API communication 70 | brand : String 71 | Brand name 72 | middleware : MiddleWares 73 | middlewares to call on certain API actions. Now two middlewares are supported: sendWillBeCalled and 74 | sendIsCalled 75 | Properties 76 | ---------- 77 | cache: Cache 78 | Temporary cache default to InMemory 79 | storage : Cache 80 | If specified, uses a more persistent cache (local storage, etc.) 81 | events: Observable 82 | An Observable object that will send data when events like 'connect', 'send', 'message' happen 83 | """ 84 | 85 | storage: None 86 | 87 | def __init__(self, **options: str) -> None: 88 | endpoint = options.get('endpoint', 'ws.derivws.com') 89 | lang = options.get('lang', 'EN') 90 | brand = options.get('brand', '') 91 | cache = options.get('cache', InMemory()) 92 | storage: any = options.get('storage') 93 | self.middlewares: MiddleWares = options.get('middlewares', MiddleWares()) 94 | self.wsconnection: Optional[WebSocketClientProtocol] = None 95 | self.wsconnection_from_inside = True 96 | self.shouldReconnect = False 97 | self.events: Subject = Subject() 98 | if options.get('connection'): 99 | self.wsconnection: Optional[WebSocketClientProtocol] = options.get('connection') 100 | self.wsconnection_from_inside = False 101 | else: 102 | if not options.get('app_id'): 103 | raise ConstructionError('An app_id is required to connect to the API') 104 | 105 | connection_argument = { 106 | 'app_id': str(options.get('app_id')), 107 | 'endpoint_url': self.get_url(endpoint), 108 | 'lang': lang, 109 | 'brand': brand 110 | } 111 | self.__set_api_url(connection_argument) 112 | self.shouldReconnect = True 113 | 114 | self.storage: Optional[Cache] = None 115 | if storage: 116 | self.storage = Cache(self, storage) 117 | # If we have the storage look that one up 118 | self.cache = Cache(self.storage if self.storage else self, cache) 119 | 120 | self.req_id = 0 121 | self.pending_requests: Dict[str, Subject] = {} 122 | # resolved: connected rejected: disconnected pending: not connected yet 123 | self.connected = EasyFuture() 124 | self.subscription_manager: SubscriptionManager = SubscriptionManager(self) 125 | self.sanity_errors: Subject = Subject() 126 | self.expect_response_types = {} 127 | self.wait_data_task = EasyFuture().set_result(1) 128 | self.add_task(self.api_connect(), 'api_connect') 129 | self.add_task(self.__wait_data(), 'wait_data') 130 | 131 | async def __wait_data(self): 132 | await self.connected 133 | while self.connected.is_resolved(): 134 | try: 135 | data = await self.wsconnection.recv() 136 | except ConnectionClosed as err: 137 | if self.connected.is_resolved(): 138 | self.connected = EasyFuture().reject(err) 139 | self.connected.exception() # call it to hide the warning of 'exception never retrieved' 140 | self.sanity_errors.on_next(err) 141 | break 142 | except Exception as err: 143 | self.sanity_errors.on_next(err) 144 | continue 145 | response = json.loads(data) 146 | 147 | self.events.on_next({'name': 'message', 'data': response}) 148 | # TODO NEXT onopen onclose, can be set by await connection 149 | req_id = response.get('req_id', None) 150 | if not req_id or req_id not in self.pending_requests: 151 | self.sanity_errors.on_next(APIError("Extra response")) 152 | continue 153 | expect_response: Future = self.expect_response_types.get(response['msg_type']) 154 | if expect_response and not expect_response.done(): 155 | expect_response.set_result(response) 156 | request = response['echo_req'] 157 | 158 | # When one of the child subscriptions of `proposal_open_contract` has an error in the response, 159 | # it should be handled in the callback of consumer instead. Calling `error()` with parent subscription 160 | # will mark the parent subscription as complete and all child subscriptions will be forgotten. 161 | 162 | is_parent_subscription = request and request.get('proposal_open_contract') and not request.get( 163 | 'contract_id') 164 | if response.get('error') and not is_parent_subscription: 165 | self.pending_requests[req_id].on_error(ResponseError(response)) 166 | continue 167 | 168 | # on_error will stop a subject object 169 | if self.pending_requests[req_id].is_stopped and response.get('subscription'): 170 | # Source is already marked as completed. In this case we should 171 | # send a forget request with the subscription id and ignore the response received. 172 | subs_id = response['subscription']['id'] 173 | self.add_task(self.forget(subs_id), 'forget subscription') 174 | continue 175 | 176 | self.pending_requests[req_id].on_next(response) 177 | 178 | def __set_api_url(self, connection_argument: dict) -> None: 179 | """ 180 | Construct the websocket request url 181 | 182 | Parameters 183 | ---------- 184 | connection_argument : dict 185 | 186 | """ 187 | self.api_url = connection_argument.get('endpoint_url') + "/websockets/v3?app_id=" + connection_argument.get( 188 | 'app_id') + "&l=" + connection_argument.get('lang') + "&brand=" + connection_argument.get('brand') 189 | 190 | def __get_api_url(self) -> str: 191 | """ 192 | Returns the api request url 193 | 194 | Returns 195 | ------- 196 | websocket api request url 197 | """ 198 | return self.api_url 199 | 200 | def get_url(self, original_endpoint: str) -> Union[str, ConstructionError]: 201 | """ 202 | Validate and return the url 203 | 204 | Parameters 205 | ---------- 206 | original_endpoint : str 207 | endpoint argument passed to constructor 208 | 209 | Returns 210 | ------- 211 | Returns api url. If validation fails then throws constructionError 212 | 213 | """ 214 | if not isinstance(original_endpoint, str): 215 | raise ConstructionError(f"Endpoint must be a string, passed: {type(original_endpoint)}") 216 | 217 | match = re.match(r'((?:\w*://)*)(.*)', original_endpoint).groups() 218 | protocol = match[0] if match[0] == "ws://" else "wss://" 219 | endpoint = match[1] 220 | 221 | url = protocol + endpoint 222 | if not is_valid_url(url): 223 | raise ConstructionError(f'Invalid URL:{original_endpoint}') 224 | 225 | return url 226 | 227 | async def api_connect(self) -> websockets.WebSocketClientProtocol: 228 | """ 229 | Make a websockets connection and returns WebSocketClientProtocol 230 | Returns 231 | ------- 232 | Returns websockets.WebSocketClientProtocol 233 | """ 234 | if not self.wsconnection and self.shouldReconnect: 235 | self.events.on_next({'name': 'connect'}) 236 | self.wsconnection = await websockets.connect(self.api_url) 237 | if self.connected.is_pending(): 238 | self.connected.resolve(True) 239 | else: 240 | self.connected = EasyFuture().resolve(True) 241 | return self.wsconnection 242 | 243 | async def send(self, request: dict) -> dict: 244 | """ 245 | Send the API call and returns response 246 | 247 | Parameters 248 | ---------- 249 | request : dict 250 | API request 251 | 252 | Returns 253 | ------- 254 | API response 255 | """ 256 | 257 | send_will_be_called = self.middlewares.call('sendWillBeCalled', {'request': request}) 258 | if send_will_be_called: 259 | return send_will_be_called 260 | 261 | self.events.on_next({'name': 'send', 'data': request}) 262 | response_future = self.send_and_get_source(request).pipe(op.first(), op.to_future()) 263 | 264 | response = await response_future 265 | self.cache.set(request, response) 266 | if self.storage: 267 | self.storage.set(request, response) 268 | send_is_called = self.middlewares.call('sendIsCalled', {'response': response, 'request': request}) 269 | if send_is_called: 270 | return send_is_called 271 | return response 272 | 273 | 274 | def send_and_get_source(self, request: dict) -> Subject: 275 | """ 276 | Send message and returns Subject 277 | 278 | Parameters 279 | ---------- 280 | request : dict 281 | API request 282 | 283 | Returns 284 | ------- 285 | Returns the Subject 286 | """ 287 | pending = Subject() 288 | if 'req_id' not in request: 289 | self.req_id += 1 290 | request['req_id'] = self.req_id 291 | self.pending_requests[request['req_id']] = pending 292 | 293 | async def send_message(): 294 | try: 295 | await self.connected 296 | await self.wsconnection.send(json.dumps(request)) 297 | except Exception as err: 298 | pending.on_error(err) 299 | 300 | self.add_task(send_message(), 'send_message') 301 | return pending 302 | 303 | async def subscribe(self, request: dict) -> Observable: 304 | """ 305 | Subscribe to a given request 306 | 307 | Parameters 308 | ---------- 309 | request : dict 310 | Subscribe request 311 | 312 | Example 313 | ------- 314 | >>> proposal_subscription = api.subscribe({"proposal_open_contract": 1, "contract_id": 11111111, "subscribe": 1}) 315 | 316 | Returns 317 | ------- 318 | Observable 319 | """ 320 | 321 | return await self.subscription_manager.subscribe(request) 322 | 323 | async def forget(self, subs_id: str) -> dict: 324 | """ 325 | Forget / unsubscribe the specific subscription. 326 | 327 | Parameters 328 | ---------- 329 | subs_id : str 330 | subscription id 331 | 332 | Returns 333 | ------- 334 | Returns dict 335 | """ 336 | 337 | return await self.subscription_manager.forget(subs_id) 338 | 339 | async def forget_all(self, *types) -> dict: 340 | """ 341 | Forget / unsubscribe the subscriptions of given types. 342 | 343 | Possible values are: 'ticks', 'candles', 'proposal', 'proposal_open_contract', 'balance', 'transaction' 344 | 345 | Parameter 346 | --------- 347 | *types : Any number of non-keyword arguments 348 | Example 349 | ------- 350 | api.forget_all("ticks", "candles") 351 | 352 | Returns 353 | ------- 354 | Returns the dict 355 | """ 356 | 357 | return await self.subscription_manager.forget_all(*types) 358 | 359 | async def disconnect(self) -> None: 360 | """ 361 | Disconnect the websockets connection 362 | 363 | """ 364 | if not self.connected.is_resolved(): 365 | return 366 | self.connected = EasyFuture().reject(ConnectionClosedOK(None, Close(1000, 'Closed by disconnect'))) 367 | self.connected.exception() # fetch exception to avoid the warning of 'exception never retrieved' 368 | if self.wsconnection_from_inside: 369 | # TODO NEXT reconnect feature 370 | self.shouldReconnect = False 371 | self.events.on_next({'name': 'close'}) 372 | await self.wsconnection.close() 373 | 374 | def expect_response(self, *msg_types): 375 | """ 376 | Expect specific message types 377 | 378 | Parameters 379 | ---------- 380 | *msg_types : variable number of non-key string argument 381 | Expect these types to be received by the API 382 | Returns 383 | ------- 384 | Resolves to a single response or an array 385 | """ 386 | for msg_type in msg_types: 387 | if msg_type not in self.expect_response_types: 388 | future: Future = asyncio.get_event_loop().create_future() 389 | 390 | async def get_by_msg_type(a_msg_type): 391 | nonlocal future 392 | val = await self.cache.get_by_msg_type(a_msg_type) 393 | if not val and self.storage: 394 | val = self.storage.get_by_msg_type(a_msg_type) 395 | if val: 396 | future.set_result(val) 397 | 398 | self.add_task(get_by_msg_type(msg_type), 'get_by_msg_type') 399 | self.expect_response_types[msg_type] = future 400 | 401 | # expect on a single response returns a single response, not a list 402 | if len(msg_types) == 1: 403 | return self.expect_response_types[msg_types[0]] 404 | 405 | return asyncio.gather(map(lambda t: self.expect_response_types[t], msg_types)) 406 | 407 | def delete_from_expect_response(self, request: dict): 408 | """ 409 | Delete the given request message type from expect_response_types 410 | 411 | Parameters 412 | ---------- 413 | request : dict 414 | 415 | """ 416 | response_type = None 417 | for k in self.expect_response_types.keys(): 418 | if k in request: 419 | response_type = k 420 | break 421 | 422 | if response_type and self.expect_response_types[response_type] \ 423 | and self.expect_response_types[response_type].done(): 424 | del self.expect_response_types[response_type] 425 | 426 | def add_task(self, coroutine: Coroutine, name: str) -> None: 427 | """ 428 | Add coroutine object to execution 429 | 430 | Parameters 431 | ---------- 432 | coroutine : Coroutine 433 | Coroutine object 434 | name: str 435 | name of the Coroutine 436 | 437 | """ 438 | name = 'deriv_api:' + name 439 | 440 | async def wrap_coro(coru: Coroutine, pname: str) -> None: 441 | try: 442 | await coru 443 | except Exception as err: 444 | self.sanity_errors.on_next(AddedTaskError(err, pname)) 445 | 446 | asyncio.create_task(wrap_coro(coroutine, name), name=name) 447 | 448 | async def clear(self): 449 | """ 450 | Disconnect and cancel all the tasks 451 | """ 452 | await self.disconnect() 453 | for task in asyncio.all_tasks(): 454 | if re.match(r"^deriv_api:", task.get_name()): 455 | task.cancel('deriv api ended') 456 | -------------------------------------------------------------------------------- /deriv_api/easy_future.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import asyncio 3 | from asyncio import Future, CancelledError, InvalidStateError 4 | from typing import Any, Optional, TypeVar, Union, Callable 5 | import weakref 6 | 7 | _S = TypeVar("_S") 8 | 9 | 10 | class EasyFuture(Future): 11 | """A class that extend asyncio Future class and has some more convenient methods 12 | Just like Promise in JS or Future in Perl""" 13 | def __init__(self, *, loop: Optional[asyncio.AbstractEventLoop] = None, label: Optional[str] = None) -> None: 14 | super().__init__(loop=loop) 15 | if not label: 16 | label = f"Future {id(self)}" 17 | self.label = label 18 | 19 | @classmethod 20 | def wrap(cls, future: Future) -> EasyFuture: 21 | """Wrap an Asyncio Future to a EasyFuture""" 22 | if isinstance(future, cls): 23 | return future 24 | 25 | easy_future = cls(loop=future.get_loop()) 26 | easy_future.cascade(future) 27 | weak_future = weakref.ref(future) 28 | 29 | def cancel_cb(cb_future: Future): 30 | out_future = weak_future() 31 | if cb_future.cancelled() and not out_future.done(): 32 | try: 33 | cb_future.result() 34 | except CancelledError as err: 35 | out_future.cancel(*err.args) 36 | 37 | easy_future.add_done_callback(cancel_cb) 38 | return easy_future 39 | 40 | def resolve(self, *args: Any) -> EasyFuture: 41 | """Set result on the future""" 42 | super().set_result(*args) 43 | return self 44 | 45 | def reject(self, *args: Union[type, BaseException]) -> EasyFuture: 46 | """Set exception on the future""" 47 | super().set_exception(*args) 48 | return self 49 | 50 | def is_pending(self) -> bool: 51 | """Check if the future is pending (not done)""" 52 | return not self.done() 53 | 54 | def is_resolved(self) -> bool: 55 | """check if the future is resolved (result set)""" 56 | return self.done() and not self.cancelled() and not self.exception() 57 | 58 | def is_rejected(self) -> bool: 59 | """check if the future is rejected (exception set)""" 60 | return self.done() and not self.cancelled() and self.exception() 61 | 62 | def is_cancelled(self) -> bool: 63 | """check if the future is cancelled""" 64 | return self.cancelled() 65 | 66 | def cascade(self, future: Future) -> EasyFuture: 67 | """copy another future result to itself""" 68 | if self.done(): 69 | raise InvalidStateError('invalid state') 70 | 71 | def done_callback(f: Future) -> None: 72 | try: 73 | result = f.result() 74 | self.set_result(result) 75 | except CancelledError as err: 76 | self.cancel(*err.args) 77 | except BaseException as err: 78 | self.set_exception(err) 79 | 80 | future.add_done_callback(done_callback) 81 | return self 82 | 83 | def then(self, then_callback: Union[Callable[[Any], Any], None], else_callback: Union[Callable[[Any], Any], None] = None) -> EasyFuture: 84 | """Simulate Perl Future's 'then' function. 85 | Parameters: 86 | then_callback: the cb function that will be called when the original Future is resolved 87 | else_callback: the cb function that will be called when the original Future is rejected, 88 | can be None 89 | 90 | Both cb function should return a Future. The Future returned by the function 'then' 91 | will have same result of cb returned Future. 92 | """ 93 | new_future = EasyFuture(loop=self.get_loop()) 94 | 95 | def done_callback(myself: EasyFuture) -> None: 96 | f: Optional[EasyFuture] = None 97 | if myself.is_cancelled(): 98 | new_future.cancel('Upstream future cancelled') 99 | return 100 | 101 | if myself.is_rejected() and else_callback: 102 | f = else_callback(myself.exception()) 103 | elif myself.is_resolved() and then_callback: 104 | f = then_callback(myself.result()) 105 | 106 | if f is None: 107 | new_future.cascade(myself) 108 | return 109 | 110 | def inside_callback(internal_future: EasyFuture) -> None: 111 | new_future.cascade(internal_future) 112 | 113 | f.add_done_callback(inside_callback) 114 | 115 | self.add_done_callback(done_callback) 116 | return new_future 117 | 118 | def catch(self, else_callback: Callable[[_S], Any]) -> EasyFuture: 119 | """An variant of 'then' function. it can only get an 'else_cb' which will be run when the future rejected""" 120 | return self.then(None, else_callback) 121 | -------------------------------------------------------------------------------- /deriv_api/errors.py: -------------------------------------------------------------------------------- 1 | def error_factory(class_type: str) -> object: 2 | class GenericError(Exception): 3 | def __init__(self, message: str): 4 | super().__init__(message) 5 | self.type = class_type 6 | self.message = message 7 | 8 | def __str__(self) -> str: 9 | return f'{self.type}:{self.message}' 10 | 11 | return GenericError 12 | 13 | 14 | class APIError(error_factory('APIError')): 15 | pass 16 | 17 | 18 | class ConstructionError(error_factory('ConstructionError')): 19 | pass 20 | 21 | 22 | class ResponseError(Exception): 23 | def __init__(self, response: dict): 24 | super().__init__(response['error']['message']) 25 | self.request = response['echo_req'] 26 | self.code = response['error']['code'] 27 | self.message = response['error']['message'] 28 | self.msg_type = response['msg_type'] 29 | self.req_id = response.get('req_id') 30 | 31 | def __str__(self) -> str: 32 | return f"ResponseError: {self.message}" 33 | 34 | 35 | class AddedTaskError(Exception): 36 | def __init__(self, exception, name): 37 | super().__init__() 38 | self.exception = exception 39 | self.name = name 40 | 41 | def __str__(self) -> str: 42 | return f"{self.name}: {str(self.exception)}" 43 | -------------------------------------------------------------------------------- /deriv_api/in_memory.py: -------------------------------------------------------------------------------- 1 | 2 | __pdoc__ = { 3 | 'deriv_api.in_memory.InMemory.get': False, 4 | 'deriv_api.in_memory.InMemory.get_by_msg_type': False, 5 | 'deriv_api.in_memory.InMemory.has': False, 6 | 'deriv_api.in_memory.InMemory.set': False 7 | } 8 | 9 | 10 | class InMemory: 11 | """An in memory storage which can be used for caching""" 12 | 13 | def __init__(self) -> None: 14 | self.store = {} 15 | self.type_store = {} 16 | 17 | def has(self, key: bytes) -> bool: 18 | """ 19 | Check the key exists in the store and returns true if exists. 20 | 21 | Parameters 22 | ---------- 23 | key : bytes 24 | Request object key 25 | 26 | Returns 27 | ------- 28 | Returns true if the request key exists in memory 29 | """ 30 | return key in self.store 31 | 32 | # we should serialize key (utils/dict_to_cache_key) before we store it 33 | # At first I want to use it directly here. 34 | # But from js version of deriv-api logic, user can choose cache object freely. 35 | # So we shouldn't suppose other cache module will serialize the key. 36 | # So we should always call serialize in the caller module 37 | def get(self, key: bytes) -> dict: 38 | """ 39 | Get the response stored in for the given request by key 40 | 41 | Parameters 42 | ---------- 43 | key : bytes 44 | Request of object key 45 | 46 | Returns 47 | ------- 48 | Response object received and stored in for the given request key 49 | """ 50 | return self.store[key] 51 | 52 | def get_by_msg_type(self, msg_type: str) -> dict: 53 | """ 54 | Get the response stored in based on message type 55 | 56 | Parameters 57 | ---------- 58 | msg_type : str 59 | Request object msg_type 60 | 61 | Returns 62 | ------- 63 | Response object stored in for the given message type 64 | """ 65 | return self.type_store.get(msg_type) 66 | 67 | def set(self, key: bytes, value: dict) -> None: 68 | """ 69 | Stores the response of the given request 70 | 71 | Parameters 72 | ---------- 73 | key : bytes 74 | Request object key 75 | value : dict 76 | Response object received from api 77 | """ 78 | self.store[key] = value 79 | self.type_store[value['msg_type']] = value 80 | -------------------------------------------------------------------------------- /deriv_api/middlewares.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, Union 2 | 3 | _implemented_middlewares = ['sendWillBeCalled', 'sendIsCalled'] 4 | 5 | 6 | class MiddleWares: 7 | """ 8 | A class that help to manage middlewares 9 | 10 | Examples: 11 | middlewares = MiddleWares() 12 | middlewares.add('sendWillBeCalled', lanmbda req: print(req)) 13 | middlewares = Middlewares({'sendWillBeCalled': lambda req: print(req)}) 14 | middleware->call('sendWillBeCalled', arg1, arg2) 15 | 16 | Parameters: 17 | options: 18 | dict with following key value pairs 19 | key: string, middleware name 20 | value: function, middleware code 21 | """ 22 | 23 | def __init__(self, middlewares: dict = {}): 24 | self.middlewares = {} 25 | for name in middlewares.keys(): 26 | self.add(name, middlewares[name]) 27 | 28 | def add(self, name: str, code: Callable[..., bool]) -> None: 29 | """ 30 | Add middleware 31 | 32 | Parameters: 33 | name: Str 34 | middleware name 35 | code: function 36 | middleware code 37 | """ 38 | if not isinstance(name, str): 39 | raise Exception(f"name {name} should be a string") 40 | if not isinstance(code, Callable): 41 | raise Exception(f"code {code} should be a Callable object") 42 | if name in _implemented_middlewares: 43 | self.middlewares[name] = code 44 | else: 45 | raise Exception(f"{name} is not supported in middleware") 46 | 47 | def call(self, name: str, args: dict) -> Union[None, dict]: 48 | """ 49 | Call middleware and return the result if there is such middleware 50 | 51 | Parameters 52 | ---------- 53 | name: string 54 | args: list 55 | the args that will feed to middleware 56 | 57 | Returns 58 | ------- 59 | If there is such middleware, then return the result of middleware 60 | else return None 61 | """ 62 | 63 | if name not in self.middlewares: 64 | return None 65 | return self.middlewares[name](args) 66 | -------------------------------------------------------------------------------- /deriv_api/streams_list.py: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by scripts/regen-py.pl at 20231004-000733 2 | 3 | # streams_list is the list of subscriptions msg_types available. 4 | # Please update it by scripts/regen-py.pl 5 | # Refer https://developers.binary.com/ 6 | streams_list = [ 'balance', 'buy', 'exchange_rates', 'p2p_advert_info', 'p2p_advertiser_create', 'p2p_advertiser_info', 'p2p_order_create', 'p2p_order_info', 'p2p_order_list', 'proposal', 'proposal_open_contract', 'ticks', 'ticks_history', 'transaction', 'website_status', ] 7 | -------------------------------------------------------------------------------- /deriv_api/subscription_manager.py: -------------------------------------------------------------------------------- 1 | from deriv_api.utils import dict_to_cache_key 2 | from deriv_api.errors import APIError 3 | from deriv_api.streams_list import streams_list 4 | from reactivex import operators as op 5 | from reactivex.subject import Subject 6 | from reactivex import Observable 7 | from typing import Optional, Union 8 | __pdoc__ = { 9 | 'deriv_api.subscription_manager.SubscriptionManager.complete_subs_by_ids': False, 10 | 'deriv_api.subscription_manager.SubscriptionManager.complete_subs_by_key': False, 11 | 'deriv_api.subscription_manager.SubscriptionManager.create_new_source': False, 12 | 'deriv_api.subscription_manager.SubscriptionManager.get_source': False, 13 | 'deriv_api.subscription_manager.SubscriptionManager.remove_key_on_error': False, 14 | 'deriv_api.subscription_manager.SubscriptionManager.save_subs_id': False, 15 | 'deriv_api.subscription_manager.SubscriptionManager.save_subs_per_msg_type': False, 16 | 'deriv_api.subscription_manager.SubscriptionManager.source_exists': False, 17 | 'deriv_api.subscription_manager.SubscriptionManager.forget': False, 18 | 'deriv_api.subscription_manager.SubscriptionManager.forget_all': False, 19 | 'deriv_api.subscription_manager.get_msg_type': False 20 | } 21 | 22 | 23 | class SubscriptionManager: 24 | """ 25 | Subscription Manager - manage subscription channels 26 | 27 | Makes sure there is always only one subscription channel for all requests of subscriptions, 28 | keeps a history of received values for the subscription of ticks and forgets channels that 29 | do not have subscribers. It also ensures that subscriptions are revived after connection 30 | drop/account changed. 31 | 32 | Parameters 33 | ---------- 34 | api : deriv_api.DerivAPI 35 | 36 | Example 37 | ------- 38 | - create a new subscription for R_100 39 | >>> source_tick_50: Observable = await api.subscribe({'ticks': 'R_50'}) 40 | >>> subscription_id = 0 41 | >>> def tick_50_callback(data): 42 | >>> global subscription_id 43 | >>> subscription_id = data['subscription']['id'] 44 | >>> print(data) 45 | >>> source_tick_50.subscribe(tick_50_callback) 46 | 47 | - forget all ticks 48 | >>> await api.forget_all('ticks') 49 | 50 | - forget based on subscription id 51 | >>> await api.forget(subscription_id) 52 | """ 53 | 54 | def __init__(self, api): 55 | self.api = api 56 | self.sources: dict = {} 57 | self.orig_sources: dict = {} 58 | self.subs_id_to_key: dict = {} 59 | self.key_to_subs_id: dict = {} 60 | self.buy_key_to_contract_id: dict = {} 61 | self.subs_per_msg_type: dict = {} 62 | 63 | async def subscribe(self, request: dict) -> Observable: 64 | """ 65 | Subscribe to a given request, returns a stream of new responses, 66 | Errors should be handled by the user of the stream 67 | 68 | Example 69 | ------- 70 | >>> ticks = api.subscribe({ 'ticks': 'R_100' }) 71 | >>> ticks.subscribe(call_back_function) 72 | 73 | Parameters 74 | ---------- 75 | request : dict 76 | A request object acceptable by the API 77 | 78 | Returns 79 | ------- 80 | Observable 81 | An RxPY SObservable 82 | """ 83 | if not get_msg_type(request): 84 | raise APIError('Subscription type is not found in deriv-api') 85 | 86 | if self.source_exists(request): 87 | return self.get_source(request) 88 | 89 | new_request: dict = request.copy() 90 | new_request['subscribe'] = 1 91 | return await self.create_new_source(new_request) 92 | 93 | def get_source(self, request: dict) -> Optional[Subject]: 94 | """ 95 | To get the source from the source list stored in sources 96 | 97 | Parameters 98 | ---------- 99 | request : dict 100 | Request object 101 | 102 | Returns 103 | ------- 104 | Returns source observable if exists, otherwise returns None 105 | """ 106 | key: bytes = dict_to_cache_key(request) 107 | if key in self.sources: 108 | return self.sources[key] 109 | 110 | # if we have a buy subscription reuse that for poc 111 | for c in self.buy_key_to_contract_id.values(): 112 | if c['contract_id'] == request['contract_id']: 113 | return self.sources[c['buy_key']] 114 | 115 | return None 116 | 117 | def source_exists(self, request: dict): 118 | """ 119 | Get the source by request 120 | 121 | Parameters 122 | ---------- 123 | request : dict 124 | A request object 125 | 126 | Returns 127 | ------- 128 | Returns source observable if exists in source list, otherwise None 129 | 130 | """ 131 | return self.get_source(request) 132 | 133 | async def create_new_source(self, request: dict) -> Observable: 134 | """ 135 | Create new source observable, stores it in source list and returns 136 | 137 | Parameters 138 | ---------- 139 | request : dict 140 | A request object 141 | 142 | Returns 143 | ------- 144 | Returns source observable 145 | """ 146 | key: bytes = dict_to_cache_key(request) 147 | 148 | def forget_old_source() -> None: 149 | if key not in self.key_to_subs_id: 150 | return 151 | # noinspection PyBroadException 152 | try: 153 | self.api.add_task(self.forget(self.key_to_subs_id[key]), 'forget old subscription') 154 | except Exception as err: 155 | self.api.sanity_errors.on_next(err) 156 | return 157 | 158 | self.orig_sources[key]: Observable = self.api.send_and_get_source(request) 159 | source: Observable = self.orig_sources[key].pipe( 160 | op.finally_action(forget_old_source), 161 | op.share() 162 | ) 163 | self.sources[key] = source 164 | self.save_subs_per_msg_type(request, key) 165 | 166 | async def process_response() -> None: 167 | # noinspection PyBroadException 168 | try: 169 | response = await source.pipe(op.first(), op.to_future()) 170 | if request.get('buy'): 171 | self.buy_key_to_contract_id[key] = { 172 | 'contract_id': response['buy']['contract_id'], 173 | 'buy_key': key 174 | } 175 | self.save_subs_id(key, response['subscription']) 176 | except Exception: 177 | self.remove_key_on_error(key) 178 | 179 | self.api.add_task(process_response(), 'subs manager: process_response') 180 | return source 181 | 182 | async def forget(self, subs_id: str) -> dict: 183 | """ 184 | Delete the source from source list, clears the subscription detail from subs_id_to_key and key_to_subs_id and 185 | make api call to unsubscribe the subscription 186 | Parameters 187 | ---------- 188 | subs_id : str 189 | Subscription id 190 | 191 | Returns 192 | ------- 193 | Returns dict - api response for forget call 194 | """ 195 | self.complete_subs_by_ids(subs_id) 196 | return await self.api.send({'forget': subs_id}) 197 | 198 | async def forget_all(self, *types) -> dict: 199 | """ 200 | Unsubscribe all subscription's of given type. For each subscription, it deletes the source from source list, 201 | clears the subscription detail from subs_id_to_key and key_to_subs_id. Make api call to unsubscribe all the 202 | subscriptions of given types. 203 | Parameters 204 | ---------- 205 | types : Positional argument 206 | subscription stream types example : ticks, candles 207 | 208 | Returns 209 | ------- 210 | Response from API call 211 | """ 212 | # To include subscriptions that were automatically unsubscribed 213 | # for example a proposal subscription is auto-unsubscribed after buy 214 | 215 | for t in types: 216 | for k in (self.subs_per_msg_type.get(t) or []): 217 | self.complete_subs_by_key(k) 218 | self.subs_per_msg_type[t] = [] 219 | return await self.api.send({'forget_all': list(types)}) 220 | 221 | def complete_subs_by_ids(self, *subs_ids): 222 | """ 223 | Completes the subscription for the given subscription id's - delete the source from source list, clears the 224 | subscription detail from subs_id_to_key and key_to_subs_id. Mark the original source as complete. 225 | 226 | Parameters 227 | ---------- 228 | subs_ids : Positional argument 229 | subscription ids 230 | 231 | """ 232 | for subs_id in subs_ids: 233 | if subs_id in self.subs_id_to_key: 234 | key = self.subs_id_to_key[subs_id] 235 | self.complete_subs_by_key(key) 236 | 237 | def save_subs_id(self, key: bytes, subscription: Union[dict, None]): 238 | """ 239 | Saves the subscription detail in subs_id_to_key and key_to_subs_id 240 | 241 | Parameters 242 | ---------- 243 | key : bytes 244 | API call request key. Key for key_to_subs_id 245 | subscription : dict or None 246 | subscription details - subscription id 247 | 248 | """ 249 | if not subscription: 250 | return self.complete_subs_by_key(key) 251 | 252 | subs_id = subscription['id'] 253 | if subs_id not in self.subs_id_to_key: 254 | self.subs_id_to_key[subs_id] = key 255 | self.key_to_subs_id[key] = subs_id 256 | 257 | return None 258 | 259 | def save_subs_per_msg_type(self, request: dict, key: bytes): 260 | """ 261 | Save the request's key in subscription per message type 262 | 263 | Parameters 264 | ---------- 265 | request : dict 266 | API request object 267 | key : bytes 268 | API request key 269 | 270 | """ 271 | msg_type = get_msg_type(request) 272 | if msg_type: 273 | self.subs_per_msg_type[msg_type] = self.subs_per_msg_type.get(msg_type) or [] 274 | self.subs_per_msg_type[msg_type].append(key) 275 | else: 276 | self.api.sanity_errors.next(APIError('Subscription type is not found in deriv-api')) 277 | 278 | def remove_key_on_error(self, key: bytes): 279 | """ 280 | Remove ths source from source list, clears the subscription detail from subs_id_to_key and key_to_subs_id. 281 | Mark the original source as complete. 282 | 283 | Parameters 284 | ---------- 285 | key : bytes 286 | Request object key in bytes. Used to identify the subscription stored in key_to_subs_id 287 | 288 | """ 289 | return lambda: self.complete_subs_by_key(key) 290 | 291 | def complete_subs_by_key(self, key: bytes): 292 | """ 293 | Identify the source from source list based on request object key and removes it. Clears the subscription detail 294 | from subs_id_to_key and key_to_subs_id. Mark the original source as complete. 295 | 296 | Parameters 297 | ---------- 298 | key : bytes 299 | Request object key to identify the subscription stored in key_to_subs_id 300 | 301 | """ 302 | if not key or not self.sources[key]: 303 | return 304 | 305 | # Delete the source 306 | del self.sources[key] 307 | orig_source: Subject = self.orig_sources.pop(key) 308 | 309 | try: 310 | # Delete the subs id if exist 311 | if key in self.key_to_subs_id: 312 | subs_id = self.key_to_subs_id[key] 313 | del self.subs_id_to_key[subs_id] 314 | # Delete the key 315 | del self.key_to_subs_id[key] 316 | 317 | # Delete the buy key to contract_id mapping 318 | del self.buy_key_to_contract_id[key] 319 | except KeyError: 320 | pass 321 | 322 | # Mark the source complete 323 | orig_source.on_completed() 324 | orig_source.dispose() 325 | 326 | 327 | def get_msg_type(request: dict) -> str: 328 | """ 329 | Get message type by request 330 | 331 | Parameters 332 | ---------- 333 | request : dict 334 | Request 335 | 336 | Returns 337 | ------- 338 | Returns the next item from the iterator 339 | """ 340 | return next((x for x in streams_list if x in request), None) 341 | -------------------------------------------------------------------------------- /deriv_api/utils.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import re 3 | 4 | 5 | def dict_to_cache_key(obj: dict) -> bytes: 6 | """convert the dictionary object to Pickled representation of object as bytes 7 | 8 | Parameter 9 | --------- 10 | obj : dict 11 | Request arguments 12 | Returns 13 | ------- 14 | bytes 15 | Pickled representation of request object as bytes 16 | """ 17 | 18 | cloned_obj: dict = obj.copy() 19 | for key in ['req_id', 'passthrough', 'subscribe']: 20 | cloned_obj.pop(key, None) 21 | 22 | return pickle.dumps(cloned_obj) 23 | 24 | 25 | def is_valid_url(url: str) -> bool: 26 | regex = re.compile( 27 | r'^wss?://' # ws:// or wss:// 28 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... 29 | r'localhost|' # localhost... 30 | r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})' # ...or ip 31 | r'(?::\d+)?' # optional port 32 | r'(?:/?|[/?]\S+)$', re.IGNORECASE) 33 | return re.match(regex, url) is not None 34 | -------------------------------------------------------------------------------- /docs/templates/config.mako: -------------------------------------------------------------------------------- 1 | <%! 2 | # Template configuration. Copy over in your template directory 3 | # (used with `--template-dir`) and adapt as necessary. 4 | # Note, defaults are loaded from this distribution file, so your 5 | # config.mako only needs to contain values you want overridden. 6 | # You can also run pdoc with `--config KEY=VALUE` to override 7 | # individual values. 8 | 9 | html_lang = 'en' 10 | show_inherited_members = False 11 | extract_module_toc_into_sidebar = True 12 | list_class_variables_in_index = False 13 | sort_identifiers = False 14 | show_type_annotations = False 15 | 16 | # Show collapsed source code block next to each item. 17 | # Disabling this can improve rendering speed of large modules. 18 | show_source_code = False 19 | 20 | # If set, format links to objects in online source code repository 21 | # according to this template. Supported keywords for interpolation 22 | # are: commit, path, start_line, end_line. 23 | #git_link_template = 'https://github.com/USER/python-derive-api/blob/{commit}/{path}#L{start_line}-L{end_line}' 24 | git_link_template = None 25 | 26 | # A prefix to use for every HTML hyperlink in the generated documentation. 27 | # No prefix results in all links being relative. 28 | link_prefix = '' 29 | 30 | # Enable syntax highlighting for code/source blocks by including Highlight.js 31 | syntax_highlighting = True 32 | 33 | # Set the style keyword such as 'atom-one-light' or 'github-gist' 34 | # Options: https://github.com/highlightjs/highlight.js/tree/master/src/styles 35 | # Demo: https://highlightjs.org/static/demo/ 36 | hljs_style = 'github' 37 | 38 | # If set, insert Google Analytics tracking code. Value is GA 39 | # tracking id (UA-XXXXXX-Y). 40 | google_analytics = '' 41 | 42 | # If set, insert Google Custom Search search bar widget above the sidebar index. 43 | # The whitespace-separated tokens represent arbitrary extra queries (at least one 44 | # must match) passed to regular Google search. Example: 45 | #google_search_query = 'inurl:github.com/USER/PROJECT site:PROJECT.github.io site:PROJECT.website' 46 | google_search_query = '' 47 | 48 | # Enable offline search using Lunr.js. For explanation of 'fuzziness' parameter, which is 49 | # added to every query word, see: https://lunrjs.com/guides/searching.html#fuzzy-matches 50 | # If 'index_docstrings' is False, a shorter index is built, indexing only 51 | # the full object reference names. 52 | #lunr_search = {'fuzziness': 1, 'index_docstrings': True} 53 | lunr_search = None 54 | 55 | # If set, render LaTeX math syntax within \(...\) (inline equations), 56 | # or within \[...\] or $$...$$ or `.. math::` (block equations) 57 | # as nicely-formatted math formulas using MathJax. 58 | # Note: in Python docstrings, either all backslashes need to be escaped (\\) 59 | # or you need to use raw r-strings. 60 | latex_math = False 61 | %> 62 | -------------------------------------------------------------------------------- /docs/templates/html.mako: -------------------------------------------------------------------------------- 1 | <% 2 | import os 3 | 4 | import pdoc 5 | from pdoc.html_helpers import extract_toc, glimpse, to_html as _to_html, format_git_link 6 | 7 | 8 | def link(dobj: pdoc.Doc, name=None): 9 | name = name or dobj.qualname + ('()' if isinstance(dobj, pdoc.Function) else '') 10 | if isinstance(dobj, pdoc.External) and not external_links: 11 | return name 12 | url = dobj.url(relative_to=module, link_prefix=link_prefix, 13 | top_ancestor=not show_inherited_members) 14 | return f'{name}' 15 | 16 | 17 | def to_html(text): 18 | return _to_html(text, docformat=docformat, module=module, link=link, latex_math=latex_math) 19 | 20 | 21 | def get_annotation(bound_method, sep=':'): 22 | annot = show_type_annotations and bound_method(link=link) or '' 23 | if annot: 24 | annot = ' ' + sep + '\N{NBSP}' + annot 25 | return annot 26 | %> 27 | 28 | <%def name="ident(name)">${name} 29 | 30 | <%def name="show_source(d)"> 31 | % if (show_source_code or git_link_template) and d.source and d.obj is not getattr(d.inherits, 'obj', None): 32 | <% git_link = format_git_link(git_link_template, d) %> 33 | % if show_source_code: 34 |
35 | 36 | Expand source code 37 | % if git_link: 38 | Browse git 39 | %endif 40 | 41 |
${d.source | h}
42 |
43 | % elif git_link: 44 | 45 | %endif 46 | %endif 47 | 48 | 49 | <%def name="show_desc(d, short=False)"> 50 | <% 51 | inherits = ' inherited' if d.inherits else '' 52 | docstring = glimpse(d.docstring) if short or inherits else d.docstring 53 | %> 54 | % if d.inherits: 55 |

56 | Inherited from: 57 | % if hasattr(d.inherits, 'cls'): 58 | ${link(d.inherits.cls)}.${link(d.inherits, d.name)} 59 | % else: 60 | ${link(d.inherits)} 61 | % endif 62 |

63 | % endif 64 |
${docstring | to_html}
65 | % if not isinstance(d, pdoc.Module): 66 | ${show_source(d)} 67 | % endif 68 | 69 | 70 | <%def name="show_module_list(modules)"> 71 |

Python module list

72 | 73 | % if not modules: 74 |

No modules found.

75 | % else: 76 |
77 | % for name, desc in modules: 78 |
79 |
${name}
80 |
${desc | glimpse, to_html}
81 |
82 | % endfor 83 |
84 | % endif 85 | 86 | 87 | <%def name="show_column_list(items)"> 88 | <% 89 | two_column = len(items) >= 6 and all(len(i.name) < 20 for i in items) 90 | %> 91 | 96 | 97 | 98 | <%def name="show_module(module)"> 99 | <% 100 | variables = module.variables(sort=sort_identifiers) 101 | classes = module.classes(sort=sort_identifiers) 102 | functions = module.functions(sort=sort_identifiers) 103 | submodules = module.submodules() 104 | %> 105 | 106 | <%def name="show_func(f)"> 107 |
108 | <% 109 | params = ', '.join(f.params(annotate=show_type_annotations, link=link)) 110 | return_type = get_annotation(f.return_annotation, '\N{non-breaking hyphen}>') 111 | %> 112 | ${f.funcdef()} ${ident(f.name)}(${params})${return_type} 113 |
114 |
${show_desc(f)}
115 | 116 | 117 |
118 | % if http_server: 119 | 127 | % endif 128 |

${'Namespace' if module.is_namespace else \ 129 | 'Package' if module.is_package and not module.supermodule else \ 130 | 'Module'} ${module.name}

131 |
132 | 133 |
134 | ${module.docstring | to_html} 135 | ${show_source(module)} 136 |
137 | 138 |
139 | % if submodules: 140 |

Sub-modules

141 |
142 | % for m in submodules: 143 |
${link(m)}
144 |
${show_desc(m, short=True)}
145 | % endfor 146 |
147 | % endif 148 |
149 | 150 |
151 | % if variables: 152 |

Global variables

153 |
154 | % for v in variables: 155 | <% return_type = get_annotation(v.type_annotation) %> 156 |
var ${ident(v.name)}${return_type}
157 |
${show_desc(v)}
158 | % endfor 159 |
160 | % endif 161 |
162 | 163 |
164 | % if functions: 165 |

Functions

166 |
167 | % for f in functions: 168 | ${show_func(f)} 169 | % endfor 170 |
171 | % endif 172 |
173 | 174 |
175 | % if classes: 176 |

Classes

177 |
178 | % for c in classes: 179 | <% 180 | class_vars = c.class_variables(show_inherited_members, sort=sort_identifiers) 181 | smethods = c.functions(show_inherited_members, sort=sort_identifiers) 182 | inst_vars = c.instance_variables(show_inherited_members, sort=sort_identifiers) 183 | methods = c.methods(show_inherited_members, sort=sort_identifiers) 184 | mro = c.mro() 185 | subclasses = c.subclasses() 186 | params = ', '.join(c.params(annotate=show_type_annotations, link=link)) 187 | %> 188 |
189 | class ${ident(c.name)} 190 | % if params: 191 | (${params}) 192 | % endif 193 |
194 | 195 |
${show_desc(c)} 196 | 197 | % if mro: 198 |

Ancestors

199 |
    200 | % for cls in mro: 201 |
  • ${link(cls)}
  • 202 | % endfor 203 |
204 | %endif 205 | 206 | % if subclasses: 207 |

Subclasses

208 |
    209 | % for sub in subclasses: 210 |
  • ${link(sub)}
  • 211 | % endfor 212 |
213 | % endif 214 | % if class_vars: 215 |

Class variables

216 |
217 | % for v in class_vars: 218 | <% return_type = get_annotation(v.type_annotation) %> 219 |
var ${ident(v.name)}${return_type}
220 |
${show_desc(v)}
221 | % endfor 222 |
223 | % endif 224 | % if smethods: 225 |

Static methods

226 |
227 | % for f in smethods: 228 | ${show_func(f)} 229 | % endfor 230 |
231 | % endif 232 | % if inst_vars: 233 |

Instance variables

234 |
235 | % for v in inst_vars: 236 | <% return_type = get_annotation(v.type_annotation) %> 237 |
var ${ident(v.name)}${return_type}
238 |
${show_desc(v)}
239 | % endfor 240 |
241 | % endif 242 | % if methods: 243 |

Methods

244 |
245 | % for f in methods: 246 | ${show_func(f)} 247 | % endfor 248 |
249 | % endif 250 | 251 | % if not show_inherited_members: 252 | <% 253 | members = c.inherited_members() 254 | %> 255 | % if members: 256 |

Inherited members

257 |
    258 | % for cls, mems in members: 259 |
  • ${link(cls)}: 260 |
      261 | % for m in mems: 262 |
    • ${link(m, name=m.name)}
    • 263 | % endfor 264 |
    265 | 266 |
  • 267 | % endfor 268 |
269 | % endif 270 | % endif 271 | 272 |
273 | % endfor 274 |
275 | % endif 276 |
277 | 278 | 279 | <%def name="module_index(module)"> 280 | <% 281 | variables = module.variables(sort=sort_identifiers) 282 | classes = module.classes(sort=sort_identifiers) 283 | functions = module.functions(sort=sort_identifiers) 284 | submodules = module.submodules() 285 | supermodule = module.supermodule 286 | %> 287 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | <% 372 | module_list = 'modules' in context.keys() # Whether we're showing module list in server mode 373 | %> 374 | 375 | % if module_list: 376 | Python module list 377 | 378 | % else: 379 | ${module.name} API documentation 380 | 381 | % endif 382 | 383 | 384 | 385 | % if syntax_highlighting: 386 | 387 | %endif 388 | 389 | <%namespace name="css" file="css.mako" /> 390 | 391 | 392 | 393 | 394 | % if google_analytics: 395 | 399 | % endif 400 | 401 | % if google_search_query: 402 | 403 | 404 | 408 | % endif 409 | 410 | % if latex_math: 411 | 412 | % endif 413 | 414 | % if syntax_highlighting: 415 | 416 | 417 | % endif 418 | 419 | <%include file="head.mako"/> 420 | 421 | 422 |
423 | % if module_list: 424 |
425 | ${show_module_list(modules)} 426 |
427 | % else: 428 |
429 | ${show_module(module)} 430 |
431 | ${module_index(module)} 432 | % endif 433 |
434 | 435 | 439 | 440 | % if http_server and module: ## Auto-reload on file change in dev mode 441 | 449 | % endif 450 | 451 | -------------------------------------------------------------------------------- /docs/usage_examples.md: -------------------------------------------------------------------------------- 1 | 2 | # Usage examples 3 | ## Short examples 4 | 5 | ```python 6 | from deriv_api import DerivAPI 7 | api = DerivAPI(app_id=app_id) 8 | ``` 9 | 10 | ### Authenticate to an account using token 11 | ```python 12 | authorize = await api.authorize(api_token) 13 | print(authorize) 14 | ``` 15 | ### Get Balance 16 | ```python 17 | account = await api.balance() 18 | print(account) 19 | ``` 20 | ### Get all the assets info 21 | ```python 22 | assets = await api.asset_index({"asset_index": 1}) 23 | print(assets) 24 | ``` 25 | 26 | To get assets info from cache 27 | ```python 28 | assets = await api.cache.asset_index({"asset_index": 1}) 29 | print(assets) 30 | ``` 31 | 32 | ### Get all active symbols 33 | ```python 34 | active_symbols = await api.active_symbols({"active_symbols": "full"}) 35 | print(active_symbols) 36 | ``` 37 | 38 | To get active symbols from cache 39 | ```python 40 | active_symbols = await api.cache.active_symbols({"active_symbols": "full"}) 41 | print(active_symbols) 42 | ``` 43 | 44 | ### Get proposal 45 | ```python 46 | proposal = await api.proposal({"proposal": 1, "amount": 100, "barrier": "+0.1", "basis": "payout", 47 | "contract_type": "CALL", "currency": "USD", "duration": 60, "duration_unit": "s", 48 | "symbol": "R_100" 49 | }) 50 | print(proposal) 51 | ``` 52 | 53 | subscribe the proposal stream 54 | ```python 55 | source_proposal: Observable = await api.subscribe({"proposal": 1, "amount": 100, "barrier": "+0.1", "basis": "payout", 56 | "contract_type": "CALL", "currency": "USD", "duration": 160, 57 | "duration_unit": "s", 58 | "symbol": "R_100", 59 | "subscribe": 1 60 | }) 61 | source_proposal.subscribe(lambda proposal: print(proposal)) 62 | ``` 63 | 64 | ### Buy 65 | ```python 66 | proposal_id = proposal.get('proposal').get('id') 67 | buy = await api.buy({"buy": proposal_id, "price": 100}) 68 | print(buy) 69 | ``` 70 | 71 | ### open contract detail 72 | ```python 73 | contract_id = buy.get('buy').get('contract_id') 74 | poc = await api.proposal_open_contract( 75 | {"proposal_open_contract": 1, "contract_id": contract_id }) 76 | print(poc) 77 | ``` 78 | 79 | subscribe the open contract stream 80 | ``` 81 | source_poc: Observable = await api.subscribe({"proposal_open_contract": 1, "contract_id": contract_id}) 82 | source_poc.subscribe(lambda poc: print(poc) 83 | ``` 84 | 85 | ### Sell 86 | ```python 87 | contract_id = buy.get('buy').get('contract_id') 88 | sell = await api.sell({"sell": contract_id, "price": 40}) 89 | print(sell) 90 | ``` 91 | 92 | ### Profit table 93 | ```python 94 | profit_table = await api.profit_table({"profit_table": 1, "description": 1, "sort": "ASC"}) 95 | print(profit_table) 96 | ``` 97 | 98 | ### Transaction statement 99 | ```python 100 | statement = await api.statement({"statement": 1, "description": 1, "limit": 100, "offset": 25}) 101 | print(statement) 102 | ``` 103 | 104 | ### Subscribe a stream 105 | 106 | We are using rxpy to maintain our deriv api subscriptions. Please distinguish api subscription from rxpy sequence subscription 107 | ```python 108 | # creating a rxpy sequence object to represent deriv api streams 109 | source_tick_50 = await api.subscribe({'ticks': 'R_50'}) 110 | 111 | # subscribe the rxpy sequence with a callback function, 112 | # when the data received, the call back function will be called 113 | source_tick_50.subscribe(lambda tick: print(tick)) 114 | ``` 115 | 116 | ### unsubscribe the rxpy sequence 117 | ```python 118 | seq_sub = source_tick_50.subscribe(lambda tick: print(tick)) 119 | seq_sub.dispose() 120 | ``` 121 | 122 | ### unsubscribe the deriv api stream 123 | 124 | There are 2 ways to unsubscribe deriv api stream 125 | 126 | - by `dispose` all sequence subscriptions 127 | ```python 128 | # creating a rxpy sequence object to represent deriv api streams 129 | source_tick_50 = await api.subscribe({'ticks': 'R_50'}) 130 | # subscribe the rxpy sequence with a callback function, 131 | # when the data received , the call back function will be called 132 | seq_sub1 = source_tick_50.subscribe(lambda tick: print(f"get tick from sub1 {tick}")) 133 | seq_sub2 = source_tick_50.subscribe(lambda tick: print(f"get tick from sub2 {tick}")) 134 | seq_sub1.dispose() 135 | seq_sub2.dispose() 136 | # When all seq subscriptions of one sequence are disposed. Then a `forget` will be called and that deriv api stream will be unsubscribed 137 | ``` 138 | 139 | 140 | - by `forget` that deriv stream 141 | ```python 142 | # get a datum first 143 | from rx import operators as op 144 | tick = await source_tick_50.pipe(op.first(), op.to_future) 145 | api.forget(tick['R_50']['subscription']['id']) 146 | ``` 147 | 148 | ### print errors 149 | ```python 150 | api.sanity_errors.subscribe(lambda err: print(err)) 151 | ``` 152 | 153 | ### do something when one type of message coming 154 | ```python 155 | async def print_hello_after_authorize(): 156 | auth_data = await api.expect_response('authorize') 157 | print(f"Hello {auth_data['authorize']['fullname']}") 158 | asyncio.create_task(print_hello_after_authorize()) 159 | api.authorize({'authorize': 'AVALIDTOKEN'}) 160 | ``` 161 | -------------------------------------------------------------------------------- /examples/simple_bot1.py: -------------------------------------------------------------------------------- 1 | # run it like PYTHONPATH=. python3 examples/simple_bot1.py 2 | import sys 3 | import asyncio 4 | import os 5 | from deriv_api import DerivAPI 6 | from deriv_api import APIError 7 | 8 | app_id = 1089 9 | api_token = os.getenv('DERIV_TOKEN', '') 10 | 11 | if len(api_token) == 0: 12 | sys.exit("DERIV_TOKEN environment variable is not set") 13 | 14 | 15 | async def sample_calls(): 16 | api = DerivAPI(app_id=app_id) 17 | 18 | response = await api.ping({'ping': 1}) 19 | if response['ping']: 20 | print(response['ping']) 21 | 22 | active_symbols = await api.active_symbols({"active_symbols": "brief", "product_type": "basic"}) 23 | print(active_symbols) 24 | 25 | # Authorize 26 | authorize = await api.authorize(api_token) 27 | print(authorize) 28 | 29 | # Get Balance 30 | response = await api.balance() 31 | response = response['balance'] 32 | currency = response['currency'] 33 | print("Your current balance is", response['currency'], response['balance']) 34 | 35 | # Get active symbols from cache 36 | cached_active_symbols = await api.cache.active_symbols({"active_symbols": "brief", "product_type": "basic"}) 37 | print(cached_active_symbols) 38 | 39 | # Get assets 40 | assets = await api.cache.asset_index({"asset_index": 1}) 41 | print(assets) 42 | 43 | # Get proposal 44 | proposal = await api.proposal({"proposal": 1, "amount": 100, "barrier": "+0.1", "basis": "payout", 45 | "contract_type": "CALL", "currency": "USD", "duration": 60, "duration_unit": "s", 46 | "symbol": "R_100" 47 | }) 48 | print(proposal) 49 | 50 | # Buy 51 | response = await api.buy({"buy": proposal.get('proposal').get('id'), "price": 100}) 52 | print(response) 53 | print(response.get('buy').get('buy_price')) 54 | print(response.get('buy').get('contract_id')) 55 | print(response.get('buy').get('longcode')) 56 | await asyncio.sleep(1) # wait 1 second 57 | print("after buy") 58 | 59 | # open contracts 60 | poc = await api.proposal_open_contract( 61 | {"proposal_open_contract": 1, "contract_id": response.get('buy').get('contract_id')}) 62 | print(poc) 63 | print("waiting is sold........................") 64 | if not poc.get('proposal_open_contract').get('is_sold'): 65 | # sell 66 | try: 67 | await asyncio.sleep(1) # wainting for 1 second for entry tick 68 | sell = await api.sell({"sell": response.get('buy').get('contract_id'), "price": 40}) 69 | print(sell) 70 | except APIError as err: 71 | print("error!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") 72 | print(err) 73 | 74 | # profit table 75 | profit_table = await api.profit_table({"profit_table": 1, "description": 1, "sort": "ASC"}) 76 | print(profit_table) 77 | 78 | # transaction statement 79 | statement = await api.statement({"statement": 1, "description": 1, "limit": 100, "offset": 25}) 80 | print(statement) 81 | print("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!end!!!!!!!!!!!!!!!!!!!!1") 82 | await api.clear() 83 | 84 | 85 | asyncio.run(sample_calls()) 86 | -------------------------------------------------------------------------------- /examples/simple_bot2.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.append('.') 3 | import asyncio 4 | import os 5 | from deriv_api import DerivAPI 6 | import websockets 7 | 8 | 9 | app_id = 1089 10 | api_token = os.getenv('DERIV_TOKEN', '') 11 | 12 | if len(api_token) == 0: 13 | sys.exit("DERIV_TOKEN environment variable is not set") 14 | 15 | async def connect(): 16 | url = f'wss://frontend.binaryws.com/websockets/v3?l=EN&app_id={app_id}' 17 | connection = await websockets.connect(url) 18 | 19 | return connection 20 | 21 | async def sample_calls(): 22 | # create your own websocket connection and pass it as argument to DerivAPI 23 | connection = await connect() 24 | api = DerivAPI(connection=connection) 25 | 26 | response = await api.ping({'ping':1}) 27 | if response['ping']: 28 | print(response['ping']) 29 | 30 | """ 31 | To test deriv_api try reconnect automatically on disconnecting, uncomment the below comment then run this script 32 | disconnect your network & reconnect back. Enable the loglevel to logging.DEBUG to see the ping/pong 33 | """ 34 | # await asyncio.sleep(300) 35 | 36 | # Authorize 37 | authorize = await api.authorize(api_token) 38 | print(authorize) 39 | 40 | # Get Balance 41 | response = await api.balance() 42 | response = response['balance'] 43 | currency = response['currency'] 44 | print("Your current balance is", response['currency'], response['balance']) 45 | 46 | # Get active symbols from cache 47 | cached_active_symbols = await api.cache.active_symbols({"active_symbols": "brief", "product_type": "basic"}) 48 | print(cached_active_symbols) 49 | 50 | # Get assets 51 | assets = await api.cache.asset_index({"asset_index": 1}) 52 | print(assets) 53 | await api.clear() 54 | 55 | asyncio.run(sample_calls()) 56 | -------------------------------------------------------------------------------- /examples/simple_bot3.py: -------------------------------------------------------------------------------- 1 | # run it like PYTHONPATH=. python3 examples/simple_bot1.py 2 | import asyncio 3 | from deriv_api import DerivAPI 4 | from rx import Observable 5 | app_id = 1089 6 | 7 | 8 | async def sample_calls(): 9 | api = DerivAPI(app_id=app_id) 10 | wait_tick = api.expect_response('tick') 11 | last_data = {} 12 | source_tick_50: Observable = await api.subscribe({'ticks': 'R_50'}) 13 | def create_subs_cb(symbol): 14 | count = 1 15 | def cb(data): 16 | nonlocal count 17 | count = count + 1 18 | last_data[symbol] = data 19 | print(f"get symbol {symbol} {count}") 20 | return cb 21 | 22 | a_sub = source_tick_50.subscribe(create_subs_cb('R_50')) 23 | b_sub = source_tick_50.subscribe(create_subs_cb('R_50')) 24 | source_tick_100: Observable = await api.subscribe({'ticks': 'R_100'}) 25 | source_tick_100.subscribe(create_subs_cb('R_100')) 26 | first_tick = await wait_tick 27 | print(f"first tick is {first_tick}") 28 | await asyncio.sleep(5) 29 | print("now will forget") 30 | #await api.forget(last_data['R_50']['subscription']['id']) 31 | #await api.forget(last_data['R_100']['subscription']['id']) 32 | #await api.forget_all('ticks') 33 | a_sub.dispose() 34 | await asyncio.sleep(5) 35 | print("disposing the last one will call forget") 36 | b_sub.dispose() 37 | await asyncio.sleep(5) 38 | await api.clear() 39 | 40 | asyncio.run(sample_calls()) 41 | -------------------------------------------------------------------------------- /examples/simple_bot4.py: -------------------------------------------------------------------------------- 1 | # run it like PYTHONPATH=. python3 examples/simple_bot4.py 2 | import sys 3 | import asyncio 4 | import os 5 | from deriv_api import DerivAPI 6 | from rx import Observable 7 | 8 | app_id = 1089 9 | api_token = os.getenv('DERIV_TOKEN', '') 10 | expected_payout = os.getenv('EXPECTED_PAYOUT', 10) 11 | 12 | if len(api_token) == 0: 13 | sys.exit("DERIV_TOKEN environment variable is not set") 14 | 15 | 16 | async def sample_calls(): 17 | api = DerivAPI(app_id=app_id) 18 | 19 | # Authorize 20 | authorize = await api.authorize(api_token) 21 | 22 | asyncio.create_task(buy_proposal(api)) 23 | 24 | # Subscribe proposal 25 | source_proposal: Observable = await api.subscribe({"proposal": 1, "amount": 10, "barrier": "+0.1", 26 | "basis": "payout", 27 | "contract_type": "CALL", "currency": "USD", "duration": 160, 28 | "duration_unit": "s", 29 | "symbol": "R_100" 30 | }) 31 | source_proposal.subscribe() 32 | await asyncio.sleep(5) 33 | 34 | 35 | # Buy contract 36 | async def buy_proposal(api): 37 | proposal = await api.expect_response('proposal') 38 | if proposal.get('proposal').get('payout') >= expected_payout: 39 | proposal_id = proposal.get('proposal').get('id') 40 | # buy contract 41 | buy = await api.buy({"buy": proposal_id, "price": 10}) 42 | contract_id = buy.get('buy').get('contract_id') 43 | 44 | # open contract stream 45 | source_poc: Observable = await api.subscribe({"proposal_open_contract": 1, "contract_id": contract_id}) 46 | source_poc.subscribe(lambda poc: print(poc)) 47 | await asyncio.sleep(10) 48 | 49 | await api.forget(proposal.get('subscription').get('id')) 50 | 51 | loop = asyncio.get_event_loop() 52 | asyncio.run(sample_calls()) 53 | loop.run_forever() 54 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = tests 3 | asyncio_mode=auto 4 | -------------------------------------------------------------------------------- /scripts/regen-py.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | 3 | use strict; 4 | use warnings; 5 | 6 | use Dir::Self; 7 | use File::Basename qw( basename dirname ); 8 | use JSON::MaybeXS; 9 | use Log::Any qw($log); 10 | use Path::Tiny; 11 | use Template; 12 | use Syntax::Keyword::Try; 13 | 14 | 15 | my $json = JSON::MaybeXS->new( 16 | canonical => 1, 17 | pretty => 1, 18 | indent_length => 4 19 | ); 20 | 21 | my $distroot = dirname(__DIR__); 22 | my $api_calls_filename = "$distroot/deriv_api/deriv_api_calls.py"; 23 | my $streams_list_filename = "$distroot/deriv_api/streams_list.py"; 24 | 25 | chomp(my $date = qx( date +%Y%m%d-%H%M%S )); 26 | 27 | my @methods; 28 | 29 | sub emit_functions { 30 | my ($root) = @_; 31 | 32 | $root = path($root); 33 | 34 | # Helper for reporting on anything in the schema we can't handle 35 | my $check = sub { 36 | my $def = shift; 37 | die "non-object def - " . $def->{type} unless $def->{type} eq 'object'; 38 | die "unexpected schema " . $def->{'$schema'} unless $def->{'$schema'} =~ m{http://json-schema.org/draft-0[34]/schema#}; 39 | }; 40 | 41 | # Expected path structure is $methodname/{send,receive}.json 42 | foreach my $dir (sort $root->children) { 43 | my $method = $dir->basename; 44 | 45 | $log->tracef("Applying method %s", $method); 46 | 47 | my ($send, $recv) = map { 48 | try { 49 | my $def = $json->decode(path($dir, $_ . '.json')->slurp_utf8); 50 | $check->($def); 51 | $def; 52 | } 53 | catch ($e) { 54 | die "$method $_ load/validation failed: $e" 55 | } 56 | } qw(send receive); 57 | 58 | # NOTE: Some type definitions use arrays, we don't support that yet 59 | 60 | my $send_props = parse_properties($send); 61 | chomp(my $encoded_props = $json->encode($send_props)); 62 | 63 | push @methods, { 64 | # the real api method name e.g. proposal_open_contract 65 | method => $method, 66 | # Any request that supports req_id will automatically get an ID and this will be used 67 | # in determining which response is which. 68 | has_req_id => exists $send_props->{req_id}, 69 | needs_method_arg => needs_method_arg($method, $send_props), 70 | description => $send->{description}, 71 | is_method => exists $send_props->{$method}, 72 | encoded_props => $encoded_props =~ s/"/'/rg =~ s/\n/\n /rg =~ s/ :/:/rg, 73 | props => parse_properties($send, full => 1), 74 | }; 75 | } 76 | } 77 | 78 | sub parse_properties { 79 | my $schema = shift; 80 | my %options = @_; 81 | my $props = $schema->{properties}; 82 | my @required = ($schema->{required} || [])->@*; 83 | my %data; 84 | for my $prop (keys %$props) { 85 | if (exists $props->{$prop}{properties}) { 86 | $data{$prop} = parse_properties($props->{$prop}); 87 | } else { 88 | my $type; 89 | $type = 'numeric' if ($props->{$prop}{type} || '') =~ /^(?:number)$/; 90 | $type = 'integer' if ($props->{$prop}{type} || '') =~ /^(?:integer)$/; 91 | $type = $1 if ($props->{$prop}{type} || '') =~ /^(string|boolean)$/; 92 | my $description = $props->{$prop}->%{description}; 93 | $description =~ s/`//g if $description; 94 | $data{$prop} = { 95 | $type ? (type => $type) : (), 96 | (grep { /^$prop$/ } @required) ? (required => 1) : (), 97 | $options{full} ? (description => $description) : (), 98 | }; 99 | } 100 | } 101 | return \%data; 102 | } 103 | 104 | sub to_camel_case { shift =~ s/_(\w)/\U$1/rg } 105 | 106 | sub needs_method_arg { 107 | my ($method, $send_props) = @_; 108 | 109 | # { "method": { "type": "integer", "enum": [1] } } 110 | return 1 unless my $enum = $send_props->{$method}{enum}; 111 | 112 | return 0 if scalar($enum->@*) == 1 and $enum->[0] == 1; 113 | 114 | return 1; 115 | } 116 | 117 | emit_functions($ENV{BINARYCOM_API_SCHEMA_PATH} // '/home/git/deriv-com/deriv-api-docs/config/v3'); 118 | 119 | my $template = Template->new( 120 | INCLUDE_PATH => "$distroot/scripts/templates", 121 | ); 122 | 123 | $template->process( 124 | "api-call-py.tt2", 125 | { 126 | scriptname => $0, 127 | date => $date, 128 | methods => \@methods, 129 | }, \my $output) or die $template->error . "\n"; 130 | 131 | path($api_calls_filename)->spew_utf8($output); 132 | 133 | $output = ''; 134 | $template->process( 135 | 'streams_list.py.tt2', 136 | { 137 | scriptname => $0, 138 | date => $date, 139 | methods => \@methods, 140 | }, \$output 141 | ) or die $template->error . "\n"; 142 | 143 | path($streams_list_filename)->spew_utf8($output); 144 | -------------------------------------------------------------------------------- /scripts/templates/api-call-py.tt2: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by [% scriptname %] at [% date %] 2 | [%# Convert JSON schema API definition into a Python class %] 3 | 4 | from numbers import Number 5 | 6 | # ======================= 7 | # ----- API Methods ----- 8 | # ======================= 9 | 10 | 11 | class DerivAPICalls: 12 | [% FOREACH m IN methods -%] 13 | 14 | async def [% m.method %](self, args=None): 15 | """ 16 | [% m.description %] 17 | 18 | Parameters: 19 | ----------- 20 | args : dict [% IF m.props %]with following keys[% END %] 21 | [% FOREACH p IN m.props -%] 22 | [% type = p.value.type -%] 23 | [% SWITCH type -%] 24 | [% CASE 'integer' -%] 25 | [% type = 'int' -%] 26 | [% CASE 'numeric' -%] 27 | [% type = 'Number' -%] 28 | [% CASE 'string' -%] 29 | [% type = 'str' -%] 30 | [% CASE 'boolean' -%] 31 | [% type = 'bool' -%] 32 | [% CASE -%] 33 | [% type = 'Any' -%] 34 | [% END -%] 35 | [% p.key %] : [% type %] 36 | [% IF p.value.description -%] 37 | [% p.value.description %] 38 | [% END -%] 39 | [% END -%] 40 | """ 41 | 42 | if args is None: 43 | args = {} 44 | 45 | config = [% m.encoded_props %] 46 | 47 | all_args = { 48 | 'method': '[% m.method %]', 49 | 'needs_method_arg': '[% m.needs_method_arg %]', 50 | 'args': args, 51 | 'config': config, 52 | } 53 | 54 | return await self.process_request(all_args) 55 | [% END -%] 56 | 57 | async def process_request(self, all_args): 58 | """ 59 | Process request 60 | """ 61 | 62 | config = all_args['config'] 63 | parsed_args = parse_args(all_args) 64 | error = validate_args(config=config, args=parsed_args) 65 | if error: 66 | raise ValueError(error) 67 | return await self.send(parsed_args) 68 | 69 | __pdoc__ = { 70 | 'parse_args' : False, 71 | 'validate_args' : False, 72 | 'deriv_api.deriv_api_calls.DerivAPICalls.process_request' : False 73 | } 74 | 75 | def parse_args(all_args): 76 | """ 77 | Parse request args 78 | """ 79 | 80 | parsed_args = all_args['args'] 81 | method = all_args['method'] 82 | 83 | if all_args['needs_method_arg'] and not(isinstance(parsed_args, dict)): 84 | parsed_args = {method: parsed_args} 85 | 86 | parsed_args[method] = parsed_args.get(method, 1) 87 | 88 | config = all_args['config'] 89 | for param in parsed_args: 90 | value = parsed_args[param] 91 | if not (param in config): 92 | return 93 | 94 | ptype = config[param].get('type') 95 | if ptype and ptype == 'string': 96 | parsed_args[param] = f'{value}' 97 | elif ptype and (ptype == 'numeric' or ptype == 'boolean'): 98 | parsed_args[param] = int(float(value)) 99 | 100 | return parsed_args 101 | 102 | 103 | type_checkers = { 104 | 'dict': lambda value: isinstance(value, dict), 105 | 'numeric': lambda value: isinstance(value, Number), 106 | 'string': lambda value: isinstance(value, str), 107 | 'boolean': lambda value: value in [True, False, 0, 1], 108 | 'integer': lambda value: isinstance(value, int) 109 | } 110 | 111 | 112 | def validate_args(config, args): 113 | """ 114 | Validate request args 115 | """ 116 | 117 | if not isinstance(args, dict): 118 | return f"Requires an dict but a {type(args)} is passed." 119 | 120 | error_messages = [] 121 | missing = [k for k in config.keys() if (config.get(k) or {}).get('required') and not (k in args)] 122 | if len(missing): 123 | error_messages.append(f'Required parameters missing: {", ".join(missing)}') 124 | 125 | for param in args.keys(): 126 | value = args[param] 127 | if param not in config: 128 | continue 129 | expected_type = config[param].get('type') 130 | 131 | if not expected_type: 132 | continue 133 | 134 | checker = type_checkers.get(expected_type) 135 | if not checker or not checker(value): 136 | error_messages.append(f'{expected_type} value expected but found {type(value)}: {param}') 137 | 138 | return ' - '.join(error_messages) if len(error_messages) else '' 139 | -------------------------------------------------------------------------------- /scripts/templates/streams_list.py.tt2: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by [% scriptname %] at [% date %] 2 | 3 | # streams_list is the list of subscriptions msg_types available. 4 | # Please update it by [% scriptname %] 5 | # Refer https://developers.binary.com/ 6 | streams_list = [[% FOREACH m IN methods -%] 7 | [% FOREACH p IN m.props -%] 8 | [% IF p.key == 'subscribe' -%] '[% m.method -%]', [% END -%] 9 | [% END -%] 10 | [% END -%]] 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | from setuptools import find_packages, setup 3 | setup( 4 | name='python_deriv_api', 5 | packages=find_packages(include=['deriv_api']), 6 | version='0.1.7', 7 | description='Python bindings for deriv.com websocket API', 8 | author='Deriv Group Services Ltd', 9 | author_email='learning+python@deriv.com', 10 | license='MIT', 11 | install_requires=['websockets==10.3', 'reactivex==4.0.*'], 12 | url='https://github.com/deriv-com/python-deriv-api', 13 | project_urls={ 14 | 'Bug Tracker': "https://github.com/deriv-com/python-deriv-api/issues", 15 | 'Documentation': "https://deriv-com.github.io/python-deriv-api", 16 | 'Source Code': "https://github.com/deriv-com/python-deriv-api", 17 | 'Changelog': "https://github.com/deriv-com/python-deriv-api/blob/master/CHANGELOG.md" 18 | }, 19 | python_requires=">=3.9.6, !=3.9.7", 20 | classifiers=[ 21 | "Programming Language :: Python :: 3", 22 | "Operating System :: OS Independent", 23 | ], 24 | long_description_content_type="text/markdown", 25 | long_description=open('README.md').read() 26 | ) 27 | -------------------------------------------------------------------------------- /tests/test_cache.py: -------------------------------------------------------------------------------- 1 | from deriv_api.cache import Cache 2 | from deriv_api.errors import ConstructionError 3 | from deriv_api.in_memory import InMemory 4 | import pytest 5 | 6 | class Api: 7 | def __init__(self): 8 | self.seq = 0 9 | async def send(self, request): 10 | # seq will change every time send is called 11 | self.seq = self.seq + 1 12 | return {'request': request, 'seq': self.seq, 'msg_type': request['msg_type']} 13 | 14 | @pytest.mark.asyncio 15 | async def test_cache(): 16 | with pytest.raises(ConstructionError, match='Cache object needs an API to work'): 17 | Cache(None, None) 18 | api = Api(); 19 | cache = Cache(api, InMemory()) 20 | assert (await cache.send({'msg_type':"a message"})) == {'request': {'msg_type': 'a message'}, 'seq': 1, 'msg_type': 'a message'} , "api send is called first time" 21 | assert (await cache.send({'msg_type':"a message"})) == {'request': {'msg_type': 'a message'}, 'seq': 1, 'msg_type': 'a message'} , "date fetched from cache second time" 22 | -------------------------------------------------------------------------------- /tests/test_custom_future.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | import pytest 4 | from deriv_api.easy_future import EasyFuture 5 | from asyncio.exceptions import InvalidStateError, CancelledError 6 | import sys 7 | 8 | def test_custom_future(): 9 | f1 = EasyFuture() 10 | assert f1.is_pending() 11 | f1.resolve("hello") 12 | assert f1.result() == "hello" 13 | assert f1.is_resolved() 14 | with pytest.raises(InvalidStateError, match="invalid state"): 15 | f1.reject("world") 16 | f2 = EasyFuture() 17 | f2.reject(Exception) 18 | assert f2.is_rejected() 19 | 20 | @pytest.mark.asyncio 21 | async def test_wrap(): 22 | # test resolved 23 | f1 = asyncio.Future() 24 | f2 = EasyFuture.wrap(f1) 25 | assert isinstance(f2, EasyFuture) 26 | assert f1.get_loop() is f2.get_loop() 27 | assert f2.is_pending() 28 | f1.set_result("hello") 29 | await f2 30 | assert f2.result() == "hello" 31 | assert f2.done() 32 | 33 | # test reject 34 | f1 = asyncio.Future() 35 | f2 = EasyFuture.wrap(f1) 36 | f1.set_exception(Exception("hello")) 37 | with pytest.raises(Exception, match='hello'): 38 | await f2 39 | assert f2.done() 40 | assert f2.is_rejected() 41 | 42 | # test upstream cancel 43 | f1 = asyncio.Future() 44 | f2 = EasyFuture.wrap(f1) 45 | f1.cancel("hello") 46 | with pytest.raises(CancelledError, match='hello'): 47 | await f2 48 | assert f2.done() 49 | assert f2.is_cancelled() 50 | 51 | # test downstream cancel 52 | f1 = asyncio.Future() 53 | f2 = EasyFuture.wrap(f1) 54 | f2.cancel("hello") 55 | with pytest.raises(CancelledError, match='hello'): 56 | await f1 57 | assert f1.done() 58 | assert f1.cancelled() 59 | 60 | 61 | @pytest.mark.asyncio 62 | async def test_future_then(): 63 | # test upstream ok 64 | # test callback future ok 65 | f1 = EasyFuture() 66 | def then_callback(last_result): 67 | f = EasyFuture() 68 | f.set_result(f"result: {last_result}") 69 | return f 70 | f2 = f1.then(then_callback) 71 | f1.set_result("f1 ok") 72 | assert (await f2) == 'result: f1 ok', "if inside future has result, then_future will has result too" 73 | 74 | # test callback fail 75 | f1 = EasyFuture() 76 | 77 | def then_callback(last_result): 78 | f = EasyFuture() 79 | f.set_exception(Exception(f"result: {last_result}")) 80 | return f 81 | 82 | f2 = f1.then(then_callback) 83 | f1.set_result("f1 ok") 84 | with pytest.raises(Exception, match='result: f1 ok'): 85 | await f2 86 | 87 | # test upstream fail 88 | # test inside future ok 89 | f1 = EasyFuture() 90 | result = None 91 | 92 | def else_callback(last_exception: Exception): 93 | f = EasyFuture() 94 | f.set_result(f"f1 exception {last_exception.args[0]}") 95 | return f 96 | 97 | f2 = f1.catch(else_callback) 98 | f1.set_exception(Exception("f1 bad")) 99 | assert (await f2) == 'f1 exception f1 bad' 100 | 101 | # test inside future exception 102 | f1 = EasyFuture() 103 | result = None 104 | 105 | def else_callback(last_exception: Exception): 106 | f = EasyFuture() 107 | f.set_exception(Exception(f"f1 exception {last_exception.args[0]}")) 108 | return f 109 | 110 | f2 = f1.then(None, else_callback) 111 | f1.set_exception(Exception("f1 bad")) 112 | with pytest.raises(Exception, match='f1 exception f1 bad'): 113 | await f2 114 | 115 | # upstream cancelled 116 | f1 = EasyFuture() 117 | 118 | def else_callback(last_exception: Exception): 119 | f = EasyFuture() 120 | f.set_exception(Exception(f"f1 exception {last_exception.args[0]}")) 121 | return f 122 | 123 | f2 = f1.then(None, else_callback) 124 | f1.cancel('f1 cancelled') 125 | with pytest.raises(asyncio.exceptions.CancelledError, match='Upstream future cancelled'): 126 | await f2 127 | 128 | # callback future cancelled 129 | f1 = EasyFuture() 130 | 131 | def then_callback(result): 132 | f = EasyFuture() 133 | f.cancel(f"callback cancelled with f1 {result}") 134 | return f 135 | 136 | f2 = f1.then(then_callback) 137 | f1.set_result('f1 ok') 138 | with pytest.raises(asyncio.exceptions.CancelledError, match='callback cancelled with f1 f1 ok'): 139 | await f2 140 | 141 | # test no right call back 142 | f1 = EasyFuture() 143 | 144 | def else_callback(result): 145 | f = EasyFuture() 146 | f.cancel(f"f1 ok {result}") 147 | return f 148 | 149 | f2 = f1.then(None, else_callback) 150 | f1.set_result('f1 ok') 151 | assert (await f2) == 'f1 ok', 'If no suitable callback, then clone the result' 152 | 153 | def test_refcount(): 154 | # test then method 155 | f1 = EasyFuture() 156 | assert sys.getrefcount(f1) == 2, "new created future has 2 refcount" 157 | def then_cb(): 158 | return EasyFuture().resolve(True) 159 | def else_cb(): 160 | return EasyFuture().resolve(True) 161 | f1.then(then_cb(), else_cb) 162 | assert sys.getrefcount(f1) == 2, "after add then else db, future has 2 refcount" 163 | 164 | #test cascade method 165 | core_future = asyncio.get_event_loop().create_future() 166 | assert sys.getrefcount(core_future) == 2, "new created future has 2 refcount" 167 | custom_future = EasyFuture() 168 | custom_future.cascade(core_future) 169 | assert sys.getrefcount(core_future) == 2, "after cascade, core_future future has 2 refcount" 170 | assert sys.getrefcount(custom_future) == 3, "after cascade, custom future has 3 refcount" 171 | 172 | # test wrap method 173 | core_future = asyncio.get_event_loop().create_future() 174 | custom_future = EasyFuture.wrap(core_future) 175 | assert sys.getrefcount(core_future) == 2, "after cascade, core_future future has 2 refcount" 176 | assert sys.getrefcount(custom_future) == 3, "after cascade, custom future has 3 refcount" 177 | -------------------------------------------------------------------------------- /tests/test_deriv_api.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import pytest 3 | import pytest_mock 4 | import reactivex 5 | from websockets.frames import Close 6 | 7 | import deriv_api 8 | from deriv_api.errors import APIError, ConstructionError, ResponseError 9 | from deriv_api.easy_future import EasyFuture 10 | from reactivex.subject import Subject 11 | import reactivex.operators as op 12 | import pickle 13 | import json 14 | from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError 15 | 16 | from deriv_api.middlewares import MiddleWares 17 | 18 | 19 | class MockedWs: 20 | def __init__(self): 21 | self.data = [] 22 | self.called = {'send': [], 'recv' : []} 23 | self.slept_at = 0 24 | self.queue = Subject() 25 | self.req_res_map = {} 26 | async def build_queue(): 27 | while 1: 28 | await asyncio.sleep(0.01) 29 | # make queue 30 | for idx, d in enumerate(self.data): 31 | if d is None: 32 | continue 33 | await asyncio.sleep(0.01) 34 | try: 35 | self.queue.on_next(json.dumps(d)) 36 | except Exception as err: 37 | print(str(err)) 38 | # if subscription, then we keep it 39 | if not d.get('subscription'): 40 | self.data[idx] = None 41 | self.task_build_queue = asyncio.create_task(build_queue()) 42 | async def send(self, request): 43 | self.called['send'].append(request) 44 | request = json.loads(request) 45 | new_request = request.copy() 46 | # req_id will be generated by api automatically 47 | req_id = new_request.pop('req_id') 48 | key = pickle.dumps(new_request) 49 | response = self.req_res_map.get(key) 50 | if response: 51 | response['req_id'] = req_id 52 | self.data.append(response) 53 | self.req_res_map.pop(key) 54 | forget_id = request.get('forget') 55 | if forget_id: 56 | found = 0 57 | for idx, d in enumerate(self.data): 58 | if d is None: 59 | continue 60 | subscription_data = d.get('subscription') 61 | if subscription_data and subscription_data['id'] == forget_id: 62 | self.data[idx] = None 63 | found = 1 64 | break 65 | self.data.append({"echo_req": { 66 | 'req_id': req_id, 67 | 'forget': forget_id, 68 | }, 69 | 'forget': found, 70 | 'req_id': req_id, 71 | 'msg_type': 'forget' 72 | }) 73 | 74 | async def recv(self): 75 | self.called['recv'].append(None) 76 | data = await self.queue.pipe(op.first(),op.to_future()) 77 | return data 78 | 79 | def add_data(self,response): 80 | request = response['echo_req'].copy() 81 | # req_id will be added by api automatically 82 | # we remove it here for consistence 83 | request.pop('req_id', None) 84 | key = pickle.dumps(request) 85 | self.req_res_map[key] = response 86 | 87 | def clear(self): 88 | self.task_build_queue.cancel('end') 89 | 90 | def test_connect_parameter(): 91 | with pytest.raises(ConstructionError, match=r"An app_id is required to connect to the API"): 92 | deriv_api_obj = deriv_api.DerivAPI(endpoint=5432) 93 | 94 | with pytest.raises(ConstructionError, match=r"Endpoint must be a string, passed: "): 95 | deriv_api_obj = deriv_api.DerivAPI(app_id=1234, endpoint=5432) 96 | 97 | with pytest.raises(ConstructionError, match=r"Invalid URL:local123host"): 98 | deriv_api_obj = deriv_api.DerivAPI(app_id=1234, endpoint='local123host') 99 | 100 | @pytest.mark.asyncio 101 | async def test_deriv_api(mocker): 102 | mocker.patch('deriv_api.DerivAPI.api_connect', return_value='') 103 | api = deriv_api.DerivAPI(app_id=1234, endpoint='localhost') 104 | assert(isinstance(api, deriv_api.DerivAPI)) 105 | await asyncio.sleep(0.1) 106 | await api.clear() 107 | 108 | @pytest.mark.asyncio 109 | async def test_get_url(mocker): 110 | api = get_deriv_api(mocker) 111 | assert api.get_url("localhost") == "wss://localhost" 112 | assert api.get_url("ws://localhost") == "ws://localhost" 113 | with pytest.raises(ConstructionError, match=r"Invalid URL:testurl"): 114 | api.get_url("testurl") 115 | await asyncio.sleep(0.1) 116 | await api.clear() 117 | 118 | def get_deriv_api(mocker): 119 | mocker.patch('deriv_api.DerivAPI.api_connect', return_value=EasyFuture().set_result(1)) 120 | api = deriv_api.DerivAPI(app_id=1234, endpoint='localhost') 121 | return api 122 | 123 | @pytest.mark.asyncio 124 | async def test_mocked_ws(): 125 | wsconnection = MockedWs() 126 | data1 = {"echo_req":{"ticks" : 'R_50', 'req_id': 1} ,"msg_type": "ticks", "req_id": 1, "subscription": {"id": "world"}} 127 | data2 = {"echo_req":{"ping": 1, 'req_id': 2},"msg_type": "ping", "pong": 1, "req_id": 2} 128 | wsconnection.add_data(data1) 129 | wsconnection.add_data(data2) 130 | await wsconnection.send(json.dumps(data1["echo_req"])) 131 | await wsconnection.send(json.dumps(data2["echo_req"])) 132 | assert json.loads(await wsconnection.recv()) == data1, "we can get first data" 133 | assert json.loads(await wsconnection.recv()) == data2, "we can get second data" 134 | assert json.loads(await wsconnection.recv()) == data1, "we can still get first data becaues it is a subscription" 135 | assert json.loads(await wsconnection.recv()) == data1, "we will not get second data because it is not a subscription" 136 | assert len(wsconnection.called['send']) == 2 137 | assert len(wsconnection.called['recv']) == 4 138 | wsconnection.clear() 139 | 140 | @pytest.mark.asyncio 141 | async def test_simple_send(): 142 | wsconnection = MockedWs() 143 | api = deriv_api.DerivAPI(connection = wsconnection) 144 | data1 = {"echo_req":{"ping": 1},"msg_type": "ping", "pong": 1} 145 | data2 = {"echo_req":{"ticks" : 'R_50'} ,"msg_type": "ticks"} 146 | wsconnection.add_data(data1) 147 | wsconnection.add_data(data2) 148 | res1 = data1.copy() 149 | add_req_id(res1, 1) 150 | res2 = data2.copy() 151 | add_req_id(res2, 2) 152 | assert await api.send(data1['echo_req']) == res1 153 | assert await api.ticks(data2['echo_req']) == res2 154 | assert len(wsconnection.called['send']) == 2 155 | wsconnection.clear() 156 | await api.clear() 157 | 158 | @pytest.mark.asyncio 159 | async def test_middleware(): 160 | wsconnection = MockedWs() 161 | send_will_be_called_args = None 162 | send_is_called_args = None 163 | send_will_be_called_return = None 164 | send_is_called_return = None 165 | def send_will_be_called(args): 166 | nonlocal send_will_be_called_args 167 | send_will_be_called_args = args 168 | nonlocal send_will_be_called_return 169 | return send_will_be_called_return 170 | 171 | def send_is_called(args): 172 | nonlocal send_is_called_args 173 | send_is_called_args = args 174 | nonlocal send_is_called_return 175 | return send_is_called_return 176 | 177 | api = deriv_api.DerivAPI(connection = wsconnection, middlewares = MiddleWares({'sendWillBeCalled': send_will_be_called, 'sendIsCalled': send_is_called})) 178 | req1 = {"ping": 2} 179 | 180 | # test sendWillBeCalled return true value, sendIsCalled will be not called and the request will not be sent to ws 181 | data1 = {"echo_req":req1,"msg_type": "ping", "pong": 1,} 182 | wsconnection.add_data(data1) 183 | send_will_be_called_return = {'value': 1} # middleware sendWillBeCalled will return a dict 184 | response = await api.send(req1) 185 | assert response == send_will_be_called_return # api will return the value of sendWillBeCalled returned 186 | assert send_will_be_called_args == {'request': req1} 187 | assert len(wsconnection.called['send']) == 0 # ws send is not called 188 | 189 | # test sendWillBeCalled return false value, ws send will be called and sendIsCalled will be called 190 | send_will_be_called_return = None 191 | send_is_called_return = None # middleware sendIsCalled will return false 192 | response = await api.send(req1) 193 | expected_response = data1.copy() 194 | expected_response['req_id'] = 1 195 | assert response == expected_response # response is the value that ws returned 196 | assert send_is_called_args == {'response': expected_response, 'request': req1} 197 | 198 | # test sendWillBeCalled return false , and sendIsCalled return a true value 199 | send_is_called_return = {'value': 2} 200 | wsconnection.add_data(data1) 201 | response = await api.send(req1) 202 | assert response == {'value': 2} # will get what sendIsCalled return 203 | 204 | wsconnection.clear() 205 | await api.clear() 206 | 207 | @pytest.mark.asyncio 208 | async def test_subscription(): 209 | wsconnection = MockedWs() 210 | api = deriv_api.DerivAPI(connection=wsconnection) 211 | r50_data = { 212 | 'echo_req': {'ticks': 'R_50', 'subscribe': 1}, 213 | 'msg_type': 'tick', 214 | 'subscription': {'id': 'A11111'} 215 | } 216 | r100_data = { 217 | 'echo_req': {'ticks': 'R_100', 'subscribe': 1}, 218 | 'msg_type': 'tick', 219 | 'subscription': {'id': 'A22222'} 220 | } 221 | wsconnection.add_data(r50_data) 222 | wsconnection.add_data(r100_data) 223 | r50_req = r50_data['echo_req'] 224 | r50_req.pop('subscribe'); 225 | r100_req = r100_data['echo_req'] 226 | r100_req.pop('subscribe'); 227 | sub1 = await api.subscribe(r50_req) 228 | sub2 = await api.subscribe(r100_req) 229 | f1 = sub1.pipe(op.take(2), op.to_list(), op.to_future()) 230 | f2 = sub2.pipe(op.take(2), op.to_list(), op.to_future()) 231 | result = await asyncio.gather(f1, f2) 232 | assert result == [[r50_data, r50_data], [r100_data, r100_data]] 233 | await asyncio.sleep(0.01) # wait sending 'forget' finished 234 | assert wsconnection.called['send'] == [ 235 | '{"ticks": "R_50", "subscribe": 1, "req_id": 1}', 236 | '{"ticks": "R_100", "subscribe": 1, "req_id": 2}', 237 | '{"forget": "A11111", "req_id": 3}', 238 | '{"forget": "A22222", "req_id": 4}'] 239 | wsconnection.clear() 240 | await api.clear() 241 | 242 | @pytest.mark.asyncio 243 | async def test_forget(): 244 | wsconnection = MockedWs() 245 | api = deriv_api.DerivAPI(connection=wsconnection) 246 | # test subscription forget will mark source done 247 | r50_data = { 248 | 'echo_req': {'ticks': 'R_50', 'subscribe': 1}, 249 | 'msg_type': 'tick', 250 | 'subscription': {'id': 'A11111'} 251 | } 252 | wsconnection.add_data(r50_data) 253 | r50_req = r50_data['echo_req'] 254 | r50_req.pop('subscribe'); 255 | sub1: rx.Observable = await api.subscribe(r50_req) 256 | complete = False 257 | 258 | def on_complete(): 259 | nonlocal complete 260 | complete = True 261 | 262 | sub1.subscribe(on_completed=on_complete) 263 | await asyncio.sleep(0.1) 264 | assert not complete, 'subscription not stopped' 265 | await api.forget('A11111') 266 | await asyncio.sleep(0.1) 267 | assert complete, 'subscription stopped after forget' 268 | wsconnection.clear() 269 | await api.clear() 270 | 271 | 272 | @pytest.mark.asyncio 273 | async def test_extra_response(): 274 | wsconnection = MockedWs() 275 | api = deriv_api.DerivAPI(connection=wsconnection) 276 | error = None 277 | async def get_sanity_error(): 278 | nonlocal error 279 | error = await api.sanity_errors.pipe(op.first(),op.to_future()) 280 | error_task = asyncio.create_task(get_sanity_error()) 281 | wsconnection.data.append({"hello":"world"}) 282 | try: 283 | await asyncio.wait_for(error_task, timeout=0.1) 284 | assert str(error) == 'APIError:Extra response' 285 | except asyncio.exceptions.TimeoutError: 286 | assert False, "error data apppear timeout " 287 | wsconnection.clear() 288 | await api.clear() 289 | 290 | @pytest.mark.asyncio 291 | async def test_response_error(): 292 | wsconnection = MockedWs() 293 | api = deriv_api.DerivAPI(connection=wsconnection) 294 | r50_data = { 295 | 'echo_req': {'ticks': 'R_50', 'subscribe': 1}, 296 | 'msg_type': 'tick', 297 | 'error': {'code': 'TestError', 'message': 'test error message'} 298 | } 299 | wsconnection.add_data(r50_data) 300 | sub1 = await api.subscribe(r50_data['echo_req']) 301 | f1 = sub1.pipe(op.first(), op.to_future()) 302 | with pytest.raises(ResponseError, match='ResponseError: test error message'): 303 | await f1 304 | r50_data = { 305 | 'echo_req': {'ticks': 'R_50', 'subscribe': 1}, 306 | 'msg_type': 'tick', 307 | 'req_id': f1.exception().req_id, 308 | 'subscription': {'id': 'A111111'} 309 | } 310 | wsconnection.data.append(r50_data) # add back r50 again 311 | #will send a `forget` if get a response again 312 | await asyncio.sleep(0.1) 313 | assert wsconnection.called['send'][-1] == '{"forget": "A111111", "req_id": 2}' 314 | poc_data = { 315 | 'echo_req': {'proposal_open_contract': 1, 'subscribe': 1}, 316 | 'msg_type': 'proposal_open_contract', 317 | 'error': {'code': 'TestError', 'message': 'test error message'}, 318 | 'subscription': {'id': 'ABC11111'} 319 | } 320 | wsconnection.add_data(poc_data) 321 | sub1 = await api.subscribe(poc_data['echo_req']) 322 | response = await sub1.pipe(op.first(), op.to_future()) 323 | assert 'error' in response, "for the poc stream with out contract_id, the error response will not terminate the stream" 324 | wsconnection.clear() 325 | await api.clear() 326 | 327 | @pytest.mark.asyncio 328 | async def test_cache(): 329 | wsconnection = MockedWs() 330 | api = deriv_api.DerivAPI(connection=wsconnection) 331 | wsconnection.add_data({'ping':'pong', 'msg_type': 'ping', 'echo_req' : {'ping': 1}}) 332 | ping1 = await api.ping({'ping': 1}) 333 | assert len(wsconnection.called['send']) == 1 334 | ping2 = await api.expect_response('ping') 335 | assert len(wsconnection.called['send']) == 1, 'send can cache value for expect_response. get ping2 from cache, no send happen' 336 | assert ping1 == ping2, "ping2 is ping1 " 337 | ping3 = await api.cache.ping({'ping': 1}) 338 | assert len(wsconnection.called['send']) == 1, 'get ping3 from cache, no send happen' 339 | assert ping1 == ping3, "ping3 is ping1 " 340 | wsconnection.clear() 341 | await api.clear() 342 | 343 | wsconnection = MockedWs() 344 | api = deriv_api.DerivAPI(connection=wsconnection) 345 | wsconnection.add_data({'ping': 'pong', 'msg_type': 'ping', 'echo_req': {'ping': 1}}) 346 | ping1 = await api.cache.ping({'ping': 1}) 347 | assert len(wsconnection.called['send']) == 1 348 | ping2 = await api.expect_response('ping') 349 | assert len(wsconnection.called['send']) == 1, 'api.cache.ping can cache value. get ping2 from cache, no send happen' 350 | assert ping1 == ping2, "ping2 is ping1 " 351 | wsconnection.clear() 352 | await api.clear() 353 | 354 | @pytest.mark.asyncio 355 | async def test_can_subscribe_one_source_many_times(): 356 | wsconnection = MockedWs() 357 | api = deriv_api.DerivAPI(connection=wsconnection) 358 | r50_data = { 359 | 'echo_req': {'ticks': 'R_50', 'subscribe': 1}, 360 | 'msg_type': 'tick', 361 | 'subscription': {'id': 'A11111'} 362 | } 363 | wsconnection.add_data(r50_data) 364 | r50_req = r50_data['echo_req'] 365 | r50_req.pop('subscribe'); 366 | sub1 = await api.subscribe(r50_req) 367 | f1 = sub1.pipe(op.take(2), op.to_list(), op.to_future()) 368 | f2 = sub1.pipe(op.take(2), op.to_list(), op.to_future()) 369 | result = await asyncio.gather(f1,f2) 370 | assert result == [[r50_data, r50_data],[r50_data, r50_data]] 371 | await asyncio.sleep(0.01) # wait sending 'forget' finished 372 | assert wsconnection.called['send'] == [ 373 | '{"ticks": "R_50", "subscribe": 1, "req_id": 1}', 374 | '{"forget": "A11111", "req_id": 2}'] 375 | wsconnection.clear() 376 | await api.clear() 377 | 378 | @pytest.mark.asyncio 379 | async def test_reuse_poc_stream(): 380 | wsconnection = MockedWs() 381 | api = deriv_api.DerivAPI(connection=wsconnection) 382 | buy_data = {'echo_req': {'buy': 1, 'subscribe': 1}, 383 | 'subscription': {'id': 'B111111'}, 384 | 'buy': {'contract_id': 1234567}, 385 | 'msg_type': 'proposal_open_contract' 386 | } 387 | wsconnection.add_data(buy_data) 388 | sub1 = await api.subscribe(buy_data['echo_req']) 389 | await asyncio.sleep(0.1) # wait for setting reused stream 390 | sub2 = await api.subscribe({'proposal_open_contract': 1, 'contract_id': 1234567}) 391 | assert id(sub1) == id(sub2) 392 | assert len(api.subscription_manager.buy_key_to_contract_id) == 1 393 | await api.forget('B111111') 394 | assert len(api.subscription_manager.buy_key_to_contract_id) == 0 395 | wsconnection.clear() 396 | await api.clear() 397 | 398 | @pytest.mark.asyncio 399 | async def test_expect_response(): 400 | wsconnection = MockedWs() 401 | api = deriv_api.DerivAPI(connection=wsconnection) 402 | wsconnection.add_data({'ping':'pong', 'msg_type': 'ping', 'echo_req' : {'ping': 1}}) 403 | get_ping = api.expect_response('ping') 404 | assert not get_ping.done(), 'get ping is a future and is pending' 405 | ping_result = await api.ping({'ping': 1}) 406 | assert get_ping.done(), 'get ping done' 407 | assert ping_result == await get_ping 408 | wsconnection.clear() 409 | await api.clear() 410 | 411 | @pytest.mark.asyncio 412 | async def test_ws_disconnect(): 413 | class MockedWs2(MockedWs): 414 | def __init__(self): 415 | self.closed = EasyFuture() 416 | self.exception = ConnectionClosedOK(Close(1000, 'test disconnect'), None, None) 417 | super().__init__() 418 | async def close(self): 419 | self.closed.resolve(self.exception) 420 | pass 421 | async def send(self): 422 | exc = await self.closed 423 | raise exc 424 | async def recv(self): 425 | exc = await self.closed 426 | raise exc 427 | 428 | # closed by api 429 | wsconnection = MockedWs2() 430 | wsconnection.exception = ConnectionClosedOK(Close(1000, 'Closed by api'), None, None) 431 | api = deriv_api.DerivAPI(connection=wsconnection) 432 | await asyncio.sleep(0.1) 433 | api.wsconnection_from_inside = True 434 | last_error = api.sanity_errors.pipe(op.first(), op.to_future()) 435 | await asyncio.sleep(0.1) # waiting for init finished 436 | await api.disconnect() # it will set connected as 'Closed by disconnect', and cause MockedWs2 raising `test disconnect` 437 | assert isinstance((await last_error), ConnectionClosedOK), 'sanity error get errors' 438 | with pytest.raises(ConnectionClosedOK, match='Closed by disconnect'): 439 | await api.send({'ping': 1}) # send will get same error 440 | with pytest.raises(ConnectionClosedOK, match='Closed by disconnect'): 441 | await api.connected # send will get same error 442 | wsconnection.clear() 443 | await api.clear() 444 | 445 | # closed by remote 446 | wsconnection = MockedWs2() 447 | api = deriv_api.DerivAPI(connection=wsconnection) 448 | wsconnection.exception = ConnectionClosedError(Close(1234, 'Closed by remote'), None, None) 449 | last_error = api.sanity_errors.pipe(op.first(), op.to_future()) 450 | await asyncio.sleep(0.1) # waiting for init finished 451 | await wsconnection.close() # it will set connected as 'Closed by disconnect', and cause MockedWs2 raising `test disconnect` 452 | assert isinstance((await last_error), ConnectionClosedError), 'sanity error get errors' 453 | with pytest.raises(ConnectionClosedError, match='Closed by remote'): 454 | await api.send({'ping': 1}) # send will get same error 455 | with pytest.raises(ConnectionClosedError, match='Closed by remote'): 456 | await api.connected # send will get same error 457 | wsconnection.clear() 458 | await api.clear() 459 | 460 | @pytest.mark.asyncio 461 | async def test_add_task(): 462 | wsconnection = MockedWs() 463 | api = deriv_api.DerivAPI(connection=wsconnection) 464 | exception_f = api.sanity_errors.pipe(op.first(), op.to_future()) 465 | async def raise_an_exception(): 466 | raise Exception("test add_task") 467 | api.add_task(raise_an_exception(), 'raise an exception') 468 | exception = await exception_f 469 | assert str(exception) == 'deriv_api:raise an exception: test add_task' 470 | await api.clear() 471 | 472 | def add_req_id(response, req_id): 473 | response['echo_req']['req_id'] = req_id 474 | response['req_id'] = req_id 475 | return response 476 | 477 | @pytest.mark.asyncio 478 | async def test_eventgs(): 479 | wsconnection = MockedWs() 480 | api = deriv_api.DerivAPI(connection=wsconnection) 481 | event_data = [] 482 | 483 | def on_next(data): 484 | nonlocal event_data 485 | event_data.append(data) 486 | 487 | api.events.subscribe(on_next=on_next ) 488 | wsdata = {'ping': 'pong', 'msg_type': 'ping', 'echo_req': {'ping': 1}} 489 | wsconnection.add_data(wsdata) 490 | await api.ping({'ping': 1}) 491 | assert event_data == [{'name': 'send', 'data': {'ping': 1, 'req_id': 1}}, {'name': 'message', 'data': wsdata}] 492 | wsconnection.clear() 493 | await api.clear() 494 | -------------------------------------------------------------------------------- /tests/test_deriv_api_calls.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import re 3 | from deriv_api.deriv_api_calls import DerivAPICalls, parse_args, validate_args 4 | 5 | class DerivedDerivAPICalls(DerivAPICalls): 6 | async def send(self, args): 7 | return args 8 | 9 | @pytest.mark.asyncio 10 | async def test_deriv_api_calls(mocker): 11 | api = DerivedDerivAPICalls() 12 | assert isinstance(api, DerivAPICalls) 13 | assert (await api.exchange_rates({'exchange_rates': 1, 'base_currency': 'USD'})) == {'exchange_rates': 1, 14 | 'base_currency': 'USD'}, 'exchange_rates can get right result' 15 | with pytest.raises(ValueError, match='Required parameters missing: base_currency'): 16 | await api.exchange_rates({}) 17 | 18 | 19 | def test_parse_parse_args(): 20 | assert parse_args( 21 | {'config': {'acc': {'type': 'boolean'}}, 'args': '1', 'method': 'acc', 'needs_method_arg': 1}) == { 22 | 'acc': 1}, "method will be a key and arg will be value if arg is not a dict and needs_method_arg is true" 23 | assert parse_args( 24 | {'config': {'acc': {'type': 'boolean'}}, 'args': {'acc': '0'}, 'method': 'acc', 'needs_method_arg': 1}) == { 25 | 'acc': 0}, "method value will from args if arg is a dict and needs_method_arg is true" 26 | assert parse_args({'config': {'acc': {'type': 'string'}}, 'args': {'hello': 0}, 'method': 'acc', 27 | 'needs_method_arg': 1}) is None, "if arg is not in config, then return none" 28 | # test type 29 | assert parse_args( 30 | {'config': {'acc': {'type': 'string'}}, 'args': {'acc': 0}, 'method': 'acc', 'needs_method_arg': 1}) == { 31 | 'acc': '0'}, "arg is string" 32 | assert parse_args( 33 | {'config': {'acc': {'type': 'numeric'}}, 'args': {'acc': '0'}, 'method': 'acc', 'needs_method_arg': 1}) == { 34 | 'acc': 0}, "arg is numeric" 35 | assert parse_args( 36 | {'config': {'acc': {'type': 'boolean'}}, 'args': {'acc': '0'}, 'method': 'acc', 'needs_method_arg': 1}) == { 37 | 'acc': 0}, "arg is boolean" 38 | 39 | def test_validate_args(): 40 | assert re.match('Requires an dict',validate_args({},"")) 41 | assert validate_args({'k1': {'required': 1}, 'k2': {}}, {'k1': 1, 'k2': 2, 'k3': 3}) == '', 'required keys are there' 42 | error_msg = validate_args({'k1': {'required': 1}, 'k2': {'required': 1}}, {'k3': 1}) 43 | assert re.search('k1', error_msg) and re.search('k2', error_msg), 'missed keys will be reported' 44 | config = { 45 | 'k1': {'type': 'dict'}, 46 | 'k2': {'type': 'string'}, 47 | 'k3': {'type': 'numeric'}, 48 | 'k4': {'type': 'boolean'}, 49 | 'k5': {} 50 | } 51 | error_msg = validate_args(config, {'k1': 1, 'k2': 1, 'k3': 'aString', 'k4': 'aString'}) 52 | assert re.search("dict value expected but found : k1 ", error_msg) 53 | assert re.search("string value expected but found : k2", error_msg) 54 | assert re.search("numeric value expected but found : k3", error_msg) 55 | assert re.search("boolean value expected but found : k4", error_msg) 56 | error_msg = validate_args(config, {'k1': {}, 'k2': "string", 'k3': 1, 'k4': True, 'k5': 1}) 57 | assert error_msg == '' 58 | -------------------------------------------------------------------------------- /tests/test_errors.py: -------------------------------------------------------------------------------- 1 | from deriv_api.errors import APIError, ConstructionError 2 | 3 | def test_app_error_class(): 4 | error = APIError("A error") 5 | assert isinstance(error, Exception) 6 | assert f'{error}' == 'APIError:A error' 7 | 8 | def test_construction_error_class(): 9 | error = ConstructionError("A error") 10 | assert isinstance(error, Exception) 11 | assert f'{error}' == 'ConstructionError:A error' 12 | -------------------------------------------------------------------------------- /tests/test_in_memory.py: -------------------------------------------------------------------------------- 1 | from deriv_api.in_memory import InMemory 2 | 3 | 4 | def test_in_memory(): 5 | obj = InMemory() 6 | assert isinstance(obj, InMemory) 7 | obj.set('hello', {'msg_type': 'test_type', 'val': 123}) 8 | assert obj.has('hello') 9 | assert obj.get('hello')['val'] == 123 10 | assert obj.get_by_msg_type('test_type') == obj.get('hello') 11 | assert obj.has('no such key') is False 12 | -------------------------------------------------------------------------------- /tests/test_middlewares.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import re 3 | from deriv_api.middlewares import MiddleWares 4 | 5 | def test_middlewares(): 6 | middlewares = MiddleWares() 7 | isinstance(middlewares, MiddleWares) 8 | 9 | with pytest.raises(Exception, match = r"should be a string") as err: 10 | MiddleWares({123: lambda i: i+1}) 11 | 12 | with pytest.raises(Exception, match = r"should be a Callable") as err: 13 | MiddleWares({"name": 123}) 14 | 15 | with pytest.raises(Exception, match = r"not supported"): 16 | MiddleWares({"hello": lambda i: i}) 17 | 18 | call_args = [] 19 | def send_will_be_called(*args): 20 | nonlocal call_args 21 | call_args = args 22 | middlewares.add('sendWillBeCalled', send_will_be_called) 23 | middlewares.call('sendWillBeCalled', 123) 24 | assert call_args == (123,) -------------------------------------------------------------------------------- /tests/test_subscription_manager.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from deriv_api.subscription_manager import SubscriptionManager, get_msg_type 4 | from reactivex.subject import Subject 5 | from reactivex import Observable 6 | import asyncio 7 | from deriv_api.errors import APIError 8 | 9 | mocked_response = {} 10 | 11 | 12 | class API: 13 | def __init__(self): 14 | self.mocked_response = {} 15 | self.send_and_get_source_request = {} 16 | self.send_request = {} 17 | self.send_and_get_source_called = 0 18 | self.send_called = 0 19 | 20 | def send_and_get_source(self, request: dict) -> Subject: 21 | self.subject = Subject() 22 | self.send_and_get_source_called = self.send_and_get_source_called + 1 23 | self.send_and_get_source_request[self.send_and_get_source_called] = request 24 | return self.subject 25 | 26 | async def send(self, request: dict): 27 | self.send_called = self.send_called + 1 28 | self.send_request[self.send_called] = request 29 | return request 30 | 31 | async def emit(self): 32 | await asyncio.sleep(0.1) 33 | self.subject.on_next(self.mocked_response) 34 | 35 | def sanity_errors(self): 36 | class SanityErrors: 37 | def next(self, error): 38 | return error 39 | return SanityErrors() 40 | def add_task(self, task, name): 41 | asyncio.create_task(task, name=f"mocked_api:{name}") 42 | 43 | def test_get_msg_type(): 44 | assert get_msg_type({'hello': 1}) is None 45 | assert get_msg_type({'proposal': 1}) == 'proposal' 46 | 47 | 48 | @pytest.mark.asyncio 49 | async def test_subscribe(): 50 | api: API = API() 51 | subscription_manager = SubscriptionManager(api) 52 | subs_id = 'ID11111' 53 | api.mocked_response = {"msg_type": "proposal", 'subscription': {'id': subs_id}} 54 | assert not subscription_manager.source_exists({'proposal': 1}), "at start there is no such source" 55 | with pytest.raises(APIError, match='Subscription type is not found in deriv-api'): 56 | await subscription_manager.subscribe({"no such type"}) 57 | # get source first time 58 | source, emit = await asyncio.gather(subscription_manager.subscribe({'proposal': 1}), api.emit()) 59 | assert isinstance(source, Observable) 60 | assert api.send_and_get_source_called == 1 61 | assert api.send_and_get_source_request[1] == {'proposal': 1, 'subscribe': 1} 62 | assert api.send_request == {} 63 | # get source second time 64 | api.__init__() 65 | source2, emit = await asyncio.gather(subscription_manager.subscribe({'proposal': 1}), api.emit()) 66 | assert api.send_and_get_source_called == 0 67 | assert (source is source2), "same result" 68 | assert (source is subscription_manager.get_source({'proposal': 1})), 'source is in the cache' 69 | assert subscription_manager.source_exists({'proposal': 1}), "source in the cache" 70 | forget_result = await subscription_manager.forget(subs_id) 71 | assert api.send_called == 1 72 | assert forget_result == {'forget': subs_id} 73 | assert api.subject.is_disposed, "source is disposed" 74 | 75 | # test buy subscription 76 | api.__init__() 77 | subs_id = 'ID22222' 78 | api.mocked_response = { 79 | "buy": { 80 | "contract_id": 12345 81 | }, 82 | "msg_type": "buy", 83 | "subscription": { 84 | "id": subs_id 85 | } 86 | } 87 | 88 | request = { 89 | "buy": 1, 90 | "price": 100, 91 | } 92 | source, emit = await asyncio.gather(subscription_manager.subscribe(request), api.emit()) 93 | assert api.send_and_get_source_called == 1 , "send_and_get_source called once" 94 | assert isinstance(source, Observable) 95 | request = { 96 | 'proposal_open_contract': 1, 97 | 'contract_id': 12345 98 | } 99 | api.__init__() 100 | source2, emit = await asyncio.gather(subscription_manager.subscribe(request), api.emit()) 101 | assert api.send_and_get_source_called == 0 , "send_and_get_source not called" 102 | assert source is source2, '"buy" source and "proposal_open_contract" source are same one and cached' 103 | await subscription_manager.forget(subs_id) 104 | api.__init__() 105 | source2, emit = await asyncio.gather(subscription_manager.subscribe(request), api.emit()) 106 | assert api.send_and_get_source_called == 1 , "cache is cleared so the new call will get" 107 | assert source2 is not source, "new source is not the old one" 108 | 109 | @pytest.mark.asyncio 110 | async def test_forget_all(): 111 | api = API() 112 | subscription_manager = SubscriptionManager(api) 113 | result = await subscription_manager.forget_all("hello") 114 | assert result == {'forget_all': ["hello"]} 115 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | from deriv_api.utils import dict_to_cache_key 2 | import pickle 3 | 4 | 5 | def test_dict_to_cache_key(): 6 | assert(pickle.loads(dict_to_cache_key({"hello": "world", "subscribe": 1, "passthrough": 1, "req_id": 1})) == {"hello": "world"}) 7 | --------------------------------------------------------------------------------