From 43c3d69c20f19397c24cdc9546ed8ffc14f1adf7 Mon Sep 17 00:00:00 2001 From: Yuya Ebihara Date: Sat, 9 May 2026 10:08:40 +0900 Subject: [PATCH] REST: Add pagination support for list_views --- pyiceberg/catalog/rest/__init__.py | 32 ++++++++++++++++---- tests/catalog/test_rest.py | 48 ++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 6 deletions(-) diff --git a/pyiceberg/catalog/rest/__init__.py b/pyiceberg/catalog/rest/__init__.py index ca0ff75e8c..aa2621081c 100644 --- a/pyiceberg/catalog/rest/__init__.py +++ b/pyiceberg/catalog/rest/__init__.py @@ -370,6 +370,7 @@ class ListTablesResponse(IcebergBaseModel): class ListViewsResponse(IcebergBaseModel): identifiers: list[ListViewResponseEntry] = Field() + next_page_token: str | None = Field(default=None, alias="next-page-token") _PLANNING_RESPONSE_ADAPTER = TypeAdapter(PlanningResponse) @@ -1102,12 +1103,31 @@ def list_views(self, namespace: str | Identifier) -> list[Identifier]: return [] namespace_tuple = self._check_valid_namespace_identifier(namespace) namespace_concat = self._encode_namespace_path(namespace_tuple) - response = self._session.get(self.url(Endpoints.list_views, namespace=namespace_concat)) - try: - response.raise_for_status() - except HTTPError as exc: - _handle_non_200_response(exc, {404: NoSuchNamespaceError}) - return [(*view.namespace, view.name) for view in ListViewsResponse.model_validate_json(response.text).identifiers] + + all_identifiers: list[Identifier] = [] + page_token: str | None = None + + while True: + # Build URL with pagination params + url = self.url(Endpoints.list_views, namespace=namespace_concat) + if page_token: + url = f"{url}?pageToken={page_token}" + + response = self._session.get(url) + try: + response.raise_for_status() + except HTTPError as exc: + _handle_non_200_response(exc, {404: NoSuchNamespaceError}) + + parsed = ListViewsResponse.model_validate_json(response.text) + all_identifiers.extend([(*view.namespace, view.name) for view in parsed.identifiers]) + + # Check if more pages exist + if not parsed.next_page_token: + break + page_token = parsed.next_page_token + + return all_identifiers @retry(**_RETRY_ARGS) def commit_table( diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py index 7977892635..2ff875aa86 100644 --- a/tests/catalog/test_rest.py +++ b/tests/catalog/test_rest.py @@ -639,6 +639,54 @@ def test_list_views_200(rest_mock: Mocker) -> None: assert RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).list_views(namespace) == [("examples", "fooshare")] +def test_list_views_paginated_200(rest_mock: Mocker) -> None: + namespace = "examples" + # First page with next-page-token + rest_mock.get( + f"{TEST_URI}v1/namespaces/{namespace}/views", + json={ + "identifiers": [ + {"namespace": ["examples"], "name": "view1"}, + {"namespace": ["examples"], "name": "view2"}, + ], + "next-page-token": "page2token", + }, + status_code=200, + request_headers=TEST_HEADERS, + ) + # Second page with next-page-token + rest_mock.get( + f"{TEST_URI}v1/namespaces/{namespace}/views?pageToken=page2token", + json={ + "identifiers": [ + {"namespace": ["examples"], "name": "view3"}, + ], + "next-page-token": "page3token", + }, + status_code=200, + request_headers=TEST_HEADERS, + ) + # Third page without next-page-token (last page) + rest_mock.get( + f"{TEST_URI}v1/namespaces/{namespace}/views?pageToken=page3token", + json={ + "identifiers": [ + {"namespace": ["examples"], "name": "view4"}, + ], + }, + status_code=200, + request_headers=TEST_HEADERS, + ) + + result = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).list_views(namespace) + assert result == [ + ("examples", "view1"), + ("examples", "view2"), + ("examples", "view3"), + ("examples", "view4"), + ] + + def test_list_views_200_sigv4(rest_mock: Mocker) -> None: namespace = "examples" rest_mock.get(