Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions polymarket_us/websocket/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -35,7 +35,7 @@ classifiers = [
dependencies = [
"httpx>=0.27.0",
"pynacl>=1.5.0",
"websockets>=12.0",
"websockets>=13.0",
]

[project.optional-dependencies]
Expand Down
46 changes: 46 additions & 0 deletions tests/test_websocket.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,59 @@
"""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

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."""

Expand Down
Loading