Skip to content

Commit dfa8737

Browse files
feat(client): add tests/ gift card API/ multiple fixes (sammchardy#1470)
* first commit * ruff check fix * ruff format fix * fix lint warnings and error * type fix for 3.7 * add testnet to test_Ws_api * change to testnet * ruff@ * return test to async in test * format with ruff * remove blank line * remove default tif * add ws_futures, refactor and add tests * lint * fix tests * ruff * add tests * remove utils * ruff format and pr comments * ruff format * fix live tests and add env vars for testnet * merge * github action * add tox env * fix and test without tox * move tox command * fix test * fix pyright * type ignore * jump test * jump test until whitelist * in progress * finish tests * remove print * add examples * improve docs * fix batch order * lint and format * lint and format * add tests for failed requests * fix for 3.7 * Add gift cards endpoints and tests' * run tests in parallel * run tests in parallel * reduce parallelism * run github action sequentially * change to 2 parallel * fix action * ruff format * delete empty file * skip dust assets test * add missing force params * add rerun to reduce flakiness * remove change * distribute tests over file * fix test * remove parallel test running * increase delay * update readme * improve get_asset_balance * add tests * reformat * tmp: continue on error * fix cancel test * ruff format * update headers * inject headers * add overridable headers * add headers tests * format * remove import * fix tests linting --------- Co-authored-by: carlosmiei <43336371+carlosmiei@users.noreply.github.com>
1 parent 81c2e10 commit dfa8737

27 files changed

+4258
-80
lines changed

.github/workflows/python-app.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,9 @@ jobs:
2626
run: pip install ruff
2727
- name: Lint code with Ruff
2828
run: ruff check --output-format=github --target-version=py39 .
29-
continue-on-error: false # TODO: delete once ruff errors are fixed
30-
# - name: Check code formatting with Ruff
31-
# run: ruff format --check .
32-
# continue-on-error: false # TODO: delete once ruff errors are fixed
29+
- name: Check code formatting with Ruff
30+
run: ruff format --check .
31+
continue-on-error: true
3332

3433
build:
3534
needs: lint
@@ -42,6 +41,7 @@ jobs:
4241
TEST_FUTURES_API_KEY: "227719da8d8499e8d3461587d19f259c0b39c2b462a77c9b748a6119abd74401"
4342
TEST_FUTURES_API_SECRET: "b14b935f9cfacc5dec829008733c40da0588051f29a44625c34967b45c11d73c"
4443
strategy:
44+
max-parallel: 1
4545
matrix:
4646
python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
4747
steps:

README.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ Features
6666
- Testnet support for Spot, Futures and Vanilla Options
6767
- Simple handling of authentication include RSA and EDDSA keys
6868
- No need to generate timestamps yourself, the wrapper does it for you
69+
- RecvWindow sent by default
6970
- Response exception handling
71+
- Customizable HTTP headers
7072
- Websocket handling with reconnection and multiplexed connections
7173
- CRUD over websockets, create/fetch/edit through websockets for minimum latency.
7274
- Symbol Depth Cache
@@ -80,6 +82,7 @@ Features
8082
- Proxy support (REST and WS)
8183
- Orjson support for faster JSON parsing
8284
- Support other domains (.us, .jp, etc)
85+
- Support for the Gift Card API
8386

8487
Upgrading to v1.0.0+
8588
--------------------
@@ -165,6 +168,9 @@ pass `testnet=True` when creating the client.
165168
# create order through websockets
166169
order_ws = client.ws_create_order( symbol="LTCUSDT", side="BUY", type="MARKET", quantity=0.1)
167170
171+
# get account using custom headers
172+
account = client.get_account(headers={'MyCustomKey': 'MyCustomValue'})
173+
168174
# socket manager using threads
169175
twm = ThreadedWebsocketManager()
170176
twm.start()

binance/async_client.py

Lines changed: 130 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import asyncio
22
from pathlib import Path
33
from typing import Any, Dict, List, Optional, Union
4-
from urllib.parse import urlencode
4+
from urllib.parse import urlencode, quote
55
import time
66
import aiohttp
77
import yarl
@@ -12,7 +12,12 @@
1212
BinanceRequestException,
1313
NotImplementedException,
1414
)
15-
from binance.helpers import convert_ts_str, get_loop, interval_to_milliseconds
15+
from binance.helpers import (
16+
convert_list_to_json_array,
17+
convert_ts_str,
18+
get_loop,
19+
interval_to_milliseconds,
20+
)
1621
from .base_client import BaseClient
1722
from .client import Client
1823

