From 62da7c54e08abd08f6755821bbfc068af429f128 Mon Sep 17 00:00:00 2001 From: harley-poly Date: Mon, 21 Sep 2026 22:07:10 -0400 Subject: [PATCH 1/2] fix: correct market response types --- .github/workflows/ci.yml | 2 +- polymarket_us/resources/markets.py | 12 +- polymarket_us/types/__init__.py | 4 + polymarket_us/types/markets.py | 28 ++-- tests/test_markets.py | 213 ++++++++++++++++------------- tests/types/market_responses.py | 50 +++++++ 6 files changed, 197 insertions(+), 112 deletions(-) create mode 100644 tests/types/market_responses.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe50513..3b59af2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: run: uv sync --all-extras - name: Run mypy - run: uv run mypy polymarket_us tests/types/order_snapshot.py + run: uv run mypy polymarket_us tests/types test: runs-on: ubuntu-latest diff --git a/polymarket_us/resources/markets.py b/polymarket_us/resources/markets.py index 208f2c0..f858405 100644 --- a/polymarket_us/resources/markets.py +++ b/polymarket_us/resources/markets.py @@ -2,10 +2,10 @@ from polymarket_us.resource import APIResource, AsyncAPIResource from polymarket_us.types import ( + GetMarketBBOResponse, + GetMarketBookResponse, GetMarketResponse, GetMarketsResponse, - MarketBBO, - MarketBook, MarketSettlement, MarketsListParams, ) @@ -26,11 +26,11 @@ def retrieve_by_slug(self, slug: str) -> GetMarketResponse: """Get a market by slug.""" return self._client.get(f"/v1/market/slug/{slug}") - def book(self, slug: str) -> MarketBook: + def book(self, slug: str) -> GetMarketBookResponse: """Get order book for a market.""" return self._client.get(f"/v1/markets/{slug}/book") - def bbo(self, slug: str) -> MarketBBO: + def bbo(self, slug: str) -> GetMarketBBOResponse: """Get best bid/offer for a market.""" return self._client.get(f"/v1/markets/{slug}/bbo") @@ -54,11 +54,11 @@ async def retrieve_by_slug(self, slug: str) -> GetMarketResponse: """Get a market by slug.""" return await self._client.get(f"/v1/market/slug/{slug}") - async def book(self, slug: str) -> MarketBook: + async def book(self, slug: str) -> GetMarketBookResponse: """Get order book for a market.""" return await self._client.get(f"/v1/markets/{slug}/book") - async def bbo(self, slug: str) -> MarketBBO: + async def bbo(self, slug: str) -> GetMarketBBOResponse: """Get best bid/offer for a market.""" return await self._client.get(f"/v1/markets/{slug}/bbo") diff --git a/polymarket_us/types/__init__.py b/polymarket_us/types/__init__.py index 5a6d56f..75f623d 100644 --- a/polymarket_us/types/__init__.py +++ b/polymarket_us/types/__init__.py @@ -16,6 +16,8 @@ Tag, ) from polymarket_us.types.markets import ( + GetMarketBBOResponse, + GetMarketBookResponse, GetMarketResponse, GetMarketsResponse, MarketBBO, @@ -113,6 +115,8 @@ "MarketsListParams", "GetMarketsResponse", "GetMarketResponse", + "GetMarketBookResponse", + "GetMarketBBOResponse", # Orders "OrderType", "OrderSide", diff --git a/polymarket_us/types/markets.py b/polymarket_us/types/markets.py index 180893d..f76599b 100644 --- a/polymarket_us/types/markets.py +++ b/polymarket_us/types/markets.py @@ -55,6 +55,7 @@ class MarketStats(TypedDict, total=False): MarketState = Literal[ + "MARKET_STATE_CLOSED", "MARKET_STATE_OPEN", "MARKET_STATE_PREOPEN", "MARKET_STATE_SUSPENDED", @@ -72,19 +73,19 @@ class MarketBook(TypedDict, total=False): bids: list[OrderBookLevel] offers: list[OrderBookLevel] state: MarketState - stats: MarketStats - transactTime: str + stats: MarketStats | None + transactTime: str | None class MarketBBO(TypedDict, total=False): """Best bid/offer for a market.""" marketSlug: str - bestBid: Amount - bestAsk: Amount + bestBid: Amount | None + bestAsk: Amount | None bidDepth: int askDepth: int - lastTradePx: Amount + lastTradePx: Amount | None sharesTraded: str openInterest: str @@ -92,9 +93,8 @@ class MarketBBO(TypedDict, total=False): class MarketSettlement(TypedDict): """Market settlement information.""" - marketSlug: str - settlementPrice: Amount - settledAt: str + slug: str + settlement: float class MarketsListParams(PaginationParams, total=False): @@ -126,3 +126,15 @@ class GetMarketResponse(TypedDict): """Response for getting a single market.""" market: MarketDetail + + +class GetMarketBookResponse(TypedDict): + """Response for getting a market's order book.""" + + marketData: MarketBook + + +class GetMarketBBOResponse(TypedDict): + """Response for getting a market's best bid/offer.""" + + marketData: MarketBBO diff --git a/tests/test_markets.py b/tests/test_markets.py index 04dba4d..7c07af0 100644 --- a/tests/test_markets.py +++ b/tests/test_markets.py @@ -4,8 +4,10 @@ import httpx import pytest +from pytest_httpx import HTTPXMock -from polymarket_us import PolymarketUS +from polymarket_us import AsyncPolymarketUS, PolymarketUS +from polymarket_us.types import GetMarketBBOResponse, GetMarketBookResponse, MarketSettlement class TestMarketsList: @@ -93,108 +95,125 @@ def test_uses_correct_path_with_slug( assert "/v1/market/slug/btc-100k" in url -class TestMarketsBook: - """Tests for markets.book().""" - - @pytest.fixture - def client(self) -> PolymarketUS: - return PolymarketUS() - - @patch.object(httpx.Client, "request") - def test_get_order_book(self, mock_request: MagicMock, client: PolymarketUS) -> None: - """Should get order book.""" - mock_response = MagicMock() - mock_response.is_success = True - mock_response.text = '{"marketSlug": "btc-100k", "bids": [], "offers": []}' - mock_response.json.return_value = { +@pytest.fixture(params=[False, True], ids=["populated", "empty"]) +def book_response(request: pytest.FixtureRequest) -> GetMarketBookResponse: + if request.param: + return { + "marketData": { + "marketSlug": "btc-100k", + "bids": [], + "offers": [], + "state": "MARKET_STATE_CLOSED", + "stats": None, + "transactTime": None, + } + } + return { + "marketData": { "marketSlug": "btc-100k", "bids": [{"px": {"value": "0.55", "currency": "USD"}, "qty": "100"}], "offers": [{"px": {"value": "0.56", "currency": "USD"}, "qty": "80"}], "state": "MARKET_STATE_OPEN", + "stats": {"lastTradePx": {"value": "0.55", "currency": "USD"}}, + "transactTime": "2026-09-21T12:00:00Z", } - mock_request.return_value = mock_response - - book = client.markets.book("btc-100k") - - assert book["marketSlug"] == "btc-100k" - assert "bids" in book - assert "offers" in book - - @patch.object(httpx.Client, "request") - def test_uses_correct_path(self, mock_request: MagicMock, client: PolymarketUS) -> None: - """Should use correct path.""" - mock_response = MagicMock() - mock_response.is_success = True - mock_response.text = '{"bids": [], "offers": []}' - mock_response.json.return_value = {"bids": [], "offers": []} - mock_request.return_value = mock_response - - client.markets.book("test-market") - - call_args = mock_request.call_args - url = call_args.args[1] if len(call_args.args) > 1 else call_args.kwargs.get("url") - assert "/v1/markets/test-market/book" in url - - -class TestMarketsBBO: - """Tests for markets.bbo().""" - - @pytest.fixture - def client(self) -> PolymarketUS: - return PolymarketUS() - - @patch.object(httpx.Client, "request") - def test_get_best_bid_offer(self, mock_request: MagicMock, client: PolymarketUS) -> None: - """Should get best bid/offer.""" - mock_response = MagicMock() - mock_response.is_success = True - mock_response.text = '{"marketSlug": "btc-100k"}' - mock_response.json.return_value = { + } + + +@pytest.fixture(params=[False, True], ids=["populated", "empty"]) +def bbo_response(request: pytest.FixtureRequest) -> GetMarketBBOResponse: + if request.param: + return { + "marketData": { + "marketSlug": "btc-100k", + "bestBid": None, + "bestAsk": None, + "lastTradePx": None, + "bidDepth": 0, + "askDepth": 0, + "sharesTraded": "", + "openInterest": "", + } + } + return { + "marketData": { "marketSlug": "btc-100k", "bestBid": {"value": "0.55", "currency": "USD"}, "bestAsk": {"value": "0.56", "currency": "USD"}, + "lastTradePx": {"value": "0.55", "currency": "USD"}, + "bidDepth": 1, + "askDepth": 1, + "sharesTraded": "100", + "openInterest": "80", } - mock_request.return_value = mock_response - - bbo = client.markets.bbo("btc-100k") - - assert "bestBid" in bbo - assert "bestAsk" in bbo - - @patch.object(httpx.Client, "request") - def test_uses_correct_path(self, mock_request: MagicMock, client: PolymarketUS) -> None: - """Should use correct path.""" - mock_response = MagicMock() - mock_response.is_success = True - mock_response.text = "{}" - mock_response.json.return_value = {} - mock_request.return_value = mock_response - - client.markets.bbo("test-market") - - call_args = mock_request.call_args - url = call_args.args[1] if len(call_args.args) > 1 else call_args.kwargs.get("url") - assert "/v1/markets/test-market/bbo" in url - - -class TestMarketsSettlement: - """Tests for markets.settlement().""" - - @pytest.fixture - def client(self) -> PolymarketUS: - return PolymarketUS() - - @patch.object(httpx.Client, "request") - def test_uses_correct_path(self, mock_request: MagicMock, client: PolymarketUS) -> None: - """Should use correct path.""" - mock_response = MagicMock() - mock_response.is_success = True - mock_response.text = "{}" - mock_response.json.return_value = {} - mock_request.return_value = mock_response - - client.markets.settlement("settled-market") - - call_args = mock_request.call_args - url = call_args.args[1] if len(call_args.args) > 1 else call_args.kwargs.get("url") - assert "/v1/markets/settled-market/settlement" in url + } + + +def test_book_returns_wire_response( + httpx_mock: HTTPXMock, book_response: GetMarketBookResponse +) -> None: + httpx_mock.add_response( + method="GET", + url="https://gateway.polymarket.us/v1/markets/btc-100k/book", + json=book_response, + ) + with PolymarketUS() as client: + assert client.markets.book("btc-100k") == book_response + + +async def test_async_book_returns_wire_response( + httpx_mock: HTTPXMock, book_response: GetMarketBookResponse +) -> None: + httpx_mock.add_response( + method="GET", + url="https://gateway.polymarket.us/v1/markets/btc-100k/book", + json=book_response, + ) + async with AsyncPolymarketUS() as client: + assert await client.markets.book("btc-100k") == book_response + + +def test_bbo_returns_wire_response( + httpx_mock: HTTPXMock, bbo_response: GetMarketBBOResponse +) -> None: + httpx_mock.add_response( + method="GET", url="https://gateway.polymarket.us/v1/markets/btc-100k/bbo", json=bbo_response + ) + with PolymarketUS() as client: + assert client.markets.bbo("btc-100k") == bbo_response + + +async def test_async_bbo_returns_wire_response( + httpx_mock: HTTPXMock, bbo_response: GetMarketBBOResponse +) -> None: + httpx_mock.add_response( + method="GET", url="https://gateway.polymarket.us/v1/markets/btc-100k/bbo", json=bbo_response + ) + async with AsyncPolymarketUS() as client: + assert await client.markets.bbo("btc-100k") == bbo_response + + +@pytest.mark.parametrize("settlement", [0, 0.5, 1]) +def test_settlement_returns_numeric_price(httpx_mock: HTTPXMock, settlement: float) -> None: + response: MarketSettlement = {"slug": "btc-100k", "settlement": settlement} + httpx_mock.add_response( + method="GET", + url="https://gateway.polymarket.us/v1/markets/btc-100k/settlement", + json=response, + ) + with PolymarketUS() as client: + assert client.markets.settlement("btc-100k") == response + + +@pytest.mark.parametrize("settlement", [0, 0.5, 1]) +async def test_async_settlement_returns_numeric_price( + httpx_mock: HTTPXMock, settlement: float +) -> None: + response: MarketSettlement = {"slug": "btc-100k", "settlement": settlement} + httpx_mock.add_response( + method="GET", + url="https://gateway.polymarket.us/v1/markets/btc-100k/settlement", + json=response, + ) + async with AsyncPolymarketUS() as client: + assert await client.markets.settlement("btc-100k") == response diff --git a/tests/types/market_responses.py b/tests/types/market_responses.py new file mode 100644 index 0000000..a6a061d --- /dev/null +++ b/tests/types/market_responses.py @@ -0,0 +1,50 @@ +"""Consumer typing checks included in the existing mypy CI job.""" + +from polymarket_us import AsyncPolymarketUS, PolymarketUS +from polymarket_us.types import ( + GetMarketBBOResponse, + GetMarketBookResponse, + MarketBBO, + MarketBook, +) + + +def read_market_data(client: PolymarketUS) -> tuple[MarketBook, MarketBBO, str, float]: + book = client.markets.book("market") + bbo = client.markets.bbo("market") + settlement = client.markets.settlement("market") + return book["marketData"], bbo["marketData"], settlement["slug"], settlement["settlement"] + + +async def read_market_data_async( + client: AsyncPolymarketUS, +) -> tuple[MarketBook, MarketBBO, str, float]: + book = await client.markets.book("market") + bbo = await client.markets.bbo("market") + settlement = await client.markets.settlement("market") + return book["marketData"], bbo["marketData"], settlement["slug"], settlement["settlement"] + + +empty_book: GetMarketBookResponse = { + "marketData": { + "marketSlug": "market", + "bids": [], + "offers": [], + "state": "MARKET_STATE_CLOSED", + "stats": None, + "transactTime": None, + } +} + +empty_bbo: GetMarketBBOResponse = { + "marketData": { + "marketSlug": "market", + "bestBid": None, + "bestAsk": None, + "lastTradePx": None, + "bidDepth": 0, + "askDepth": 0, + "sharesTraded": "", + "openInterest": "", + } +} From 21ff055106b5923aba2681bb7615459f2bf725ff Mon Sep 17 00:00:00 2001 From: harley-poly Date: Tue, 22 Sep 2026 09:40:58 -0400 Subject: [PATCH 2/2] docs: explain market response type migration BREAKING CHANGE: Market book and BBO response types now expose the marketData envelope. Settlement types now expose slug and numeric settlement instead of marketSlug, settlementPrice, and settledAt. Typed consumers must update field accesses and handle nullable market data fields; runtime responses are unchanged. --- README.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/README.md b/README.md index b055cbe..d341f6c 100644 --- a/README.md +++ b/README.md @@ -267,6 +267,25 @@ asyncio.run(main()) | `markets.bbo(slug)` | Get best bid/offer | | `markets.settlement(slug)` | Get settlement price | +#### Market response type migration + +The response types now match the existing JSON returned by both sync and async +clients; runtime responses are unchanged. Typed callers should read book and BBO +data through `marketData`. Settlement uses `slug` and a numeric `settlement`, +replacing the previous `marketSlug`, `settlementPrice`, and `settledAt` declarations. + +```python +book = client.markets.book("btc-100k")["marketData"] +bbo = client.markets.bbo("btc-100k")["marketData"] +settlement = client.markets.settlement("btc-100k") +slug = settlement["slug"] +settlement_price = settlement["settlement"] +``` + +With `AsyncPolymarketUS`, await each method call before reading these keys. +Handle `None` for book `stats` and `transactTime`, and BBO `bestBid`, `bestAsk`, and +`lastTradePx`. Books also support `MARKET_STATE_CLOSED`. + ### Orders (Authenticated) | Method | Description |