From 6b9447f6b45f0f62f1bf129a371569588f42496a Mon Sep 17 00:00:00 2001 From: Pablo Date: Tue, 18 Feb 2025 01:07:33 -0600 Subject: [PATCH 1/6] add timeout to jobs --- .github/workflows/python-app.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 2b0012d20..658baafff 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -16,6 +16,7 @@ permissions: jobs: lint: runs-on: ubuntu-latest + timeout-minutes: 5 steps: - uses: actions/checkout@v4 - name: Set up Python @@ -33,6 +34,7 @@ jobs: build: needs: lint runs-on: ubuntu-22.04 + timeout-minutes: 20 env: PROXY: "http://51.83.140.52:16301" TEST_TESTNET: "true" @@ -74,6 +76,7 @@ jobs: needs: build if: ${{ always() }} runs-on: ubuntu-latest + timeout-minutes: 5 steps: - name: Coveralls Finished uses: coverallsapp/github-action@v2 From c5c41ab086a8004ace8f8497858861d99c53e159 Mon Sep 17 00:00:00 2001 From: Pablo Date: Sat, 5 Apr 2025 20:48:23 -0400 Subject: [PATCH 2/6] chore: run tests in parallel --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index ab2427681..d26e34650 100644 --- a/tox.ini +++ b/tox.ini @@ -12,7 +12,7 @@ passenv = TEST_API_SECRET TEST_FUTURES_API_KEY TEST_FUTURES_API_SECRET -commands = pytest -n 1 -v tests/ --doctest-modules --cov binance --cov-report term-missing --reruns 3 --reruns-delay 120 +commands = pytest -n 5 -v tests/ --doctest-modules --cov binance --cov-report term-missing --reruns 3 --reruns-delay 120 [pep8] ignore = E501 From ab2842d2ac0871c1b7802a0f7a95e171a8cc5f6e Mon Sep 17 00:00:00 2001 From: Pablo Date: Sat, 5 Apr 2025 21:05:15 -0400 Subject: [PATCH 3/6] fix event loop not exisiting --- binance/ws/websocket_api.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/binance/ws/websocket_api.py b/binance/ws/websocket_api.py index 8542b62db..1e2b7b1e9 100644 --- a/binance/ws/websocket_api.py +++ b/binance/ws/websocket_api.py @@ -13,10 +13,15 @@ def __init__(self, url: str, tld: str = "com", testnet: bool = False): self._tld = tld self._testnet = testnet self._responses: Dict[str, asyncio.Future] = {} - self._connection_lock = ( - asyncio.Lock() - ) # used to ensure only one connection is established at a time + self._connection_lock: Optional[asyncio.Lock] = None super().__init__(url=url, prefix="", path="", is_binary=False) + + @property + def connection_lock(self) -> asyncio.Lock: + if self._connection_lock is None: + loop = asyncio.get_event_loop() + self._connection_lock = asyncio.Lock() + return self._connection_lock def _handle_message(self, msg): """Override message handling to support request-response""" @@ -51,7 +56,7 @@ async def _ensure_ws_connection(self) -> None: 3. Wait for connection to be ready 4. Handle reconnection if needed """ - async with self._connection_lock: + async with self.connection_lock: try: if ( self.ws is None From 3c8bd496eb24e6385ac3e25da252c7e4c610e602 Mon Sep 17 00:00:00 2001 From: Pablo Date: Sat, 5 Apr 2025 21:08:32 -0400 Subject: [PATCH 4/6] pyright --- binance/ws/websocket_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/binance/ws/websocket_api.py b/binance/ws/websocket_api.py index 1e2b7b1e9..52c5518d7 100644 --- a/binance/ws/websocket_api.py +++ b/binance/ws/websocket_api.py @@ -1,4 +1,4 @@ -from typing import Dict +from typing import Dict, Optional import asyncio from websockets import WebSocketClientProtocol # type: ignore From bf1aca278d66e6e176e6aa32e2f15d16d2b1ef21 Mon Sep 17 00:00:00 2001 From: Pablo Date: Sun, 6 Apr 2025 23:46:00 -0400 Subject: [PATCH 5/6] comment coveralls --- .github/workflows/python-app.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index a3a2d61a5..021b766bd 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -67,18 +67,18 @@ jobs: run: pyright - name: Test with tox run: tox -e py - - name: Coveralls Parallel - uses: coverallsapp/github-action@v2 - with: - flag-name: run-${{ join(matrix.*, '-') }} - parallel: true - finish: - needs: build - if: ${{ always() }} - runs-on: ubuntu-latest - timeout-minutes: 5 - steps: - - name: Coveralls Finished - uses: coverallsapp/github-action@v2 - with: - parallel-finished: true + # - name: Coveralls Parallel + # uses: coverallsapp/github-action@v2 + # with: + # flag-name: run-${{ join(matrix.*, '-') }} + # parallel: true + # finish: + # needs: build + # if: ${{ always() }} + # runs-on: ubuntu-latest + # timeout-minutes: 5 + # steps: + # - name: Coveralls Finished + # uses: coverallsapp/github-action@v2 + # with: + # parallel-finished: true From 9302143d7181c6619a0f84a8310c9b8a160b8daf Mon Sep 17 00:00:00 2001 From: Pablo Date: Mon, 7 Apr 2025 00:38:20 -0400 Subject: [PATCH 6/6] solve close connection error --- tests/conftest.py | 24 +++++++++++---- tests/test_ws_api.py | 69 +++++++++++++++++++++++++++++--------------- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ca4550e5d..660369746 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -62,20 +62,32 @@ def futuresClient(): @pytest.fixture(scope="function") -def clientAsync(): - return AsyncClient(api_key, api_secret, https_proxy=proxy, testnet=testnet) +async def clientAsync(): + client = AsyncClient(api_key, api_secret, https_proxy=proxy, testnet=testnet) + try: + yield client + finally: + await client.close_connection() @pytest.fixture(scope="function") -def futuresClientAsync(): - return AsyncClient( +async def futuresClientAsync(): + client = AsyncClient( futures_api_key, futures_api_secret, https_proxy=proxy, testnet=testnet ) + try: + yield client + finally: + await client.close_connection() @pytest.fixture(scope="function") -def liveClientAsync(): - return AsyncClient(api_key, api_secret, https_proxy=proxy, testnet=False) +async def liveClientAsync(): + client = AsyncClient(api_key, api_secret, https_proxy=proxy, testnet=False) + try: + yield client + finally: + await client.close_connection() @pytest.fixture(scope="function") def manager(): diff --git a/tests/test_ws_api.py b/tests/test_ws_api.py index dfdbad1cc..a3694ee5d 100644 --- a/tests/test_ws_api.py +++ b/tests/test_ws_api.py @@ -114,38 +114,58 @@ async def test_testnet_url(): @pytest.mark.asyncio async def test_message_handling(clientAsync): """Test message handling with various message types""" - # Test valid message - future = asyncio.Future() - clientAsync.ws_api._responses["123"] = future - valid_msg = {"id": "123", "status": 200, "result": {"test": "data"}} - clientAsync.ws_api._handle_message(json.dumps(valid_msg)) - result = await clientAsync.ws_api._responses["123"] - assert result == valid_msg - -@pytest.mark.asyncio -async def test_message_handling_raise_exception(clientAsync): - with pytest.raises(BinanceAPIException): + try: + # Test valid message future = asyncio.Future() clientAsync.ws_api._responses["123"] = future - valid_msg = {"id": "123", "status": 400, "error": {"code": "0", "msg": "error message"}} + valid_msg = {"id": "123", "status": 200, "result": {"test": "data"}} clientAsync.ws_api._handle_message(json.dumps(valid_msg)) - await future + result = await clientAsync.ws_api._responses["123"] + assert result == valid_msg + finally: + # Ensure cleanup + await clientAsync.close_connection() + + +@pytest.mark.asyncio +async def test_message_handling_raise_exception(clientAsync): + try: + with pytest.raises(BinanceAPIException): + future = asyncio.Future() + clientAsync.ws_api._responses["123"] = future + valid_msg = {"id": "123", "status": 400, "error": {"code": "0", "msg": "error message"}} + clientAsync.ws_api._handle_message(json.dumps(valid_msg)) + await future + finally: + # Ensure cleanup + await clientAsync.close_connection() + + @pytest.mark.asyncio async def test_message_handling_raise_exception_without_id(clientAsync): - with pytest.raises(BinanceAPIException): - future = asyncio.Future() - clientAsync.ws_api._responses["123"] = future - valid_msg = {"id": "123", "status": 400, "error": {"code": "0", "msg": "error message"}} - clientAsync.ws_api._handle_message(json.dumps(valid_msg)) - await future - + try: + with pytest.raises(BinanceAPIException): + future = asyncio.Future() + clientAsync.ws_api._responses["123"] = future + valid_msg = {"id": "123", "status": 400, "error": {"code": "0", "msg": "error message"}} + clientAsync.ws_api._handle_message(json.dumps(valid_msg)) + await future + finally: + # Ensure cleanup + await clientAsync.close_connection() + + @pytest.mark.asyncio async def test_message_handling_invalid_json(clientAsync): - with pytest.raises(json.JSONDecodeError): - clientAsync.ws_api._handle_message("invalid json") + try: + with pytest.raises(json.JSONDecodeError): + clientAsync.ws_api._handle_message("invalid json") - with pytest.raises(json.JSONDecodeError): - clientAsync.ws_api._handle_message("invalid json") + with pytest.raises(json.JSONDecodeError): + clientAsync.ws_api._handle_message("invalid json") + finally: + # Ensure cleanup + await clientAsync.close_connection() @pytest.mark.asyncio(scope="function") @@ -199,6 +219,7 @@ async def test_ws_queue_overflow(clientAsync): # Restore original queue size clientAsync.ws_api.MAX_QUEUE_SIZE = original_size + @pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+") @pytest.mark.asyncio async def test_ws_api_with_stream(clientAsync):