diff --git a/pyproject.toml b/pyproject.toml index c62625f..c329943 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 "] @@ -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_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..74097d5 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,83 @@ 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("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 + + +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("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 + + +@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/__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..7c50d8f 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,31 @@ 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, delete_existing: bool = True + ) -> bool: + """ + Renames the given namespace. + + 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( + payload=payload, + path=RENAME_NAMESPACE_PATH, + ) + + return result["renamed"] + class AsyncIndexOperations: async def _execute_request_async(self, payload, path): @@ -1267,6 +1293,31 @@ 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, delete_existing: bool = False + ) -> bool: + """ + Renames the given namespace. + + 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( + payload=payload, + path=RENAME_NAMESPACE_PATH, + ) + + return result["renamed"] + class ResumableQueryHandle: """