├── .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 | [](https://pypi.org/project/python_deriv_api/)
5 | [](https://www.python.org/download/releases/3.9.6/)
6 | [](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}%def>
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 |
36 | Expand source code
37 | % if git_link:
38 | Browse git
39 | %endif
40 |
41 |
42 | ${d.source | h}
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 |
No modules found.
75 | % else: 76 |${link(item, item.name)}
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 |
${module.name}
${link(m)}
var ${ident(v.name)}${return_type}
189 | class ${ident(c.name)}
190 | % if params:
191 | (${params})
192 | % endif
193 |
var ${ident(v.name)}${return_type}
var ${ident(v.name)}${return_type}
${link(cls)}
:
260 | ${link(m, name=m.name)}