Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <support@upstash.com>"]
Expand Down Expand Up @@ -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" }
Expand Down
4 changes: 2 additions & 2 deletions tests/core/test_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
86 changes: 80 additions & 6 deletions tests/core/test_namespace_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand All @@ -32,32 +32,106 @@ 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
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
2 changes: 1 addition & 1 deletion upstash_vector/__init__.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
51 changes: 51 additions & 0 deletions upstash_vector/core/index_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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:
"""
Expand Down