From 8a6112f5ccf1aac60b21f3ded3882639e29ff844 Mon Sep 17 00:00:00 2001 From: kartojal Date: Mon, 20 Jul 2026 18:38:41 +0200 Subject: [PATCH] feat(data): support multi-status filtering on combo positions --- src/polymarket/_internal/actions/data.py | 16 +++++++-- src/polymarket/clients/async_public.py | 6 +++- src/polymarket/clients/async_secure.py | 6 +++- src/polymarket/clients/public.py | 6 +++- src/polymarket/clients/secure.py | 6 +++- tests/integration/test_data_paginated.py | 25 +++++++++++++ tests/unit/test_data_paginated_specs.py | 46 ++++++++++++++++++++++++ 7 files changed, 105 insertions(+), 6 deletions(-) diff --git a/src/polymarket/_internal/actions/data.py b/src/polymarket/_internal/actions/data.py index aef02d9b..6c8d7c83 100644 --- a/src/polymarket/_internal/actions/data.py +++ b/src/polymarket/_internal/actions/data.py @@ -275,7 +275,7 @@ def list_closed_positions_spec( def list_combo_positions_spec( *, user: str, - status: ComboPositionStatus | None = None, + status: ComboPositionStatus | Sequence[ComboPositionStatus] | None = None, sort: ComboPositionSort | None = None, condition_id: str | Sequence[str] | None = None, updated_after: int | None = None, @@ -283,7 +283,8 @@ def list_combo_positions_spec( ) -> KeysetPaginatedSpec[ComboPosition]: if not user: raise UserInputError("user is required.") - _check_enum("status", status, _COMBO_POSITION_STATUS) + if status is not None: + status = _normalize_combo_status_filter(status) _check_enum("sort", sort, _COMBO_POSITION_SORT) if condition_id is not None: condition_id = _normalize_combo_condition_filter(condition_id) @@ -518,6 +519,17 @@ def _check_nonnegative_int(name: str, value: int | None) -> None: raise UserInputError(f"{name} must be non-negative.") +def _normalize_combo_status_filter( + value: ComboPositionStatus | Sequence[ComboPositionStatus], +) -> ComboPositionStatus | tuple[ComboPositionStatus, ...]: + values = (value,) if isinstance(value, str) else tuple(value) + if not values: + raise UserInputError("status must be a non-empty sequence.") + for item in values: + _check_enum("status", item, _COMBO_POSITION_STATUS) + return value if isinstance(value, str) else values + + def _normalize_combo_condition_filter(value: str | Sequence[str]) -> str | tuple[str, ...]: values = (value,) if isinstance(value, str) else tuple(value) if not values: diff --git a/src/polymarket/clients/async_public.py b/src/polymarket/clients/async_public.py index e02e5b3a..264f6cc0 100644 --- a/src/polymarket/clients/async_public.py +++ b/src/polymarket/clients/async_public.py @@ -714,7 +714,7 @@ def list_combo_positions( self, *, user: str, - status: ComboPositionStatus | None = None, + status: ComboPositionStatus | Sequence[ComboPositionStatus] | None = None, sort: ComboPositionSort | None = None, condition_id: str | Sequence[str] | None = None, updated_after: int | None = None, @@ -723,6 +723,10 @@ def list_combo_positions( ) -> AsyncPaginator[ComboPosition]: """List combo positions for a user. + Args: + status: One status or a non-empty sequence of statuses. Multiple + statuses are matched as alternatives in the given order. + Returns: An async paginator over matching combo positions. """ diff --git a/src/polymarket/clients/async_secure.py b/src/polymarket/clients/async_secure.py index 5cd117a0..3717fc0d 100644 --- a/src/polymarket/clients/async_secure.py +++ b/src/polymarket/clients/async_secure.py @@ -1342,7 +1342,7 @@ def list_combo_positions( self, *, user: str | None = None, - status: ComboPositionStatus | None = None, + status: ComboPositionStatus | Sequence[ComboPositionStatus] | None = None, sort: ComboPositionSort | None = None, condition_id: str | Sequence[str] | None = None, updated_after: int | None = None, @@ -1351,6 +1351,10 @@ def list_combo_positions( ) -> AsyncPaginator[ComboPosition]: """List combo positions for a user or the authenticated wallet. + Args: + status: One status or a non-empty sequence of statuses. Multiple + statuses are matched as alternatives in the given order. + Returns: An async paginator over matching combo positions. """ diff --git a/src/polymarket/clients/public.py b/src/polymarket/clients/public.py index ad7ff321..ecd687c3 100644 --- a/src/polymarket/clients/public.py +++ b/src/polymarket/clients/public.py @@ -494,7 +494,7 @@ def list_combo_positions( self, *, user: str, - status: ComboPositionStatus | None = None, + status: ComboPositionStatus | Sequence[ComboPositionStatus] | None = None, sort: ComboPositionSort | None = None, condition_id: str | Sequence[str] | None = None, updated_after: int | None = None, @@ -503,6 +503,10 @@ def list_combo_positions( ) -> Paginator[ComboPosition]: """List combo positions for a user. + Args: + status: One status or a non-empty sequence of statuses. Multiple + statuses are matched as alternatives in the given order. + Returns: A paginator over matching combo positions. """ diff --git a/src/polymarket/clients/secure.py b/src/polymarket/clients/secure.py index 73d4a1e7..51996494 100644 --- a/src/polymarket/clients/secure.py +++ b/src/polymarket/clients/secure.py @@ -871,7 +871,7 @@ def list_combo_positions( self, *, user: str | None = None, - status: ComboPositionStatus | None = None, + status: ComboPositionStatus | Sequence[ComboPositionStatus] | None = None, sort: ComboPositionSort | None = None, condition_id: str | Sequence[str] | None = None, updated_after: int | None = None, @@ -880,6 +880,10 @@ def list_combo_positions( ) -> Paginator[ComboPosition]: """List combo positions for a user or the authenticated wallet. + Args: + status: One status or a non-empty sequence of statuses. Multiple + statuses are matched as alternatives in the given order. + Returns: A paginator over matching combo positions. """ diff --git a/tests/integration/test_data_paginated.py b/tests/integration/test_data_paginated.py index 464a879a..fb31b3bb 100644 --- a/tests/integration/test_data_paginated.py +++ b/tests/integration/test_data_paginated.py @@ -8,6 +8,7 @@ ClosedPosition, ComboActivity, ComboPosition, + ComboPositionStatus, LeaderboardEntry, MetaMarketPosition, Position, @@ -84,6 +85,30 @@ def test_list_combo_positions_filters_by_condition_id() -> None: assert filtered.items[0].condition_id == condition_id +@pytest.mark.integration +def test_list_combo_positions_filters_by_multiple_statuses() -> None: + statuses: list[ComboPositionStatus] = ["RESOLVED_WIN", "RESOLVED_PARTIAL", "RESOLVED_LOSS"] + with PublicClient() as client: + paginator = client.list_combo_positions(user=COMBO_WALLET, status=statuses, page_size=1) + first = paginator.first_page() + pages = [first] + if first.next_cursor is not None: + pages.append(paginator.from_cursor(first.next_cursor).first_page()) + assert first.items + assert all(position.status in statuses for page in pages for position in page.items) + + +@pytest.mark.integration +def test_list_combo_positions_filters_by_single_status() -> None: + status: ComboPositionStatus = "RESOLVED_WIN" + with PublicClient() as client: + page = client.list_combo_positions( + user=COMBO_WALLET, status=status, page_size=10 + ).first_page() + assert page.items + assert all(p.status == status for p in page.items) + + @pytest.mark.integration def test_list_market_positions_first_page() -> None: condition_ids = _condition_ids_for_event() diff --git a/tests/unit/test_data_paginated_specs.py b/tests/unit/test_data_paginated_specs.py index 18de6c18..330b9047 100644 --- a/tests/unit/test_data_paginated_specs.py +++ b/tests/unit/test_data_paginated_specs.py @@ -70,6 +70,52 @@ def test_list_combo_positions_spec_validates_status() -> None: data_actions.list_combo_positions_spec(user="0xWALLET", status="CLOSED") # type: ignore[arg-type] +@pytest.mark.parametrize( + "statuses", + [ + ["RESOLVED_WIN", "RESOLVED_PARTIAL", "RESOLVED_LOSS"], + ("RESOLVED_WIN", "RESOLVED_PARTIAL", "RESOLVED_LOSS"), + ], +) +def test_list_combo_positions_spec_builds_request_with_multiple_statuses( + statuses: list[str] | tuple[str, ...], +) -> None: + spec = data_actions.list_combo_positions_spec( + user="0xWALLET", + status=statuses, # type: ignore[arg-type] + ) + assert spec.base_params == { + "user": "0xWALLET", + "status": "RESOLVED_WIN,RESOLVED_PARTIAL,RESOLVED_LOSS", + } + + +def test_list_combo_positions_spec_validates_each_status_in_sequence() -> None: + with pytest.raises(UserInputError, match="status"): + data_actions.list_combo_positions_spec(user="0xWALLET", status=["OPEN", "CLOSED"]) # type: ignore[list-item] + + +def test_list_combo_positions_spec_rejects_empty_status_sequence() -> None: + with pytest.raises(UserInputError, match="status"): + data_actions.list_combo_positions_spec(user="0xWALLET", status=[]) + + +def test_list_combo_positions_spec_rejects_non_string_status_member() -> None: + with pytest.raises(UserInputError, match="status"): + data_actions.list_combo_positions_spec( + user="0xWALLET", + status=["OPEN", 1], # type: ignore[list-item] + ) + + +def test_list_combo_positions_spec_rejects_comma_separated_status_string() -> None: + with pytest.raises(UserInputError, match="status"): + data_actions.list_combo_positions_spec( + user="0xWALLET", + status="RESOLVED_WIN,RESOLVED_LOSS", # type: ignore[arg-type] + ) + + def test_list_combo_positions_spec_rejects_non_combo_condition_id() -> None: with pytest.raises(UserInputError, match="combo condition ID"): data_actions.list_combo_positions_spec(user="0xWALLET", condition_id=_CTF_CONDITION_ID)