diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3b59af2..93f513f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,3 +72,26 @@ jobs: - name: Run tests run: uv run pytest + + test-minimum-websockets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install uv + uses: astral-sh/setup-uv@38f3f104447c67c051c4a08e39b64a148898af3a # v4 + + - name: Install dependencies + run: | + uv sync --all-extras + uv pip install 'websockets==13.0' + + - name: Run tests with minimum websockets + run: | + uv run --no-sync python -c 'import websockets; assert websockets.__version__ == "13.0"' + uv run --no-sync pytest diff --git a/polymarket_us/websocket/base.py b/polymarket_us/websocket/base.py index b2f3db1..001022d 100644 --- a/polymarket_us/websocket/base.py +++ b/polymarket_us/websocket/base.py @@ -9,7 +9,7 @@ from typing import Any import websockets -from websockets.asyncio.client import ClientConnection +from websockets.asyncio.client import ClientConnection, connect from polymarket_us.auth import create_auth_headers from polymarket_us.errors import PolymarketUSError @@ -50,7 +50,7 @@ async def connect(self) -> None: url = f"{self.base_url}{self.path}" headers = create_auth_headers(self.key_id, self.secret_key, "GET", self.path) - self._ws = await websockets.connect(url, additional_headers=headers) + self._ws = await connect(url, additional_headers=headers) self._emit("open") # Start message handler diff --git a/pyproject.toml b/pyproject.toml index 7558736..c129bd8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "polymarket-us" -version = "1.0.0" +version = "1.0.1" description = "Polymarket US Python SDK" readme = "README.md" license = "MIT" @@ -35,7 +35,7 @@ classifiers = [ dependencies = [ "httpx>=0.27.0", "pynacl>=1.5.0", - "websockets>=12.0", + "websockets>=13.0", ] [project.optional-dependencies] diff --git a/tests/test_websocket.py b/tests/test_websocket.py index 46a9a41..28cb762 100644 --- a/tests/test_websocket.py +++ b/tests/test_websocket.py @@ -1,6 +1,13 @@ """Tests for WebSocket functionality.""" +import asyncio +import base64 +from typing import Literal + import pytest +from nacl.signing import SigningKey +from websockets.asyncio.server import ServerConnection, serve +from websockets.http11 import Request from polymarket_us import AuthenticationError, PolymarketUS from polymarket_us.websocket import MarketsWebSocket, PrivateWebSocket @@ -8,6 +15,45 @@ TEST_SECRET_KEY = "nWGxne/9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A=" +@pytest.mark.parametrize("stream", ["markets", "private"]) +async def test_connect_sends_auth_headers(stream: Literal["markets", "private"]) -> None: + requests: asyncio.Queue[Request] = asyncio.Queue() + + async def handler(connection: ServerConnection) -> None: + assert connection.request is not None + await requests.put(connection.request) + await connection.send('{"heartbeat": {}}') + await connection.wait_closed() + + async with serve(handler, "127.0.0.1", 0, close_timeout=1) as server: + port = server.sockets[0].getsockname()[1] + with PolymarketUS( + key_id="test-key", + secret_key=TEST_SECRET_KEY, + api_base_url=f"http://127.0.0.1:{port}", + ) as client: + ws = client.ws.markets() if stream == "markets" else client.ws.private() + heartbeat = asyncio.Event() + ws.on("heartbeat", heartbeat.set) + try: + await asyncio.wait_for(ws.connect(), timeout=5) + request = await asyncio.wait_for(requests.get(), timeout=5) + assert request.path == f"/v1/ws/{stream}" + assert request.headers["X-PM-Access-Key"] == "test-key" + timestamp = request.headers["X-PM-Timestamp"] + assert timestamp.isdigit() + signature = base64.b64decode(request.headers["X-PM-Signature"]) + signing_key = SigningKey(base64.b64decode(TEST_SECRET_KEY)) + signing_key.verify_key.verify( + (timestamp + "GET" + request.path).encode(), signature + ) + await asyncio.wait_for(heartbeat.wait(), timeout=5) + assert ws.is_connected + finally: + await asyncio.wait_for(ws.close(), timeout=5) + assert not ws.is_connected + + class TestWebSocketFactory: """Tests for WebSocket factory."""