diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6c767b..098fed5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 3.3.0 - 2023-08-07 + +### Changed +- Add support for proxy in Websocket clients +- Remove support for python 3.7 + + ## 3.2.0 - 2023-08-01 ### Changed diff --git a/README.md b/README.md index 16794195..c35e1213 100644 --- a/README.md +++ b/README.md @@ -270,6 +270,67 @@ logging.info("closing ws connection") my_client.stop() ``` +#### Proxy + +Proxy is supported for both WebSocket API and WebSocket Stream. + +To use it, pass in the `proxies` parameter when initializing the client. + +The format of the `proxies` parameter is the same as the one used in the Spot RESTful API. + +It consists on a dictionary with the following format, where the key is the type of the proxy and the value is the proxy URL: + +For websockets, the proxy type is `http`. + +```python +proxies = { 'http': 'http://1.2.3.4:8080' } +``` + +You can also use authentication for the proxy by adding the `username` and `password` parameters to the proxy URL: + +```python +proxies = { 'http': 'http://username:password@host:port' } +``` + + +```python + +# WebSocket API Client +from binance.websocket.spot.websocket_api import SpotWebsocketAPIClient + +def message_handler(_, message): + logging.info(message) + +proxies = { 'http': 'http://1.2.3.4:8080' } + +my_client = SpotWebsocketAPIClient(on_message=message_handler, proxies=proxies) + +my_client.ticker(symbol="BNBBUSD", type="FULL") + +time.sleep(5) +logging.info("closing ws connection") +my_client.stop() +``` + +```python + +# WebSocket Stream Client +from binance.websocket.spot.websocket_stream import SpotWebsocketStreamClient + +def message_handler(_, message): + logging.info(message) + +proxies = { 'http': 'http://1.2.3.4:8080' } + +my_client = SpotWebsocketStreamClient(on_message=message_handler, proxies=proxies) + +# Subscribe to a single symbol stream +my_client.agg_trade(symbol="bnbusdt") +time.sleep(5) +logging.info("closing ws connection") +my_client.stop() +``` + #### Request Id Client can assign a request id to each request. The request id will be returned in the response message. Not mandatory in the library, it generates a uuid format string if not provided. diff --git a/binance/__version__.py b/binance/__version__.py index 11731085..88c513ea 100644 --- a/binance/__version__.py +++ b/binance/__version__.py @@ -1 +1 @@ -__version__ = "3.2.0" +__version__ = "3.3.0" diff --git a/binance/lib/utils.py b/binance/lib/utils.py index ebb2129d..50aea4e9 100644 --- a/binance/lib/utils.py +++ b/binance/lib/utils.py @@ -1,6 +1,8 @@ import json import time import uuid + +from urllib.parse import urlparse from collections import OrderedDict from urllib.parse import urlencode from binance.lib.authentication import hmac_hashing @@ -112,3 +114,19 @@ def websocket_api_signature(api_key: str, api_secret: str, parameters: dict): parameters["signature"] = hmac_hashing(api_secret, urlencode(parameters)) return parameters + + +def parse_proxies(proxies: dict): + """Parse proxy url from dict, only support http and https proxy, not support socks5 proxy""" + proxy_url = proxies.get("http") or proxies.get("https") + if not proxy_url: + return {} + + parsed = urlparse(proxy_url) + return { + "http_proxy_host": parsed.hostname, + "http_proxy_port": parsed.port, + "http_proxy_auth": (parsed.username, parsed.password) + if parsed.username and parsed.password + else None, + } diff --git a/binance/websocket/binance_socket_manager.py b/binance/websocket/binance_socket_manager.py index 858bd1f2..61567cf2 100644 --- a/binance/websocket/binance_socket_manager.py +++ b/binance/websocket/binance_socket_manager.py @@ -1,3 +1,5 @@ +from typing import Optional + import logging import threading from websocket import ( @@ -6,6 +8,7 @@ WebSocketException, WebSocketConnectionClosedException, ) +from binance.lib.utils import parse_proxies class BinanceSocketManager(threading.Thread): @@ -19,6 +22,7 @@ def __init__( on_ping=None, on_pong=None, logger=None, + proxies: Optional[dict] = None, ): threading.Thread.__init__(self) if not logger: @@ -31,15 +35,19 @@ def __init__( self.on_ping = on_ping self.on_pong = on_pong self.on_error = on_error + self.proxies = proxies + + self._proxy_params = parse_proxies(proxies) if proxies else {} + self.create_ws_connection() def create_ws_connection(self): self.logger.debug( - "Creating connection with WebSocket Server: %s", self.stream_url + f"Creating connection with WebSocket Server: {self.stream_url}, proxies: {self.proxies}", ) - self.ws = create_connection(self.stream_url) + self.ws = create_connection(self.stream_url, **self._proxy_params) self.logger.debug( - "WebSocket connection has been established: %s", self.stream_url + f"WebSocket connection has been established: {self.stream_url}, proxies: {self.proxies}", ) self._callback(self.on_open) diff --git a/binance/websocket/spot/websocket_api/__init__.py b/binance/websocket/spot/websocket_api/__init__.py index 1df3f297..6f09f994 100644 --- a/binance/websocket/spot/websocket_api/__init__.py +++ b/binance/websocket/spot/websocket_api/__init__.py @@ -1,3 +1,5 @@ +from typing import Optional + from binance.websocket.websocket_client import BinanceWebsocketClient @@ -13,6 +15,7 @@ def __init__( on_error=None, on_ping=None, on_pong=None, + proxies: Optional[dict] = None, ): self.api_key = api_key self.api_secret = api_secret @@ -25,6 +28,7 @@ def __init__( on_error=on_error, on_ping=on_ping, on_pong=on_pong, + proxies=proxies, ) # Market diff --git a/binance/websocket/spot/websocket_stream.py b/binance/websocket/spot/websocket_stream.py index 1f53e5f6..b1640b1c 100644 --- a/binance/websocket/spot/websocket_stream.py +++ b/binance/websocket/spot/websocket_stream.py @@ -1,3 +1,5 @@ +from typing import Optional + from binance.websocket.websocket_client import BinanceWebsocketClient @@ -12,6 +14,7 @@ def __init__( on_ping=None, on_pong=None, is_combined=False, + proxies: Optional[dict] = None, ): if is_combined: stream_url = stream_url + "/stream" @@ -25,6 +28,7 @@ def __init__( on_error=on_error, on_ping=on_ping, on_pong=on_pong, + proxies=proxies, ) def agg_trade(self, symbol: str, id=None, action=None, **kwargs): diff --git a/binance/websocket/websocket_client.py b/binance/websocket/websocket_client.py index 187090f9..c01f101f 100644 --- a/binance/websocket/websocket_client.py +++ b/binance/websocket/websocket_client.py @@ -1,3 +1,5 @@ +from typing import Optional + import json import logging from binance.lib.utils import get_timestamp @@ -18,6 +20,7 @@ def __init__( on_ping=None, on_pong=None, logger=None, + proxies: Optional[dict] = None, ): if not logger: logger = logging.getLogger(__name__) @@ -31,6 +34,7 @@ def __init__( on_ping, on_pong, logger, + proxies, ) # start the thread @@ -47,6 +51,7 @@ def _initialize_socket( on_ping, on_pong, logger, + proxies, ): return BinanceSocketManager( stream_url, @@ -57,6 +62,7 @@ def _initialize_socket( on_ping=on_ping, on_pong=on_pong, logger=logger, + proxies=proxies, ) def _single_stream(self, stream): diff --git a/docs/source/CHANGELOG.rst b/docs/source/CHANGELOG.rst index ed8b6995..3ec5308a 100644 --- a/docs/source/CHANGELOG.rst +++ b/docs/source/CHANGELOG.rst @@ -2,6 +2,16 @@ Changelog ========= +3.3.0 - 2023-08-07 +------------------ + +Changed +^^^^^^^ + +* Add support for proxy in Websocket clients +* Remove support for python 3.7 + + 3.2.0 - 2023-08-01 ------------------ diff --git a/setup.py b/setup.py index a794fb83..bdc27384 100644 --- a/setup.py +++ b/setup.py @@ -43,10 +43,10 @@ "Intended Audience :: Developers", "Intended Audience :: Financial and Insurance Industry", "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], - python_requires=">=3.7", + python_requires=">=3.8", ) diff --git a/tests/lib/test_utils.py b/tests/lib/test_utils.py index 511fd743..70c8eab4 100644 --- a/tests/lib/test_utils.py +++ b/tests/lib/test_utils.py @@ -1,3 +1,5 @@ +import pytest + from binance.error import ( ParameterRequiredError, ParameterTypeError, @@ -8,6 +10,7 @@ check_type_parameter, convert_list_to_json_array, purge_map, + parse_proxies, ) from binance.lib.utils import check_required_parameters from binance.lib.utils import check_enum_parameter @@ -117,3 +120,34 @@ def test_remove_empty_parameter(): purge_map({"foo": "bar", "foo2": 0}).should.equal({"foo": "bar"}) purge_map({"foo": "bar", "foo2": []}).should.equal({"foo": "bar", "foo2": []}) purge_map({"foo": "bar", "foo2": {}}).should.equal({"foo": "bar", "foo2": {}}) + + +def test_parse_proxies(): + proxies = {"http": "http://1.2.3.4:8080"} + output = { + "http_proxy_host": "1.2.3.4", + "http_proxy_port": 8080, + "http_proxy_auth": None, + } + + proxy_data = parse_proxies(proxies) + assert proxy_data == output + + proxies_2 = {"https": "http://1.2.3.4:8080"} + + proxy_data_2 = parse_proxies(proxies_2) + assert proxy_data_2 == output + + +def test_parse_proxies_non_supported(): + proxies = {"socks5": "http://1.2.3.4:8080"} + + proxy_data = parse_proxies(proxies) + assert proxy_data == {} + + +def test_parse_proxies_invalid(): + proxies = {"http": "http://x1.2.3.4.6-8080:x"} + + with pytest.raises(ValueError): + parse_proxies(proxies) diff --git a/tox.ini b/tox.ini index a5106a47..ff9e0251 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py36,py37,py38,py39 +envlist = py38,py39,310,311 [testenv] deps =