This is an automated email from the ASF dual-hosted git repository.

Fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git


The following commit(s) were added to refs/heads/main by this push:
     new 2f791166 REST: Add pagination support for `list_views` (#3349)
2f791166 is described below

commit 2f7911666d280599068e44eb588d92da14e10f8d
Author: Yuya Ebihara <[email protected]>
AuthorDate: Sat May 16 03:20:17 2026 +0900

    REST: Add pagination support for `list_views` (#3349)
    
    <!--
    Thanks for opening a pull request!
    -->
    
    <!-- In the case this PR will resolve an issue, please replace
    ${GITHUB_ISSUE_ID} below with the actual Github issue id. -->
    <!-- Closes #${GITHUB_ISSUE_ID} -->
    
    # Rationale for this change
    
    Follows REST catalog spec:
    
    - /v1/{prefix}/views:
    
https://github.com/apache/iceberg/blob/e7a5a87f26f9de5b200254155aa037368b13a29c/open-api/rest-catalog-open-api.yaml#L1525-L1562
    - ListTablesResponse:
    
https://github.com/apache/iceberg/blob/e7a5a87f26f9de5b200254155aa037368b13a29c/open-api/rest-catalog-open-api.yaml#L4210-L4219
    - PageToken:
    
https://github.com/apache/iceberg/blob/e7a5a87f26f9de5b200254155aa037368b13a29c/open-api/rest-catalog-open-api.yaml#L2233-L2256
    
    ## Are these changes tested?
    
    Yes, includes unit tests for paginated cases.
    
    ## Are there any user-facing changes?
    
    - Before: returned incomplete view list when server paginated (only
    first page)
    - After: returns complete view list (fetches all pages)
    
    <!-- In the case of user-facing changes, please add the changelog label.
    -->
---
 pyiceberg/catalog/rest/__init__.py | 28 ++++++++++---
 tests/catalog/test_rest.py         | 84 ++++++++++++++++++++++++++++++++++++++
 2 files changed, 106 insertions(+), 6 deletions(-)

diff --git a/pyiceberg/catalog/rest/__init__.py 
b/pyiceberg/catalog/rest/__init__.py
index 8c5647e7..1832f6e1 100644
--- a/pyiceberg/catalog/rest/__init__.py
+++ b/pyiceberg/catalog/rest/__init__.py
@@ -380,6 +380,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)
@@ -1112,12 +1113,27 @@ class RestCatalog(Catalog):
             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]
+        url = self.url(Endpoints.list_views, namespace=namespace_concat)
+
+        views: list[Identifier] = []
+        page_token: str | None = None
+
+        while True:
+            params = {"pageToken": page_token} if page_token else None
+            response = self._session.get(url, params=params)
+            try:
+                response.raise_for_status()
+            except HTTPError as exc:
+                _handle_non_200_response(exc, {404: NoSuchNamespaceError})
+
+            parsed = ListViewsResponse.model_validate_json(response.text)
+            views.extend([(*view.namespace, view.name) for view in 
parsed.identifiers])
+
+            if not parsed.next_page_token:
+                break
+            page_token = parsed.next_page_token
+
+        return views
 
     @retry(**_RETRY_ARGS)
     def load_view(self, identifier: str | Identifier) -> View:
diff --git a/tests/catalog/test_rest.py b/tests/catalog/test_rest.py
index edbe67d9..2adfe9f0 100644
--- a/tests/catalog/test_rest.py
+++ b/tests/catalog/test_rest.py
@@ -641,6 +641,90 @@ 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_paginated_200_none_next_page_token(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,
+    )
+    # The last page with NONE next-page-token
+    rest_mock.get(
+        f"{TEST_URI}v1/namespaces/{namespace}/views?pageToken=page2token",
+        json={
+            "identifiers": [
+                {"namespace": ["examples"], "name": "view3"},
+            ],
+            "next-page-token": None,
+        },
+        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"),
+    ]
+
+
 def test_list_views_200_sigv4(rest_mock: Mocker) -> None:
     namespace = "examples"
     rest_mock.get(

Reply via email to