Skip to content

Commit 8696a92

Browse files
committed
REST: Add pagination support for list_namespaces
1 parent 6e68c9c commit 8696a92

2 files changed

Lines changed: 100 additions & 12 deletions

File tree

pyiceberg/catalog/rest/__init__.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ def _parse_endpoints(cls, v: list[str] | None) -> set[Endpoint] | None:
334334

335335
class ListNamespaceResponse(IcebergBaseModel):
336336
namespaces: list[Identifier] = Field()
337+
next_page_token: str | None = Field(default=None, alias="next-page-token")
337338

338339

339340
class NamespaceResponse(IcebergBaseModel):
@@ -1182,19 +1183,37 @@ def drop_namespace(self, namespace: str | Identifier) -> None:
11821183
def list_namespaces(self, namespace: str | Identifier = ()) -> list[Identifier]:
11831184
self._check_endpoint(Capability.V1_LIST_NAMESPACES)
11841185
namespace_tuple = self.identifier_to_tuple(namespace)
1185-
response = self._session.get(
1186-
self.url(
1187-
f"{Endpoints.list_namespaces}?parent={self._encode_namespace_path(namespace_tuple)}"
1188-
if namespace_tuple
1189-
else Endpoints.list_namespaces
1190-
),
1191-
)
1192-
try:
1193-
response.raise_for_status()
1194-
except HTTPError as exc:
1195-
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
11961186

1197-
return ListNamespaceResponse.model_validate_json(response.text).namespaces
1187+
all_namespaces: list[Identifier] = []
1188+
page_token: str | None = None
1189+
1190+
while True:
1191+
# Build URL with pagination params
1192+
if namespace_tuple:
1193+
base_url = f"{Endpoints.list_namespaces}?parent={self._encode_namespace_path(namespace_tuple)}"
1194+
separator = "&"
1195+
else:
1196+
base_url = Endpoints.list_namespaces
1197+
separator = "?"
1198+
1199+
# Add page token if present
1200+
url = f"{base_url}{separator}pageToken={page_token}" if page_token else base_url
1201+
1202+
response = self._session.get(self.url(url))
1203+
try:
1204+
response.raise_for_status()
1205+
except HTTPError as exc:
1206+
_handle_non_200_response(exc, {404: NoSuchNamespaceError})
1207+
1208+
parsed = ListNamespaceResponse.model_validate_json(response.text)
1209+
all_namespaces.extend(parsed.namespaces)
1210+
1211+
# Check if more pages exist
1212+
if not parsed.next_page_token:
1213+
break
1214+
page_token = parsed.next_page_token
1215+
1216+
return all_namespaces
11981217

11991218
@retry(**_RETRY_ARGS)
12001219
def load_namespace_properties(self, namespace: str | Identifier) -> Properties:

tests/catalog/test_rest.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,75 @@ def test_list_namespace_with_parent_200(rest_mock: Mocker) -> None:
736736
]
737737

738738

739+
def test_list_namespaces_paginated_200(rest_mock: Mocker) -> None:
740+
# First page with next-page-token
741+
rest_mock.get(
742+
f"{TEST_URI}v1/namespaces",
743+
json={
744+
"namespaces": [["ns1"], ["ns2"]],
745+
"next-page-token": "page2token",
746+
},
747+
status_code=200,
748+
request_headers=TEST_HEADERS,
749+
)
750+
# Second page with next-page-token
751+
rest_mock.get(
752+
f"{TEST_URI}v1/namespaces?pageToken=page2token",
753+
json={
754+
"namespaces": [["ns3"], ["ns4"]],
755+
"next-page-token": "page3token",
756+
},
757+
status_code=200,
758+
request_headers=TEST_HEADERS,
759+
)
760+
# Third page without next-page-token (last page)
761+
rest_mock.get(
762+
f"{TEST_URI}v1/namespaces?pageToken=page3token",
763+
json={
764+
"namespaces": [["ns5"]],
765+
},
766+
status_code=200,
767+
request_headers=TEST_HEADERS,
768+
)
769+
770+
result = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).list_namespaces()
771+
assert result == [
772+
("ns1",),
773+
("ns2",),
774+
("ns3",),
775+
("ns4",),
776+
("ns5",),
777+
]
778+
779+
780+
def test_list_namespaces_with_parent_paginated_200(rest_mock: Mocker) -> None:
781+
# First page
782+
rest_mock.get(
783+
f"{TEST_URI}v1/namespaces?parent=accounting",
784+
json={
785+
"namespaces": [["accounting", "tax"]],
786+
"next-page-token": "page2",
787+
},
788+
status_code=200,
789+
request_headers=TEST_HEADERS,
790+
)
791+
# Second page (last)
792+
rest_mock.get(
793+
f"{TEST_URI}v1/namespaces?parent=accounting&pageToken=page2",
794+
json={
795+
"namespaces": [["accounting", "payroll"]],
796+
},
797+
status_code=200,
798+
request_headers=TEST_HEADERS,
799+
)
800+
801+
result = RestCatalog("rest", uri=TEST_URI, token=TEST_TOKEN).list_namespaces(("accounting",))
802+
assert result == [
803+
("accounting", "tax"),
804+
("accounting", "payroll"),
805+
]
806+
807+
739808
def test_list_namespace_with_parent_404(rest_mock: Mocker) -> None:
740809
rest_mock.get(
741810
f"{TEST_URI}v1/namespaces?parent=some_namespace",

0 commit comments

Comments
 (0)