Skip to content

Commit 48d6dde

Browse files
committed
feat(grpc): add GrpcIndex.delete_namespace (LSB-0051)
## Purpose `GrpcIndex` was missing `delete_namespace`, blocking backcompat parity with the legacy `GRPCIndex`. Users need to delete namespaces via the gRPC path without falling back to REST. ## Solution - Added `delete_namespace` to `GrpcChannelProtocol` in `_protocol.py` so mypy can verify all call sites. - Added `GrpcIndex.delete_namespace` in `grpc/__init__.py` mirroring the signature and validation logic of `Index.delete_namespace`: keyword-only `name=`, legacy `namespace=` alias via `**kwargs`, `TypeError` on unexpected kwargs, `ValidationError` on both-provided or empty/whitespace name. - Delegates to `self._channel.delete_namespace(effective, timeout_s=timeout)`; returns `None` (Rust side returns an empty dict which is discarded). - 6 unit tests covering success, legacy alias, both-kwargs error, positional TypeError, empty name, and timeout forwarding. - Integration test `test_namespace_crud_lifecycle_grpc` covering the full create/describe/list/delete round-trip via `grpc=True`.
1 parent bda17a1 commit 48d6dde

4 files changed

Lines changed: 154 additions & 0 deletions

File tree

‎pinecone/grpc/__init__.py‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1381,6 +1381,42 @@ def describe_namespace(
13811381
indexed_fields=indexed_fields,
13821382
)
13831383

1384+
def delete_namespace(
1385+
self,
1386+
*,
1387+
name: str | None = None,
1388+
timeout: float | None = None,
1389+
**kwargs: str,
1390+
) -> None:
1391+
"""Delete a namespace by name, removing all its vectors.
1392+
1393+
Args:
1394+
name (str): Name of the namespace to delete.
1395+
timeout (float | None): Per-call timeout in seconds.
1396+
1397+
Returns:
1398+
None — a successful delete returns no payload.
1399+
1400+
Raises:
1401+
:exc:`ValidationError`: If the name is not a string or is empty/whitespace.
1402+
:exc:`TypeError`: If unexpected keyword arguments are passed.
1403+
"""
1404+
legacy_namespace: str | None = kwargs.pop("namespace", None)
1405+
if kwargs:
1406+
raise TypeError(
1407+
f"delete_namespace() got unexpected keyword arguments: {sorted(kwargs)!r}"
1408+
)
1409+
if name is not None and legacy_namespace is not None:
1410+
raise ValidationError("Provide either name= or namespace=, not both")
1411+
effective: str = name if name is not None else (legacy_namespace or "")
1412+
if not isinstance(effective, str):
1413+
raise ValidationError("namespace name must be a string")
1414+
if not effective or not effective.strip():
1415+
raise ValidationError("namespace name must be a non-empty string")
1416+
1417+
logger.info("Deleting namespace %r via gRPC", effective)
1418+
self._channel.delete_namespace(effective, timeout_s=timeout)
1419+
13841420
def close(self) -> None:
13851421
"""Close the underlying gRPC channel, REST client, and release resources."""
13861422
self._executor.shutdown(wait=True)

‎pinecone/grpc/_protocol.py‎

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

113+
def delete_namespace(
114+
self,
115+
namespace: str,
116+
*,
117+
timeout_s: float | None = None,
118+
) -> None:
119+
"""Delete a namespace."""
120+
...
121+
113122
def close(self) -> None:
114123
"""Close the channel and release resources."""
115124
...

‎tests/integration/test_data_plane.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,56 @@ def test_namespace_crud_lifecycle_rest(client: Pinecone, shared_index_dim2: str)
943943
assert ns_name not in post_names
944944

945945

