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/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 | 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": "", + } +}