@@ -102,16 +107,32 @@ async def close_connection(self):
102107
async def _request(
103108
self, method, uri: str, signed: bool, force_params: bool = False, **kwargs
104109
):
110+
# this check needs to be done before __get_request_kwargs to avoid
111+
# polluting the signature
112+
headers = {}
113+
if method.upper() in ["POST", "PUT", "DELETE"]:
114+
headers.update({"Content-Type": "application/x-www-form-urlencoded"})
115+
116+
if "data" in kwargs:
117+
for key in kwargs["data"]:
118+
if key == "headers":
119+
headers.update(kwargs["data"][key])
120+
del kwargs["data"][key]
121+
break
122+
105123
kwargs = self._get_request_kwargs(method, signed, force_params, **kwargs)
106124

107-
if method == 'get':
125+
if method == "get":
108126
# url encode the query string
109-
if 'params' in kwargs:
127+
if "params" in kwargs:
110128
uri = f"{uri}?{kwargs['params']}"
111-
kwargs.pop('params')
129+
kwargs.pop("params")
112130

113131
async with getattr(self.session, method)(
114-
yarl.URL(uri, encoded=True), proxy=self.https_proxy, **kwargs
132+
yarl.URL(uri, encoded=True),
133+
proxy=self.https_proxy,
134+
headers=headers,
135+
**kwargs,
115136
) as response:
116137
self.response = response
117138
return await self._handle_response(response)
@@ -138,59 +159,64 @@ async def _request_api(
138159
**kwargs,
139160
):
140161
uri = self._create_api_uri(path, signed, version)
141-
return await self._request(method, uri, signed, **kwargs)
162+
force_params = kwargs.pop("force_params", False)
163+
return await self._request(method, uri, signed, force_params, **kwargs)
142164

143165
async def _request_futures_api(
144166
self, method, path, signed=False, version=1, **kwargs
145167
) -> Dict:
146168
version = self._get_version(version, **kwargs)
147169
uri = self._create_futures_api_uri(path, version=version)
148-
149-
return await self._request(method, uri, signed, False, **kwargs)
170+
force_params = kwargs.pop("force_params", False)
171+
return await self._request(method, uri, signed, force_params, **kwargs)
150172

151173
async def _request_futures_data_api(
152174
self, method, path, signed=False, **kwargs
153175
) -> Dict:
154176
uri = self._create_futures_data_api_uri(path)
155-
156-
return await self._request(method, uri, signed, True, **kwargs)
177+
force_params = kwargs.pop("force_params", True)
178+
return await self._request(method, uri, signed, force_params, **kwargs)
157179

158180
async def _request_futures_coin_api(
159181
self, method, path, signed=False, version=1, **kwargs
160182
) -> Dict:
161183
version = self._get_version(version, **kwargs)
162184
uri = self._create_futures_coin_api_url(path, version=version)
163-
164-
return await self._request(method, uri, signed, False, **kwargs)
185+
force_params = kwargs.pop("force_params", False)
186+
return await self._request(method, uri, signed, force_params, **kwargs)
165187

166188
async def _request_futures_coin_data_api(
167189
self, method, path, signed=False, version=1, **kwargs
168190
) -> Dict:
169191
version = self._get_version(version, **kwargs)
170192
uri = self._create_futures_coin_data_api_url(path, version=version)
171193

172-
return await self._request(method, uri, signed, True, **kwargs)
194+
force_params = kwargs.pop("force_params", True)
195+
return await self._request(method, uri, signed, force_params, **kwargs)
173196

174197
async def _request_options_api(self, method, path, signed=False, **kwargs) -> Dict:
175198
uri = self._create_options_api_uri(path)
199+
force_params = kwargs.pop("force_params", True)
176200

177-
return await self._request(method, uri, signed, True, **kwargs)
201+
return await self._request(method, uri, signed, force_params, **kwargs)
178202

