Skip to content

Commit 3a039ce

Browse files
committed
feat(grpc): add GrpcIndex.list_namespaces auto-paginating generator
Adds GrpcIndex.list_namespaces() that composes on top of list_namespaces_paginated() to auto-follow pagination tokens, matching the yield-whole-pages semantics of Index.list_namespaces. Closes LSB-0053
1 parent 390994a commit 3a039ce

3 files changed

Lines changed: 184 additions & 0 deletions

File tree

‎pinecone/grpc/__init__.py‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,49 @@ def list_namespaces_paginated(
13861386
total_count=result.get("total_count", 0),
13871387
)
13881388

1389+
def list_namespaces(
1390+
self,
1391+
*,
1392+
prefix: str | None = None,
1393+
limit: int | None = None,
1394+
timeout: float | None = None,
1395+
) -> Iterator[ListNamespacesResponse]:
1396+
"""List namespaces, automatically following pagination.
1397+
1398+
Yields one :class:`ListNamespacesResponse` per page. The generator
1399+
automatically follows pagination tokens until all pages have been
1400+
retrieved.
1401+
1402+
Args:
1403+
prefix (str | None): Return only namespaces whose names start with this prefix.
1404+
limit (int | None): Maximum number of namespaces to return per page.
1405+
timeout (float | None): Per-call timeout in seconds.
1406+
1407+
Yields:
1408+
:class:`ListNamespacesResponse` for each page of results.
1409+
1410+
Examples:
1411+
.. code-block:: python
1412+
1413+
for page in idx.list_namespaces(prefix="prod-"):
1414+
for ns in page.namespaces:
1415+
print(ns.name, ns.record_count)
1416+
"""
1417+
pagination_token: str | None = None
1418+
while True:
1419+
page = self.list_namespaces_paginated(
1420+
prefix=prefix,
1421+
limit=limit,
1422+
pagination_token=pagination_token,
1423+
timeout=timeout,
1424+
)
1425+
if page.namespaces:
1426+
yield page
1427+
if page.pagination is not None and page.pagination.next is not None:
1428+
pagination_token = page.pagination.next
1429+
else:
1430+
break
1431+
13891432
def describe_namespace(
13901433
self,
13911434
*,

‎tests/integration/test_data_plane.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,6 +1045,57 @@ def test_list_namespaces_generator_rest(client: Pinecone, shared_index_dim2: str
10451045
assert isinstance(ns_item.record_count, int) and ns_item.record_count >= 0
10461046

10471047

1048+
# ---------------------------------------------------------------------------
1049+
# list_namespaces generator — gRPC
1050+
# ---------------------------------------------------------------------------
1051+
1052+
1053+
@pytest.mark.integration
1054+
def test_list_namespaces_generator_grpc(client: Pinecone, shared_index_dim2: str) -> None:
1055+
# shared_index_dim2
1056+
"""list_namespaces() generator on GrpcIndex yields ListNamespacesResponse pages;
1057+
generator follows pagination tokens automatically until exhausted.
1058+
1059+
Verifies claims:
1060+
- unified-ns-0007 (gRPC): The namespace list generator yields ListNamespacesResponse
1061+
pages; the paginated method returns one page (not auto-paginating).
1062+
"""
1063+
ns = f"ns-{uuid.uuid4().hex[:8]}"
1064+
ns_a = f"{ns}-a"
1065+
ns_b = f"{ns}-b"
1066+
index = client.index(name=shared_index_dim2, grpc=True)
1067+
1068+
# Upsert one vector into each namespace to create them implicitly
1069+
index.upsert(vectors=[{"id": "lnsg-v1", "values": [0.1, 0.2]}], namespace=ns_a)
1070+
index.upsert(vectors=[{"id": "lnsg-v2", "values": [0.3, 0.4]}], namespace=ns_b)
1071+
1072+
# Poll until both namespaces appear
1073+
poll_until(
1074+
query_fn=lambda: index.list_namespaces_paginated(prefix=f"{ns}-", limit=100),
1075+
check_fn=lambda r: len(r.namespaces) >= 2,
1076+
timeout=120,
1077+
description="both test namespaces appear via list_namespaces_paginated (grpc)",
1078+
)
1079+
1080+
# --- Exercise the generator ---
1081+
pages = list(index.list_namespaces(prefix=f"{ns}-"))
1082+
assert len(pages) >= 1, "list_namespaces() generator must yield at least one page"
1083+
1084+
# Collect all namespace names across all yielded pages
1085+
all_ns_names = [ns_item.name for page in pages for ns_item in page.namespaces]
1086+
assert ns_a in all_ns_names, f"Expected {ns_a!r} in generator output; got {all_ns_names}"
1087+
assert ns_b in all_ns_names, f"Expected {ns_b!r} in generator output; got {all_ns_names}"
1088+
1089+
# Verify shape of every yielded page and its namespace descriptions
1090+
for page in pages:
1091+
assert isinstance(page, ListNamespacesResponse)
1092+
assert len(page.namespaces) >= 1, "Each yielded page must contain at least one namespace"
1093+
for ns_item in page.namespaces:
1094+
assert isinstance(ns_item, NamespaceDescription)
1095+
assert isinstance(ns_item.name, str) and ns_item.name.startswith(f"{ns}-")
1096+
assert isinstance(ns_item.record_count, int) and ns_item.record_count >= 0
1097+
1098+
10481099
# ---------------------------------------------------------------------------
10491100
# list_paginated multi-page — REST sync
10501101
# ---------------------------------------------------------------------------

‎tests/unit/grpc/test_grpc_index.py‎

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,93 @@ def test_grpc_list_namespaces_paginated_timeout_forwarded(self) -> None:
281281
pagination_token=None,
282282
timeout_s=2.5,
283283
)
284+
285+
286+
class TestGrpcListNamespaces:
287+
def test_grpc_list_namespaces_yields_pages_single(self) -> None:
288+
mock_channel = MagicMock()
289+
idx = _make_grpc_index(mock_channel)
290+
291+
page = ListNamespacesResponse(
292+
namespaces=[NamespaceDescription(name="a", record_count=1)],
293+
pagination=None,
294+
total_count=1,
295+
)
296+
idx.list_namespaces_paginated = MagicMock(return_value=page)
297+
298+
pages = list(idx.list_namespaces())
299+
300+
assert pages == [page]
301+
idx.list_namespaces_paginated.assert_called_once_with(
302+
prefix=None,
303+
limit=None,
304+
pagination_token=None,
305+
timeout=None,
306+
)
307+
308+
def test_grpc_list_namespaces_yields_pages_multi(self) -> None:
309+
mock_channel = MagicMock()
310+
idx = _make_grpc_index(mock_channel)
311+
312+
from pinecone.models.namespaces.models import Pagination
313+
314+
page1 = ListNamespacesResponse(
315+
namespaces=[NamespaceDescription(name="a", record_count=1)],
316+
pagination=Pagination(next="tok1"),
317+
total_count=2,
318+
)
319+
page2 = ListNamespacesResponse(
320+
namespaces=[NamespaceDescription(name="b", record_count=2)],
321+
pagination=None,
322+
total_count=2,
323+
)
324+
idx.list_namespaces_paginated = MagicMock(side_effect=[page1, page2])
325+
326+
pages = list(idx.list_namespaces())
327+
328+
assert pages == [page1, page2]
329+
assert idx.list_namespaces_paginated.call_count == 2
330+
second_call_kwargs = idx.list_namespaces_paginated.call_args_list[1].kwargs
331+
assert second_call_kwargs["pagination_token"] == "tok1"
332+
333+
def test_grpc_list_namespaces_skips_empty_pages(self) -> None:
334+
mock_channel = MagicMock()
335+
idx = _make_grpc_index(mock_channel)
336+
337+
from pinecone.models.namespaces.models import Pagination
338+
339+
empty_page = ListNamespacesResponse(
340+
namespaces=[],
341+
pagination=Pagination(next="tok1"),
342+
total_count=0,
343+
)
344+
final_page = ListNamespacesResponse(
345+
namespaces=[NamespaceDescription(name="a", record_count=1)],
346+
pagination=None,
347+
total_count=1,
348+
)
349+
idx.list_namespaces_paginated = MagicMock(side_effect=[empty_page, final_page])
350+
351+
pages = list(idx.list_namespaces())
352+
353+
assert pages == [final_page]
354+
355+
def test_grpc_list_namespaces_limit_forwarded(self) -> None:
356+
mock_channel = MagicMock()
357+
idx = _make_grpc_index(mock_channel)
358+
359+
page = ListNamespacesResponse(
360+
namespaces=[NamespaceDescription(name="a", record_count=1)],
361+
pagination=None,
362+
total_count=1,
363+
)
364+
idx.list_namespaces_paginated = MagicMock(return_value=page)
365+
366+
list(idx.list_namespaces(limit=5))
367+
368+
idx.list_namespaces_paginated.assert_called_once_with(
369+
prefix=None,
370+
limit=5,
371+
pagination_token=None,
372+
timeout=None,
373+
)

0 commit comments

Comments
 (0)