From cd627aa04085795a46b223faf51741b67a311a3a Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Wed, 27 Aug 2025 14:53:55 +0300 Subject: [PATCH 1/2] Add rename namespace api --- pyproject.toml | 2 +- tests/core/test_info.py | 4 +- tests/core/test_namespace_operations.py | 67 ++++++++++++++++++++++--- upstash_vector/__init__.py | 2 +- upstash_vector/core/index_operations.py | 41 +++++++++++++++ 5 files changed, 106 insertions(+), 10 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c62625f..e15e748 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "upstash-vector" -version = "0.8.1" +version = "0.8.2" description = "Serverless Vector SDK from Upstash" license = "MIT" authors = ["Upstash "] diff --git a/tests/core/test_info.py b/tests/core/test_info.py index c5800e3..fb12d41 100644 --- a/tests/core/test_info.py +++ b/tests/core/test_info.py @@ -20,7 +20,7 @@ def test_info(index: Index): assert info.pending_vector_count == 0 assert info.dimension == 2 assert info.similarity_function == "COSINE" - assert len(info.namespaces) == len(NAMESPACES) + assert len(info.namespaces) >= len(NAMESPACES) for ns in NAMESPACES: assert ns in info.namespaces assert info.namespaces[ns].vector_count == 0 @@ -78,7 +78,7 @@ async def test_info_async(async_index: AsyncIndex): assert info.pending_vector_count == 0 assert info.dimension == 2 assert info.similarity_function == "COSINE" - assert len(info.namespaces) == len(NAMESPACES) + assert len(info.namespaces) >= len(NAMESPACES) for ns in NAMESPACES: assert ns in info.namespaces assert info.namespaces[ns].vector_count == 0 diff --git a/tests/core/test_namespace_operations.py b/tests/core/test_namespace_operations.py index 566e63b..fc5126e 100644 --- a/tests/core/test_namespace_operations.py +++ b/tests/core/test_namespace_operations.py @@ -11,7 +11,7 @@ def test_list_namespaces(index: Index): all_ns = index.list_namespaces() - assert len(all_ns) == len(NAMESPACES) + assert len(all_ns) >= len(NAMESPACES) assert NAMESPACES[0] in all_ns assert NAMESPACES[1] in all_ns @@ -23,7 +23,7 @@ async def test_list_namespaces_async(async_index: AsyncIndex): all_ns = await async_index.list_namespaces() - assert len(all_ns) == len(NAMESPACES) + assert len(all_ns) >= len(NAMESPACES) assert NAMESPACES[0] in all_ns assert NAMESPACES[1] in all_ns @@ -32,17 +32,22 @@ def test_delete_namespaces(index: Index): for ns in NAMESPACES: ensure_ns_exists(index, ns) + deleted = None for ns in NAMESPACES: if ns == DEFAULT_NAMESPACE: continue + deleted = ns # Should not fail index.delete_namespace(namespace=ns) info = index.info() - # Only default namespace should exist - assert len(info.namespaces) == 1 + # default namespace should exist + assert len(info.namespaces) >= 1 + assert DEFAULT_NAMESPACE in info.namespaces + assert deleted is not None + assert deleted not in info.namespaces @pytest.mark.asyncio @@ -50,14 +55,64 @@ async def test_delete_namespaces_async(async_index: AsyncIndex): for ns in NAMESPACES: await ensure_ns_exists_async(async_index, ns) + deleted = None for ns in NAMESPACES: if ns == DEFAULT_NAMESPACE: continue + deleted = ns # Should not fail await async_index.delete_namespace(namespace=ns) info = await async_index.info() - # Only default namespace should exist - assert len(info.namespaces) == 1 + # default namespace should exist + assert len(info.namespaces) >= 1 + assert DEFAULT_NAMESPACE in info.namespaces + assert deleted is not None + assert deleted not in info.namespaces + + +def test_rename_namespace(index: Index): + try: + index.delete_namespace("old_name") + except Exception: + pass + + try: + index.delete_namespace("new_name") + except Exception: + pass + + ensure_ns_exists(index, "old_name") + + ok = index.rename_namespace("old_name", "new_name") + assert ok is True + + info = index.info() + + assert "new_name" in info.namespaces + assert "old_name" not in info.namespaces + + +@pytest.mark.asyncio +async def test_rename_namespace_async(async_index: AsyncIndex): + try: + await async_index.delete_namespace("old_name") + except Exception: + pass + + try: + await async_index.delete_namespace("new_name") + except Exception: + pass + + await ensure_ns_exists_async(async_index, "old_name") + + ok = await async_index.rename_namespace("old_name", "new_name") + assert ok is True + + info = await async_index.info() + + assert "new_name" in info.namespaces + assert "old_name" not in info.namespaces diff --git a/upstash_vector/__init__.py b/upstash_vector/__init__.py index 84abcdb..8d3845f 100644 --- a/upstash_vector/__init__.py +++ b/upstash_vector/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.8.1" +__version__ = "0.8.2" from upstash_vector.client import AsyncIndex, Index from upstash_vector.types import Vector diff --git a/upstash_vector/core/index_operations.py b/upstash_vector/core/index_operations.py index 02489bc..f7ac4ec 100644 --- a/upstash_vector/core/index_operations.py +++ b/upstash_vector/core/index_operations.py @@ -50,6 +50,7 @@ INFO_PATH = "/info" LIST_NAMESPACES_PATH = "/list-namespaces" DELETE_NAMESPACE_PATH = "/delete-namespace" +RENAME_NAMESPACE_PATH = "/rename-namespace" UPDATE_PATH = "/update" RESUMABLE_QUERY_PATH = "/resumable-query" RESUMABLE_QUERY_DATA_PATH = "/resumable-query-data" @@ -660,6 +661,26 @@ def delete_namespace(self, namespace: str) -> None: payload=None, path=_path_for(namespace, DELETE_NAMESPACE_PATH) ) + def rename_namespace(self, namespace: str, new_namespace: str) -> bool: + """ + Renames the given namespace. + + There should not be a namespace with the new namespace name. + + Returns whether the rename operation succeeded or not. + """ + payload = { + "namespace": namespace, + "newNamespace": new_namespace, + } + + result = self._execute_request( + payload=payload, + path=RENAME_NAMESPACE_PATH, + ) + + return result["renamed"] + class AsyncIndexOperations: async def _execute_request_async(self, payload, path): @@ -1267,6 +1288,26 @@ async def delete_namespace(self, namespace: str) -> None: payload=None, path=_path_for(namespace, DELETE_NAMESPACE_PATH) ) + async def rename_namespace(self, namespace: str, new_namespace: str) -> bool: + """ + Renames the given namespace. + + There should not be a namespace with the new namespace name. + + Returns whether the rename operation succeeded or not. + """ + payload = { + "namespace": namespace, + "newNamespace": new_namespace, + } + + result = await self._execute_request_async( + payload=payload, + path=RENAME_NAMESPACE_PATH, + ) + + return result["renamed"] + class ResumableQueryHandle: """ From 71d1f9c108c7c5bbf26b3c612a9294322219f1c9 Mon Sep 17 00:00:00 2001 From: Metin Dumandag <29387993+mdumandag@users.noreply.github.com> Date: Thu, 18 Sep 2025 09:08:06 +0300 Subject: [PATCH 2/2] add delete existing option --- pyproject.toml | 2 +- tests/core/test_namespace_operations.py | 39 ++++++++++++++++++------- upstash_vector/core/index_operations.py | 18 +++++++++--- 3 files changed, 44 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e15e748..c329943 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,7 @@ httpx = ">=0.23.0, <1" mypy = "^1.14.1" pytest = "^8.3.4" pytest-asyncio = "^0.24.0" -ruff = "^0.8.2" +ruff = "^0.13.0" numpy = [ { version = "^1.24.4", python = "<=3.8" }, { version = "^1.26.4", python = ">=3.9" } diff --git a/tests/core/test_namespace_operations.py b/tests/core/test_namespace_operations.py index fc5126e..74097d5 100644 --- a/tests/core/test_namespace_operations.py +++ b/tests/core/test_namespace_operations.py @@ -74,11 +74,6 @@ async def test_delete_namespaces_async(async_index: AsyncIndex): def test_rename_namespace(index: Index): - try: - index.delete_namespace("old_name") - except Exception: - pass - try: index.delete_namespace("new_name") except Exception: @@ -95,13 +90,21 @@ def test_rename_namespace(index: Index): assert "old_name" not in info.namespaces +def test_rename_namespace_existing(index: Index): + ensure_ns_exists(index, "old_name") + ensure_ns_exists(index, "new_name") + + ok = index.rename_namespace("old_name", "new_name", delete_existing=True) + assert ok is True + + info = index.info() + + assert "new_name" in info.namespaces + assert "old_name" not in info.namespaces + + @pytest.mark.asyncio async def test_rename_namespace_async(async_index: AsyncIndex): - try: - await async_index.delete_namespace("old_name") - except Exception: - pass - try: await async_index.delete_namespace("new_name") except Exception: @@ -116,3 +119,19 @@ async def test_rename_namespace_async(async_index: AsyncIndex): assert "new_name" in info.namespaces assert "old_name" not in info.namespaces + + +@pytest.mark.asyncio +async def test_rename_namespace_existing_async(async_index: AsyncIndex): + await ensure_ns_exists_async(async_index, "old_name") + await ensure_ns_exists_async(async_index, "new_name") + + ok = await async_index.rename_namespace( + "old_name", "new_name", delete_existing=True + ) + assert ok is True + + info = await async_index.info() + + assert "new_name" in info.namespaces + assert "old_name" not in info.namespaces diff --git a/upstash_vector/core/index_operations.py b/upstash_vector/core/index_operations.py index f7ac4ec..7c50d8f 100644 --- a/upstash_vector/core/index_operations.py +++ b/upstash_vector/core/index_operations.py @@ -661,17 +661,22 @@ def delete_namespace(self, namespace: str) -> None: payload=None, path=_path_for(namespace, DELETE_NAMESPACE_PATH) ) - def rename_namespace(self, namespace: str, new_namespace: str) -> bool: + def rename_namespace( + self, namespace: str, new_namespace: str, delete_existing: bool = True + ) -> bool: """ Renames the given namespace. - There should not be a namespace with the new namespace name. + There should not be a namespace with the new namespace name, unless + the `delete_existing` flag is set to `True`. In that case, the existing + namespace is deleted before the rename operation. Returns whether the rename operation succeeded or not. """ payload = { "namespace": namespace, "newNamespace": new_namespace, + "deleteExisting": delete_existing, } result = self._execute_request( @@ -1288,17 +1293,22 @@ async def delete_namespace(self, namespace: str) -> None: payload=None, path=_path_for(namespace, DELETE_NAMESPACE_PATH) ) - async def rename_namespace(self, namespace: str, new_namespace: str) -> bool: + async def rename_namespace( + self, namespace: str, new_namespace: str, delete_existing: bool = False + ) -> bool: """ Renames the given namespace. - There should not be a namespace with the new namespace name. + There should not be a namespace with the new namespace name, unless + the `delete_existing` flag is set to `True`. In that case, the existing + namespace is deleted before the rename operation. Returns whether the rename operation succeeded or not. """ payload = { "namespace": namespace, "newNamespace": new_namespace, + "deleteExisting": delete_existing, } result = await self._execute_request_async(