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(