946+
# ---------------------------------------------------------------------------
947+
# namespace CRUD — gRPC
948+
# ---------------------------------------------------------------------------
949+
950+
951+
@pytest.mark.integration
952+
def test_namespace_crud_lifecycle_grpc(client: Pinecone, shared_index_dim2: str) -> None:
953+
# shared_index_dim2
954+
"""GrpcIndex create_namespace / describe_namespace / list_namespaces_paginated / delete_namespace round-trip.
955+
956+
Verifies claims:
957+
- unified-ns-0001: Can create a named namespace via gRPC.
958+
- unified-ns-0002: Creation returns name and record_count == 0.
959+
- unified-ns-0003: Can describe a namespace by name via gRPC.
960+
- unified-ns-0004: Can delete a namespace by name via gRPC; returns None.
961+
- unified-ns-0005: Can list namespaces via gRPC with optional prefix filtering.
962+
"""
963+
ns = f"ns-{uuid.uuid4().hex[:8]}"
964+
ns_name = f"{ns}-grpc"
965+
index: GrpcIndex = client.index(name=shared_index_dim2, grpc=True)
966+
967+
# 1. Create namespace — returns NamespaceDescription with record_count == 0
968+
created = index.create_namespace(name=ns_name)
969+
assert isinstance(created, NamespaceDescription)
970+
assert created.name == ns_name
971+
assert created.record_count == 0 # unified-ns-0002
972+
973+
# 2. Describe namespace — returns same details
974+
described = index.describe_namespace(name=ns_name)
975+
assert isinstance(described, NamespaceDescription)
976+
assert described.name == ns_name
977+
assert isinstance(described.record_count, int)
978+
979+
# 3. Namespace appears in list_namespaces_paginated with prefix match
980+
list_resp = index.list_namespaces_paginated(prefix=f"{ns}-", limit=100)
981+
assert isinstance(list_resp, ListNamespacesResponse)
982+
ns_names = [ns_item.name for ns_item in list_resp.namespaces]
983+
assert ns_name in ns_names
984+
985+
# 4. Delete namespace — returns None on success (unified-ns-0004)
986+
result = index.delete_namespace(name=ns_name)
987+
assert result is None
988+
989+
# 5. After deletion, namespace no longer appears in listing
990+
post_delete = index.list_namespaces_paginated(prefix=f"{ns}-", limit=100)
991+
assert isinstance(post_delete, ListNamespacesResponse)
992+
post_names = [ns_item.name for ns_item in post_delete.namespaces]
993+
assert ns_name not in post_names
994+
995+
946996
# ---------------------------------------------------------------------------
947997
# list_namespaces generator — REST sync
948998
# ---------------------------------------------------------------------------

‎tests/unit/grpc/test_grpc_index.py‎

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,3 +131,62 @@ def test_grpc_describe_namespace_passes_timeout(self) -> None:
131131
idx.describe_namespace(name="ns1", timeout=5.0)
132132

133133
mock_channel.describe_namespace.assert_called_once_with("ns1", timeout_s=5.0)
134+
135+
136+
class TestGrpcDeleteNamespace:
137+
def test_grpc_delete_namespace_returns_none(self) -> None:
138+
mock_channel = MagicMock()
139+
mock_channel.delete_namespace.return_value = None
140+
idx = _make_grpc_index(mock_channel)
141+
142+
result = idx.delete_namespace(name="movies")
143+
144+
mock_channel.delete_namespace.assert_called_once_with("movies", timeout_s=None)
145+
assert result is None
146+
147+
def test_grpc_delete_namespace_accepts_legacy_namespace_kwarg(self) -> None:
148+
mock_channel = MagicMock()
149+
mock_channel.delete_namespace.return_value = None
150+
idx = _make_grpc_index(mock_channel)
151+
152+
result = idx.delete_namespace(namespace="movies") # type: ignore[call-arg]
153+
154+
mock_channel.delete_namespace.assert_called_once_with("movies", timeout_s=None)
155+
assert result is None
156+
157+
def test_grpc_delete_namespace_both_kwargs_raise(self) -> None:
158+
from pinecone.errors.exceptions import ValidationError
159+
160+
mock_channel = MagicMock()
161+
idx = _make_grpc_index(mock_channel)
162+
163+
with pytest.raises(ValidationError, match=r"name=.*namespace="):
164+
idx.delete_namespace(name="movies", namespace="movies") # type: ignore[call-arg]
165+
166+
def test_grpc_delete_namespace_positional_name_raises(self) -> None:
167+
mock_channel = MagicMock()
168+
idx = _make_grpc_index(mock_channel)
169+
170+
with pytest.raises(TypeError):
171+
idx.delete_namespace("my-ns") # type: ignore[misc]
172+
173+
def test_grpc_delete_namespace_empty_name_raises(self) -> None:
174+
from pinecone.errors.exceptions import ValidationError
175+
176+
mock_channel = MagicMock()
177+
idx = _make_grpc_index(mock_channel)
178+
179+
with pytest.raises(ValidationError, match="non-empty"):
180+
idx.delete_namespace(name="")
181+
182+
with pytest.raises(ValidationError, match="non-empty"):
183+
idx.delete_namespace(name=" ")
184+
185+
def test_grpc_delete_namespace_passes_timeout(self) -> None:
186+
mock_channel = MagicMock()
187+
mock_channel.delete_namespace.return_value = None
188+
idx = _make_grpc_index(mock_channel)
189+
190+
idx.delete_namespace(name="ns1", timeout=3.0)
191+
192+
mock_channel.delete_namespace.assert_called_once_with("ns1", timeout_s=3.0)

0 commit comments

Comments
 (0)