179203
async def _request_margin_api(
180204
self, method, path, signed=False, version=1, **kwargs
181205
) -> Dict:
182206
version = self._get_version(version, **kwargs)
183207
uri = self._create_margin_api_uri(path, version)
184208

185-
return await self._request(method, uri, signed, **kwargs)
209+
force_params = kwargs.pop("force_params", False)
210+
return await self._request(method, uri, signed, force_params, **kwargs)
186211

187212
async def _request_papi_api(
188213
self, method, path, signed=False, version=1, **kwargs
189214
) -> Dict:
190215
version = self._get_version(version, **kwargs)
191216
uri = self._create_papi_api_uri(path, version)
192217

193-
return await self._request(method, uri, signed, **kwargs)
218+
force_params = kwargs.pop("force_params", False)
219+
return await self._request(method, uri, signed, force_params, **kwargs)
194220

195221
async def _request_website(self, method, path, signed=False, **kwargs) -> Dict:
196222
uri = self._create_website_uri(path)
@@ -712,13 +738,16 @@ async def get_account(self, **params):
712738

713739
get_account.__doc__ = Client.get_account.__doc__
714740

715-
async def get_asset_balance(self, asset, **params):
741+
async def get_asset_balance(self, asset=None, **params):
716742
res = await self.get_account(**params)
717743
# find asset balance in list of balances
718744
if "balances" in res:
719-
for bal in res["balances"]:
720-
if bal["asset"].lower() == asset.lower():
721-
return bal
745+
if asset:
746+
for bal in res["balances"]:
747+
if bal["asset"].lower() == asset.lower():
748+
return bal
749+
else:
750+
return res["balances"]
722751
return None
723752

724753
get_asset_balance.__doc__ = Client.get_asset_balance.__doc__
@@ -1775,17 +1804,28 @@ async def futures_create_order(self, **params):
17751804
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
17761805
return await self._request_futures_api("post", "order", True, data=params)
17771806

1807+
async def futures_modify_order(self, **params):
1808+
"""Modify an existing order. Currently only LIMIT order modification is supported.
1809+
1810+
https://binance-docs.github.io/apidocs/futures/en/#modify-order-trade
1811+
1812+
"""
1813+
return await self._request_futures_api("put", "order", True, data=params)
1814+
17781815
async def futures_create_test_order(self, **params):
17791816
return await self._request_futures_api("post", "order/test", True, data=params)
17801817

17811818
async def futures_place_batch_order(self, **params):
17821819
for order in params["batchOrders"]:
17831820
if "newClientOrderId" not in order:
17841821
order["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
1785-
query_string = urlencode(params)
1786-
query_string = query_string.replace("%27", "%22")
1822+
order = self._order_params(order)
1823+
query_string = urlencode(params).replace("%40", "@").replace("%27", "%22")
17871824
params["batchOrders"] = query_string[12:]
1788-
return await self._request_futures_api("post", "batchOrders", True, data=params)
1825+
1826+
return await self._request_futures_api(
1827+
"post", "batchOrders", True, data=params, force_params=True
1828+
)
17891829

17901830
async def futures_get_order(self, **params):
17911831
return await self._request_futures_api("get", "order", True, data=params)
@@ -1805,8 +1845,16 @@ async def futures_cancel_all_open_orders(self, **params):
18051845
)
18061846

18071847
async def futures_cancel_orders(self, **params):
1848+
if params.get("orderidlist"):
1849+
params["orderidlist"] = quote(
1850+
convert_list_to_json_array(params["orderidlist"])
1851+
)
1852+
if params.get("origclientorderidlist"):
1853+
params["origclientorderidlist"] = quote(
1854+
convert_list_to_json_array(params["origclientorderidlist"])
1855+
)
18081856
return await self._request_futures_api(
1809-
"delete", "batchOrders", True, data=params
1857+
"delete", "batchOrders", True, data=params, force_params=True
18101858
)
18111859

18121860
async def futures_countdown_cancel_all(self, **params):
@@ -2036,10 +2084,18 @@ async def futures_coin_cancel_order(self, **params):
20362084

20372085
async def futures_coin_cancel_all_open_orders(self, **params):
20382086
return await self._request_futures_coin_api(
2039-
"delete", "allOpenOrders", signed=True, data=params
2087+
"delete", "allOpenOrders", signed=True, data=params, force_params=True
20402088
)
20412089

