Skip to content

Commit 390994a

Browse files
committed
feat(grpc): add GrpcIndex.list_namespaces_paginated
Exposes the ListNamespaces gRPC RPC on GrpcIndex by: - Adding list_namespaces() to GrpcChannelProtocol in _protocol.py - Implementing list_namespaces_paginated() on GrpcIndex, converting the Rust channel dict response into a ListNamespacesResponse - Importing ListNamespacesResponse in grpc/__init__.py - Adding 6 unit tests covering response parsing, param forwarding, and pagination token handling
1 parent 48d6dde commit 390994a

3 files changed

Lines changed: 167 additions & 1 deletion

File tree

‎pinecone/grpc/__init__.py‎

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from pinecone.grpc.future import PineconeFuture
2929
from pinecone.models.namespaces.models import (
3030
IndexedFields,
31+
ListNamespacesResponse,
3132
NamespaceDescription,
3233
NamespaceFieldConfig,
3334
NamespaceSchema,
@@ -1322,6 +1323,69 @@ def search_records(
13221323
timeout=timeout,
13231324
)
13241325

1326+
def list_namespaces_paginated(
1327+
self,
1328+
*,
1329+
prefix: str | None = None,
1330+
limit: int | None = None,
1331+
pagination_token: str | None = None,
1332+
timeout: float | None = None,
1333+
) -> ListNamespacesResponse:
1334+
"""Fetch a single page of namespace descriptions via gRPC.
1335+
1336+
Args:
1337+
prefix (str | None): Return only namespaces whose names start with this prefix.
1338+
limit (int | None): Maximum number of namespaces to return in this page.
1339+
pagination_token (str | None): Token from a previous response to fetch the next page.
1340+
timeout (float | None): Per-call timeout in seconds.
1341+
1342+
Returns:
1343+
:class:`ListNamespacesResponse` with namespace descriptions, pagination info,
1344+
and total count.
1345+
"""
1346+
logger.info("Listing namespaces (paginated) via gRPC")
1347+
result = self._channel.list_namespaces(
1348+
prefix=prefix,
1349+
limit=limit,
1350+
pagination_token=pagination_token,
1351+
timeout_s=timeout,
1352+
)
1353+
1354+
namespaces: list[NamespaceDescription] = []
1355+
for ns_data in result.get("namespaces", []):
1356+
schema: NamespaceSchema | None = None
1357+
raw_schema = ns_data.get("schema")
1358+
if raw_schema is not None:
1359+
schema = NamespaceSchema(
1360+
fields={
1361+
k: NamespaceFieldConfig(filterable=v["filterable"])
1362+
for k, v in raw_schema.get("fields", {}).items()
1363+
}
1364+
)
1365+
indexed_fields: IndexedFields | None = None
1366+
raw_indexed = ns_data.get("indexed_fields")
1367+
if raw_indexed is not None:
1368+
indexed_fields = IndexedFields(fields=list(raw_indexed))
1369+
namespaces.append(
1370+
NamespaceDescription(
1371+
name=ns_data.get("name", ""),
1372+
record_count=ns_data.get("record_count", 0),
1373+
schema=schema,
1374+
indexed_fields=indexed_fields,
1375+
)
1376+
)
1377+
1378+
pagination: Pagination | None = None
1379+
raw_pag = result.get("pagination")
1380+
if raw_pag is not None:
1381+
pagination = Pagination(next=raw_pag.get("next"))
1382+
1383+
return ListNamespacesResponse(
1384+
namespaces=namespaces,
1385+
pagination=pagination,
1386+
total_count=result.get("total_count", 0),
1387+
)
1388+
13251389
def describe_namespace(
13261390
self,
13271391
*,

‎pinecone/grpc/_protocol.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,17 @@ def describe_namespace(
110110
"""Describe a namespace."""
111111
...
112112

113+
def list_namespaces(
114+
self,
115+
*,
116+
pagination_token: str | None = None,
117+
limit: int | None = None,
118+
prefix: str | None = None,
119+
timeout_s: float | None = None,
120+
) -> dict[str, Any]:
121+
"""List namespaces (single page)."""
122+
...
123+
113124
def delete_namespace(
114125
self,
115126
namespace: str,

‎tests/unit/grpc/test_grpc_index.py‎

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import pinecone.grpc
1010
from pinecone.grpc import GRPCIndex, GrpcIndex
11-
from pinecone.models.namespaces.models import NamespaceDescription
11+
from pinecone.models.namespaces.models import ListNamespacesResponse, NamespaceDescription
1212

1313
_MOCK_GRPC_MODULE_PATH = "pinecone._grpc"
1414

@@ -190,3 +190,94 @@ def test_grpc_delete_namespace_passes_timeout(self) -> None:
190190
idx.delete_namespace(name="ns1", timeout=3.0)
191191

192192
mock_channel.delete_namespace.assert_called_once_with("ns1", timeout_s=3.0)
193+
194+
195+
class TestGrpcListNamespacesPaginated:
196+
def test_grpc_list_namespaces_paginated_routes_to_channel(self) -> None:
197+
mock_channel = MagicMock()
198+
mock_channel.list_namespaces.return_value = {
199+
"namespaces": [{"name": "ns1", "record_count": 5}],
200+
"total_count": 1,
201+
}
202+
idx = _make_grpc_index(mock_channel)
203+
204+
result = idx.list_namespaces_paginated()
205+
206+
assert isinstance(result, ListNamespacesResponse)
207+
assert len(result.namespaces) == 1
208+
assert isinstance(result.namespaces[0], NamespaceDescription)
209+
assert result.namespaces[0].name == "ns1"
210+
assert result.namespaces[0].record_count == 5
211+
assert result.total_count == 1
212+
assert result.pagination is None
213+
214+
def test_grpc_list_namespaces_paginated_prefix_forwarded(self) -> None:
215+
mock_channel = MagicMock()
216+
mock_channel.list_namespaces.return_value = {"namespaces": [], "total_count": 0}
217+
idx = _make_grpc_index(mock_channel)
218+
219+
idx.list_namespaces_paginated(prefix="prod-")
220+
221+
mock_channel.list_namespaces.assert_called_once_with(
222+
prefix="prod-",
223+
limit=None,
224+
pagination_token=None,
225+
timeout_s=None,
226+
)
227+
228+
def test_grpc_list_namespaces_paginated_pagination_token_forwarded(self) -> None:
229+
mock_channel = MagicMock()
230+
mock_channel.list_namespaces.return_value = {"namespaces": [], "total_count": 0}
231+
idx = _make_grpc_index(mock_channel)
232+
233+
idx.list_namespaces_paginated(pagination_token="tok123")
234+
235+
mock_channel.list_namespaces.assert_called_once_with(
236+
prefix=None,
237+
limit=None,
238+
pagination_token="tok123",
239+
timeout_s=None,
240+
)
241+
242+
def test_grpc_list_namespaces_paginated_limit_forwarded(self) -> None:
243+
mock_channel = MagicMock()
244+
mock_channel.list_namespaces.return_value = {"namespaces": [], "total_count": 0}
245+
idx = _make_grpc_index(mock_channel)
246+
247+
idx.list_namespaces_paginated(limit=10)
248+
249+
mock_channel.list_namespaces.assert_called_once_with(
250+
prefix=None,
251+
limit=10,
252+
pagination_token=None,
253+
timeout_s=None,
254+
)
255+
256+
def test_grpc_list_namespaces_paginated_pagination_next_parsed(self) -> None:
257+
mock_channel = MagicMock()
258+
mock_channel.list_namespaces.return_value = {
259+
"namespaces": [{"name": "a", "record_count": 1}],
260+
"pagination": {"next": "nexttoken"},
261+
"total_count": 5,
262+
}
263+
idx = _make_grpc_index(mock_channel)
264+
265+
result = idx.list_namespaces_paginated()
266+
267+
assert result.pagination is not None
268+
assert result.pagination.next == "nexttoken"
269+
assert result.total_count == 5
270+
271+
def test_grpc_list_namespaces_paginated_timeout_forwarded(self) -> None:
272+
mock_channel = MagicMock()
273+
mock_channel.list_namespaces.return_value = {"namespaces": [], "total_count": 0}
274+
idx = _make_grpc_index(mock_channel)
275+
276+
idx.list_namespaces_paginated(timeout=2.5)
277+
278+
mock_channel.list_namespaces.assert_called_once_with(
279+
prefix=None,
280+
limit=None,
281+
pagination_token=None,
282+
timeout_s=2.5,
283+
)

0 commit comments

Comments
 (0)