20422090
async def futures_coin_cancel_orders(self, **params):
2091+
if params.get("orderidlist"):
2092+
params["orderidlist"] = quote(
2093+
convert_list_to_json_array(params["orderidlist"])
2094+
)
2095+
if params.get("origclientorderidlist"):
2096+
params["origclientorderidlist"] = quote(
2097+
convert_list_to_json_array(params["origclientorderidlist"])
2098+
)
20432099
return await self._request_futures_coin_api(
20442100
"delete", "batchOrders", True, data=params
20452101
)
@@ -3601,3 +3657,51 @@ async def ws_futures_account_status(self, **params):
36013657
https://developers.binance.com/docs/derivatives/usds-margined-futures/account/websocket-api/Account-Information
36023658
"""
36033659
return await self._ws_futures_api_request("account.status", True, params)
3660+
3661+
####################################################
3662+
# Gift Card API Endpoints
3663+
####################################################
3664+
3665+
async def gift_card_fetch_token_limit(self, **params):
3666+
return await self._request_margin_api(
3667+
"get", "giftcard/buyCode/token-limit", signed=True, data=params
3668+
)
3669+
3670+
gift_card_fetch_token_limit.__doc__ = Client.gift_card_fetch_token_limit.__doc__
3671+
3672+
async def gift_card_fetch_rsa_public_key(self, **params):
3673+
return await self._request_margin_api(
3674+
"get", "giftcard/cryptography/rsa-public-key", signed=True, data=params
3675+
)
3676+
3677+
gift_card_fetch_rsa_public_key.__doc__ = (
3678+
Client.gift_card_fetch_rsa_public_key.__doc__
3679+
)
3680+
3681+
async def gift_card_verify(self, **params):
3682+
return await self._request_margin_api(
3683+
"get", "giftcard/verify", signed=True, data=params
3684+
)
3685+
3686+
gift_card_verify.__doc__ = Client.gift_card_verify.__doc__
3687+
3688+
async def gift_card_redeem(self, **params):
3689+
return await self._request_margin_api(
3690+
"post", "giftcard/redeemCode", signed=True, data=params
3691+
)
3692+
3693+
gift_card_redeem.__doc__ = Client.gift_card_redeem.__doc__
3694+
3695+
async def gift_card_create(self, **params):
3696+
return await self._request_margin_api(
3697+
"post", "giftcard/createCode", signed=True, data=params
3698+
)
3699+
3700+
gift_card_create.__doc__ = Client.gift_card_create.__doc__
3701+
3702+
async def gift_card_create_dual_token(self, **params):
3703+
return await self._request_margin_api(
3704+
"post", "giftcard/buyCode", signed=True, data=params
3705+
)
3706+
3707+
gift_card_create_dual_token.__doc__ = Client.gift_card_create_dual_token.__doc__

binance/base_client.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class BaseClient:
6060

6161
REQUEST_TIMEOUT: float = 10
6262

63-
REQUEST_RECVWINDOW: int = 10000 # 10 seconds
63+
REQUEST_RECVWINDOW: int = 10000 # 10 seconds
6464

6565
SYMBOL_TYPE_SPOT = "SPOT"
6666

@@ -209,6 +209,7 @@ def __init__(
209209
def _get_headers(self) -> Dict:
210210
headers = {
211211
"Accept": "application/json",
212+
"Content-Type": "application/json",
212213
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", # noqa
213214
}
214215
if self.API_KEY:
@@ -480,8 +481,13 @@ def _get_request_kwargs(
480481
del kwargs["data"]
481482

482483
# Temporary fix for Signature issue while using batchOrders in AsyncClient
483-
if "params" in kwargs.keys() and "batchOrders" in kwargs["params"]:
484-
kwargs["data"] = kwargs["params"]
485-
del kwargs["params"]
484+
if "params" in kwargs.keys():
485+
if (
486+
"batchOrders" in kwargs["params"]
487+
or "orderidlist" in kwargs["params"]
488+
or "origclientorderidlist" in kwargs["params"]
489+
):
490+
kwargs["data"] = kwargs["params"]
491+
del kwargs["params"]
486492

487493
return kwargs

0 commit comments

Comments
 (0)