diff --git a/pinecone/__init__.py b/pinecone/__init__.py index 2e55fe84..69f1d84f 100644 --- a/pinecone/__init__.py +++ b/pinecone/__init__.py @@ -55,6 +55,7 @@ "QueryResponse": ("pinecone.db_data.models", "QueryResponse"), "UpsertResponse": ("pinecone.db_data.models", "UpsertResponse"), "UpdateRequest": ("pinecone.db_data.models", "UpdateRequest"), + "NamespaceDescription": ("pinecone.core.openapi.db_data.models", "NamespaceDescription"), "ImportErrorMode": ("pinecone.db_data.resources.sync.bulk_import", "ImportErrorMode"), "VectorDictionaryMissingKeysError": ( "pinecone.db_data.errors", diff --git a/pinecone/__init__.pyi b/pinecone/__init__.pyi index 06de82ed..cf4cc0b7 100644 --- a/pinecone/__init__.pyi +++ b/pinecone/__init__.pyi @@ -46,6 +46,7 @@ from pinecone.db_data.models import ( UpsertResponse, UpdateRequest, ) +from pinecone.core.openapi.db_data.models import NamespaceDescription from pinecone.db_data.resources.sync.bulk_import import ImportErrorMode from pinecone.db_data.errors import ( VectorDictionaryMissingKeysError, @@ -130,6 +131,7 @@ __all__ = [ "QueryResponse", "UpsertResponse", "UpdateRequest", + "NamespaceDescription", "ImportErrorMode", # Error classes "VectorDictionaryMissingKeysError", diff --git a/pinecone/db_data/index.py b/pinecone/db_data/index.py index 955ebf18..fdee9092 100644 --- a/pinecone/db_data/index.py +++ b/pinecone/db_data/index.py @@ -15,6 +15,8 @@ UpsertResponse, ListResponse, SearchRecordsResponse, + ListNamespacesResponse, + NamespaceDescription, ) from .dataclasses import Vector, SparseValues, FetchResponse, SearchQuery, SearchRerank from .interfaces import IndexInterface @@ -35,6 +37,7 @@ validate_and_convert_errors, filter_dict, PluginAware, + require_kwargs, ) from .query_results_aggregator import QueryResultsAggregator, QueryNamespacesResults from pinecone.openapi_support import OPENAPI_ENDPOINT_PARAMS @@ -47,6 +50,7 @@ if TYPE_CHECKING: from pinecone.config import Config, OpenApiConfiguration from .resources.sync.bulk_import import BulkImportResource + from .resources.sync.namespace import NamespaceResource from pinecone.core.openapi.db_data.models import ( StartImportResponse, @@ -75,6 +79,9 @@ class Index(PluginAware, IndexInterface): _bulk_import_resource: Optional["BulkImportResource"] """ :meta private: """ + _namespace_resource: Optional["NamespaceResource"] + """ :meta private: """ + def __init__( self, api_key: str, @@ -115,6 +122,9 @@ def __init__( self._bulk_import_resource = None """ :meta private: """ + self._namespace_resource = None + """ :meta private: """ + # Pass the same api_client to the ImportFeatureMixin super().__init__(api_client=self._api_client) @@ -152,6 +162,20 @@ def bulk_import(self) -> "BulkImportResource": self._bulk_import_resource = BulkImportResource(api_client=self._api_client) return self._bulk_import_resource + @property + def namespace(self) -> "NamespaceResource": + """:meta private:""" + if self._namespace_resource is None: + from .resources.sync.namespace import NamespaceResource + + self._namespace_resource = NamespaceResource( + api_client=self._api_client, + config=self._config, + openapi_config=self._openapi_config, + pool_threads=self._pool_threads, + ) + return self._namespace_resource + def _openapi_kwargs(self, kwargs: Dict[str, Any]) -> Dict[str, Any]: return filter_dict(kwargs, OPENAPI_ENDPOINT_PARAMS) @@ -605,3 +629,27 @@ def cancel_import(self, id: str): id (str): The id of the import operation to cancel. """ return self.bulk_import.cancel(id=id) + + @validate_and_convert_errors + @require_kwargs + def describe_namespace(self, namespace: str, **kwargs) -> "NamespaceDescription": + return self.namespace.describe(namespace=namespace, **kwargs) + + @validate_and_convert_errors + @require_kwargs + def delete_namespace(self, namespace: str, **kwargs) -> Dict[str, Any]: + return self.namespace.delete(namespace=namespace, **kwargs) + + @validate_and_convert_errors + @require_kwargs + def list_namespaces( + self, limit: Optional[int] = None, **kwargs + ) -> Iterator[ListNamespacesResponse]: + return self.namespace.list(limit=limit, **kwargs) + + @validate_and_convert_errors + @require_kwargs + def list_namespaces_paginated( + self, limit: Optional[int] = None, pagination_token: Optional[str] = None, **kwargs + ) -> ListNamespacesResponse: + return self.namespace.list_paginated(limit=limit, pagination_token=pagination_token, **kwargs) \ No newline at end of file diff --git a/pinecone/db_data/index_asyncio.py b/pinecone/db_data/index_asyncio.py index 18140d9c..6bfd53da 100644 --- a/pinecone/db_data/index_asyncio.py +++ b/pinecone/db_data/index_asyncio.py @@ -22,6 +22,8 @@ DeleteRequest, ListResponse, SearchRecordsResponse, + ListNamespacesResponse, + NamespaceDescription, ) from ..utils import ( @@ -29,6 +31,7 @@ parse_non_empty_args, validate_and_convert_errors, filter_dict, + require_kwargs, ) from .types import ( SparseVectorTypedDict, @@ -50,6 +53,7 @@ if TYPE_CHECKING: from .resources.asyncio.bulk_import_asyncio import BulkImportResourceAsyncio + from .resources.asyncio.namespace_asyncio import NamespaceResourceAsyncio from pinecone.core.openapi.db_data.models import ( StartImportResponse, @@ -140,6 +144,9 @@ async def main(): _bulk_import_resource: Optional["BulkImportResourceAsyncio"] """ :meta private: """ + _namespace_resource: Optional["NamespaceResourceAsyncio"] + """ :meta private: """ + def __init__( self, api_key: str, @@ -173,6 +180,9 @@ def __init__( self._bulk_import_resource = None """ :meta private: """ + self._namespace_resource = None + """ :meta private: """ + async def __aenter__(self): return self @@ -241,6 +251,15 @@ def bulk_import(self) -> "BulkImportResourceAsyncio": self._bulk_import_resource = BulkImportResourceAsyncio(api_client=self._api_client) return self._bulk_import_resource + @property + def namespace(self) -> "NamespaceResourceAsyncio": + """:meta private:""" + if self._namespace_resource is None: + from .resources.asyncio.namespace_asyncio import NamespaceResourceAsyncio + + self._namespace_resource = NamespaceResourceAsyncio(api_client=self._api_client) + return self._namespace_resource + @validate_and_convert_errors async def upsert( self, @@ -650,5 +669,29 @@ async def cancel_import(self, id: str): """ return await self.bulk_import.cancel(id=id) + @validate_and_convert_errors + @require_kwargs + async def describe_namespace(self, namespace: str, **kwargs) -> "NamespaceDescription": + return await self.namespace.describe(namespace=namespace, **kwargs) + + @validate_and_convert_errors + @require_kwargs + async def delete_namespace(self, namespace: str, **kwargs) -> Dict[str, Any]: + return await self.namespace.delete(namespace=namespace, **kwargs) + + @validate_and_convert_errors + @require_kwargs + async def list_namespaces( + self, limit: Optional[int] = None, **kwargs + ) -> AsyncIterator[ListNamespacesResponse]: + async for namespace in self.namespace.list(limit=limit, **kwargs): + yield namespace + + @validate_and_convert_errors + @require_kwargs + async def list_namespaces_paginated( + self, limit: Optional[int] = None, pagination_token: Optional[str] = None, **kwargs + ) -> ListNamespacesResponse: + return await self.namespace.list_paginated(limit=limit, pagination_token=pagination_token, **kwargs) IndexAsyncio = _IndexAsyncio diff --git a/pinecone/db_data/index_asyncio_interface.py b/pinecone/db_data/index_asyncio_interface.py index 73ce3f0c..e057f0a0 100644 --- a/pinecone/db_data/index_asyncio_interface.py +++ b/pinecone/db_data/index_asyncio_interface.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Union, List, Optional, Dict, Any +from typing import Union, List, Optional, Dict, Any, AsyncIterator from pinecone.core.openapi.db_data.models import ( FetchResponse, @@ -10,6 +10,8 @@ ListResponse, SparseValues, SearchRecordsResponse, + NamespaceDescription, + ListNamespacesResponse, ) from .query_results_aggregator import QueryNamespacesResults from .types import ( @@ -23,6 +25,7 @@ SearchRerankTypedDict, ) from .dataclasses import SearchQuery, SearchRerank +from pinecone.utils import require_kwargs class IndexAsyncioInterface(ABC): @@ -810,3 +813,77 @@ async def search_records( ) -> SearchRecordsResponse: """Alias of the search() method.""" pass + + @abstractmethod + @require_kwargs + async def describe_namespace(self, namespace: str, **kwargs) -> NamespaceDescription: + """Describe a namespace within an index, showing the vector count within the namespace. + + Args: + namespace (str): The namespace to describe + + Returns: + NamespaceDescription: Information about the namespace including vector count + """ + pass + + @abstractmethod + @require_kwargs + async def delete_namespace(self, namespace: str, **kwargs) -> Dict[str, Any]: + """Delete a namespace from an index. + + Args: + namespace (str): The namespace to delete + + Returns: + Dict[str, Any]: Response from the delete operation + """ + pass + + @abstractmethod + @require_kwargs + async def list_namespaces( + self, limit: Optional[int] = None, **kwargs + ) -> AsyncIterator[ListNamespacesResponse]: + """List all namespaces in an index. This method automatically handles pagination to return all results. + + Args: + limit (Optional[int]): The maximum number of namespaces to return. If unspecified, the server will use a default value. [optional] + + Returns: + ``ListNamespacesResponse``: Object containing the list of namespaces. + + Examples: + .. code-block:: python + >>> async for namespace in index.list_namespaces(limit=5): + ... print(f"Namespace: {namespace.name}, Vector count: {namespace.vector_count}") + Namespace: namespace1, Vector count: 1000 + Namespace: namespace2, Vector count: 2000 + """ + pass + + @abstractmethod + @require_kwargs + async def list_namespaces_paginated( + self, limit: Optional[int] = None, pagination_token: Optional[str] = None, **kwargs + ) -> ListNamespacesResponse: + """List all namespaces in an index with pagination support. The response includes pagination information if there are more results available. + + Consider using the ``list_namespaces`` method to avoid having to handle pagination tokens manually. + + Args: + limit (Optional[int]): The maximum number of namespaces to return. If unspecified, the server will use a default value. [optional] + pagination_token (Optional[str]): A token needed to fetch the next page of results. This token is returned + in the response if additional results are available. [optional] + + Returns: + ``ListNamespacesResponse``: Object containing the list of namespaces and pagination information. + + Examples: + .. code-block:: python + >>> results = await index.list_namespaces_paginated(limit=5) + >>> results.pagination.next + eyJza2lwX3Bhc3QiOiI5OTMiLCJwcmVmaXgiOiI5OSJ9 + >>> next_results = await index.list_namespaces_paginated(limit=5, pagination_token=results.pagination.next) + """ + pass \ No newline at end of file diff --git a/pinecone/db_data/interfaces.py b/pinecone/db_data/interfaces.py index d22d03d7..12c47071 100644 --- a/pinecone/db_data/interfaces.py +++ b/pinecone/db_data/interfaces.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Union, List, Optional, Dict, Any +from typing import Union, List, Optional, Dict, Any, Iterator from pinecone.core.openapi.db_data.models import ( FetchResponse, @@ -10,6 +10,8 @@ ListResponse, SparseValues, SearchRecordsResponse, + NamespaceDescription, + ListNamespacesResponse, ) from .query_results_aggregator import QueryNamespacesResults from multiprocessing.pool import ApplyResult @@ -24,6 +26,7 @@ SearchRerankTypedDict, ) from .dataclasses import SearchQuery, SearchRerank +from pinecone.utils import require_kwargs class IndexInterface(ABC): @@ -790,3 +793,78 @@ def list(self, **kwargs): namespace (Optional[str]): The namespace to fetch vectors from. If not specified, the default namespace is used. [optional] """ pass + + @abstractmethod + @require_kwargs + def describe_namespace(self, namespace: str, **kwargs) -> NamespaceDescription: + """Describe a namespace within an index, showing the vector count within the namespace. + + Args: + namespace (str): The namespace to describe + + Returns: + NamespaceDescription: Information about the namespace including vector count + """ + pass + + @abstractmethod + @require_kwargs + def delete_namespace(self, namespace: str, **kwargs) -> Dict[str, Any]: + """Delete a namespace from an index. + + Args: + namespace (str): The namespace to delete + + Returns: + Dict[str, Any]: Response from the delete operation + """ + pass + + @abstractmethod + @require_kwargs + def list_namespaces( + self, limit: Optional[int] = None, **kwargs + ) -> Iterator[ListNamespacesResponse]: + """List all namespaces in an index. This method automatically handles pagination to return all results. + + Args: + limit (Optional[int]): The maximum number of namespaces to return. If unspecified, the server will use a default value. [optional] + + Returns: + ``ListNamespacesResponse``: Object containing the list of namespaces. + + Examples: + .. code-block:: python + >>> results = list(index.list_namespaces(limit=5)) + >>> for namespace in results: + ... print(f"Namespace: {namespace.name}, Vector count: {namespace.vector_count}") + Namespace: namespace1, Vector count: 1000 + Namespace: namespace2, Vector count: 2000 + """ + pass + + @abstractmethod + @require_kwargs + def list_namespaces_paginated( + self, limit: Optional[int] = None, pagination_token: Optional[str] = None, **kwargs + ) -> ListNamespacesResponse: + """List all namespaces in an index with pagination support. The response includes pagination information if there are more results available. + + Consider using the ``list_namespaces`` method to avoid having to handle pagination tokens manually. + + Args: + limit (Optional[int]): The maximum number of namespaces to return. If unspecified, the server will use a default value. [optional] + pagination_token (Optional[str]): A token needed to fetch the next page of results. This token is returned + in the response if additional results are available. [optional] + + Returns: + ``ListNamespacesResponse``: Object containing the list of namespaces and pagination information. + + Examples: + .. code-block:: python + >>> results = index.list_namespaces_paginated(limit=5) + >>> results.pagination.next + eyJza2lwX3Bhc3QiOiI5OTMiLCJwcmVmaXgiOiI5OSJ9 + >>> next_results = index.list_namespaces_paginated(limit=5, pagination_token=results.pagination.next) + """ + pass \ No newline at end of file diff --git a/pinecone/db_data/resources/asyncio/namespace_asyncio.py b/pinecone/db_data/resources/asyncio/namespace_asyncio.py new file mode 100644 index 00000000..5be4e4ae --- /dev/null +++ b/pinecone/db_data/resources/asyncio/namespace_asyncio.py @@ -0,0 +1,107 @@ +from typing import Optional, AsyncIterator + +from pinecone.core.openapi.db_data.api.namespace_operations_api import AsyncioNamespaceOperationsApi +from pinecone.core.openapi.db_data.models import ( + ListNamespacesResponse, + NamespaceDescription, +) + +from pinecone.utils import install_json_repr_override, require_kwargs + +from ..sync.namespace_request_factory import NamespaceRequestFactory + +for m in [ListNamespacesResponse, NamespaceDescription]: + install_json_repr_override(m) + + +class NamespaceResourceAsyncio: + def __init__(self, api_client) -> None: + self.__namespace_operations_api = AsyncioNamespaceOperationsApi(api_client) + + @require_kwargs + async def describe(self, namespace: str, **kwargs) -> NamespaceDescription: + """ + Args: + namespace (str): The namespace to describe + + Returns: + ``NamespaceDescription``: Information about the namespace including vector count + + Describe a namespace within an index, showing the vector count within the namespace. + """ + args = NamespaceRequestFactory.describe_namespace_args(namespace=namespace, **kwargs) + return await self.__namespace_operations_api.describe_namespace(**args) + + @require_kwargs + async def delete(self, namespace: str, **kwargs): + """ + Args: + namespace (str): The namespace to delete + + Delete a namespace from an index. + """ + args = NamespaceRequestFactory.delete_namespace_args(namespace=namespace, **kwargs) + return await self.__namespace_operations_api.delete_namespace(**args) + + @require_kwargs + async def list(self, limit: Optional[int] = None, **kwargs) -> AsyncIterator[ListNamespacesResponse]: + """ + Args: + limit (Optional[int]): The maximum number of namespaces to fetch in each network call. If unspecified, the server will use a default value. [optional] + pagination_token (Optional[str]): When there are multiple pages of results, a pagination token is returned in the response. The token can be used + to fetch the next page of results. [optional] + + Returns: + Returns an async generator that yields each namespace. It automatically handles pagination tokens on your behalf so you can + easily iterate over all results. The ``list`` method accepts all of the same arguments as list_paginated + + .. code-block:: python + async for namespace in index.list_namespaces(): + print(namespace) + + You can convert the generator into a list by using an async list comprehension: + + .. code-block:: python + namespaces = [namespace async for namespace in index.list_namespaces()] + + You should be cautious with this approach because it will fetch all namespaces at once, which could be a large number + of network calls and a lot of memory to hold the results. + """ + done = False + while not done: + results = await self.list_paginated(limit=limit, **kwargs) + if results.namespaces is not None and len(results.namespaces) > 0: + for namespace in results.namespaces: + yield namespace + + if results.pagination and results.pagination.next: + kwargs.update({"pagination_token": results.pagination.next}) + else: + done = True + + @require_kwargs + async def list_paginated( + self, limit: Optional[int] = None, pagination_token: Optional[str] = None, **kwargs + ) -> ListNamespacesResponse: + """ + Args: + limit (Optional[int]): The maximum number of namespaces to return. If unspecified, the server will use a default value. [optional] + pagination_token (Optional[str]): A token needed to fetch the next page of results. This token is returned + in the response if additional results are available. [optional] + + Returns: + ``ListNamespacesResponse``: Object containing the list of namespaces and pagination information. + + List all namespaces in an index with pagination support. The response includes pagination information if there are more results available. + + Consider using the ``list`` method to avoid having to handle pagination tokens manually. + + Examples: + .. code-block:: python + >>> results = await index.list_paginated(limit=5) + >>> results.pagination.next + eyJza2lwX3Bhc3QiOiI5OTMiLCJwcmVmaXgiOiI5OSJ9 + >>> next_results = await index.list_paginated(limit=5, pagination_token=results.pagination.next) + """ + args = NamespaceRequestFactory.list_namespaces_args(limit=limit, pagination_token=pagination_token, **kwargs) + return await self.__namespace_operations_api.list_namespaces_operation(**args) \ No newline at end of file diff --git a/pinecone/db_data/resources/sync/namespace.py b/pinecone/db_data/resources/sync/namespace.py new file mode 100644 index 00000000..944573bc --- /dev/null +++ b/pinecone/db_data/resources/sync/namespace.py @@ -0,0 +1,123 @@ +from typing import Optional, Iterator + +from pinecone.core.openapi.db_data.api.namespace_operations_api import NamespaceOperationsApi +from pinecone.core.openapi.db_data.models import ( + ListNamespacesResponse, + NamespaceDescription, +) + +from pinecone.utils import install_json_repr_override, PluginAware, require_kwargs + +from .namespace_request_factory import NamespaceRequestFactory + +for m in [ListNamespacesResponse, NamespaceDescription]: + install_json_repr_override(m) + + +class NamespaceResource(PluginAware): + def __init__( + self, + api_client, + config, + openapi_config, + pool_threads: int, + ) -> None: + self.config = config + """ :meta private: """ + + self._openapi_config = openapi_config + """ :meta private: """ + + self._pool_threads = pool_threads + """ :meta private: """ + + self.__namespace_operations_api = NamespaceOperationsApi(api_client) + super().__init__() + + @require_kwargs + def describe(self, namespace: str, **kwargs) -> NamespaceDescription: + """ + Args: + namespace (str): The namespace to describe + + Returns: + ``NamespaceDescription``: Information about the namespace including vector count + + Describe a namespace within an index, showing the vector count within the namespace. + """ + args = NamespaceRequestFactory.describe_namespace_args(namespace=namespace, **kwargs) + return self.__namespace_operations_api.describe_namespace(**args) + + @require_kwargs + def delete(self, namespace: str, **kwargs): + """ + Args: + namespace (str): The namespace to delete + + Delete a namespace from an index. + """ + args = NamespaceRequestFactory.delete_namespace_args(namespace=namespace, **kwargs) + return self.__namespace_operations_api.delete_namespace(**args) + + @require_kwargs + def list(self, limit: Optional[int] = None, **kwargs) -> Iterator[ListNamespacesResponse]: + """ + Args: + limit (Optional[int]): The maximum number of namespaces to fetch in each network call. If unspecified, the server will use a default value. [optional] + pagination_token (Optional[str]): When there are multiple pages of results, a pagination token is returned in the response. The token can be used + to fetch the next page of results. [optional] + + Returns: + Returns a generator that yields each namespace. It automatically handles pagination tokens on your behalf so you can + easily iterate over all results. The ``list`` method accepts all of the same arguments as list_paginated + + .. code-block:: python + for namespace in index.list_namespaces(): + print(namespace) + + You can convert the generator into a list by wrapping the generator in a call to the built-in ``list`` function: + + .. code-block:: python + namespaces = list(index.list_namespaces()) + + You should be cautious with this approach because it will fetch all namespaces at once, which could be a large number + of network calls and a lot of memory to hold the results. + """ + done = False + while not done: + results = self.list_paginated(limit=limit, **kwargs) + if results.namespaces is not None and len(results.namespaces) > 0: + for namespace in results.namespaces: + yield namespace + + if results.pagination and results.pagination.next: + kwargs.update({"pagination_token": results.pagination.next}) + else: + done = True + + @require_kwargs + def list_paginated( + self, limit: Optional[int] = None, pagination_token: Optional[str] = None, **kwargs + ) -> ListNamespacesResponse: + """ + Args: + limit (Optional[int]): The maximum number of namespaces to return. If unspecified, the server will use a default value. [optional] + pagination_token (Optional[str]): A token needed to fetch the next page of results. This token is returned + in the response if additional results are available. [optional] + + Returns: + ``ListNamespacesResponse``: Object containing the list of namespaces and pagination information. + + List all namespaces in an index with pagination support. The response includes pagination information if there are more results available. + + Consider using the ``list`` method to avoid having to handle pagination tokens manually. + + Examples: + .. code-block:: python + >>> results = index.list_paginated(limit=5) + >>> results.pagination.next + eyJza2lwX3Bhc3QiOiI5OTMiLCJwcmVmaXgiOiI5OSJ9 + >>> next_results = index.list_paginated(limit=5, pagination_token=results.pagination.next) + """ + args = NamespaceRequestFactory.list_namespaces_args(limit=limit, pagination_token=pagination_token, **kwargs) + return self.__namespace_operations_api.list_namespaces_operation(**args) \ No newline at end of file diff --git a/pinecone/db_data/resources/sync/namespace_request_factory.py b/pinecone/db_data/resources/sync/namespace_request_factory.py new file mode 100644 index 00000000..7174276b --- /dev/null +++ b/pinecone/db_data/resources/sync/namespace_request_factory.py @@ -0,0 +1,34 @@ +from typing import Optional, TypedDict, Any, cast + +from pinecone.utils import parse_non_empty_args + + +class DescribeNamespaceArgs(TypedDict, total=False): + namespace: str + + +class DeleteNamespaceArgs(TypedDict, total=False): + namespace: str + + +class NamespaceRequestFactory: + @staticmethod + def describe_namespace_args(namespace: str, **kwargs) -> DescribeNamespaceArgs: + if not isinstance(namespace, str): + raise ValueError('namespace must be string') + base_args = {"namespace": namespace} + return cast(DescribeNamespaceArgs, {**base_args, **kwargs}) + + @staticmethod + def delete_namespace_args(namespace: str, **kwargs) -> DeleteNamespaceArgs: + if not isinstance(namespace, str): + raise ValueError('namespace must be string') + base_args = {"namespace": namespace} + return cast(DeleteNamespaceArgs, {**base_args, **kwargs}) + + @staticmethod + def list_namespaces_args( + limit: Optional[int] = None, pagination_token: Optional[str] = None, **kwargs + ) -> dict[str, Any]: + base_args = parse_non_empty_args([("limit", limit), ("pagination_token", pagination_token)]) + return {**base_args, **kwargs} \ No newline at end of file diff --git a/tests/integration/data/test_namespace.py b/tests/integration/data/test_namespace.py new file mode 100644 index 00000000..459bfdfd --- /dev/null +++ b/tests/integration/data/test_namespace.py @@ -0,0 +1,161 @@ +import os +import time +import logging + +import pytest + +from pinecone import NamespaceDescription + +logger = logging.getLogger(__name__) + +def setup_namespace_data(index, namespace: str, num_vectors: int = 2): + """Helper function to set up test data in a namespace""" + vectors = [(f"id_{i}", [0.1, 0.2]) for i in range(num_vectors)] + index.upsert(vectors=vectors, namespace=namespace) + # Wait for data to be upserted + time.sleep(5) + + +def verify_namespace_exists(index, namespace: str) -> bool: + """Helper function to verify if a namespace exists""" + try: + index.describe_namespace(namespace=namespace) + return True + except Exception: + return False + + +def delete_all_namespaces(index): + """Helper function to delete all namespaces in an index""" + try: + # Get all namespaces + namespaces = list(index.list_namespaces()) + + # Delete each namespace + for namespace in namespaces: + try: + index.delete_namespace(namespace=namespace.name) + except Exception as e: + logger.error(f"Error deleting namespace {namespace.name}: {e}") + + # Wait for deletions to complete + time.sleep(5) + except Exception as e: + logger.error(f"Error in delete_all_namespaces: {e}") + +@pytest.mark.skipif( + os.getenv("USE_GRPC") == "true", reason="Disable until grpc namespaces support is added" +) +class TestNamespaceOperations: + def test_describe_namespace(self, idx): + """Test describing a namespace""" + # Setup test data + test_namespace = "test_describe_namespace_sync" + setup_namespace_data(idx, test_namespace) + + try: + # Test describe + description = idx.describe_namespace(namespace=test_namespace) + assert isinstance(description, NamespaceDescription) + assert description.name == test_namespace + finally: + # Delete all namespaces before next test is run + delete_all_namespaces(idx) + + def test_delete_namespace(self, idx): + """Test deleting a namespace""" + # Setup test data + test_namespace = "test_delete_namespace_sync" + setup_namespace_data(idx, test_namespace) + + # Verify namespace exists + assert verify_namespace_exists(idx, test_namespace) + + # Delete namespace + idx.delete_namespace(namespace=test_namespace) + + # Wait for namespace to be deleted + time.sleep(10) + + # Verify namespace is deleted + assert not verify_namespace_exists(idx, test_namespace) + + def test_list_namespaces(self, idx): + """Test listing namespaces""" + # Create multiple test namespaces + test_namespaces = ["test_list_1", "test_list_2", "test_list_3"] + for ns in test_namespaces: + setup_namespace_data(idx, ns) + + try: + # Get all namespaces + namespaces = list(idx.list_namespaces()) + + # Verify results + assert len(namespaces) == len(test_namespaces) + namespace_names = [ns.name for ns in namespaces] + for test_ns in test_namespaces: + assert test_ns in namespace_names + + # Verify each namespace has correct structure + for ns in namespaces: + assert isinstance(ns, NamespaceDescription) + assert hasattr(ns, 'name') + assert hasattr(ns, 'vector_count') + finally: + # Delete all namespaces before next test is run + delete_all_namespaces(idx) + + def test_list_namespaces_with_limit(self, idx): + """Test listing namespaces with limit""" + # Create multiple test namespaces + test_namespaces = [f"test_limit_{i}" for i in range(5)] + for ns in test_namespaces: + setup_namespace_data(idx, ns) + + try: + # Get namespaces with limit + namespaces = list(idx.list_namespaces(limit=2)) + + # Verify results + assert len(namespaces) >= 2 # Should get at least 2 namespaces + for ns in namespaces: + assert isinstance(ns, NamespaceDescription) + assert hasattr(ns, 'name') + assert hasattr(ns, 'vector_count') + + finally: + # Delete all namespaces before next test is run + delete_all_namespaces(idx) + + + def test_list_namespaces_paginated(self, idx): + """Test listing namespaces with pagination""" + # Create multiple test namespaces + test_namespaces = [f"test_paginated_{i}" for i in range(5)] + for ns in test_namespaces: + setup_namespace_data(idx, ns) + + try: + # Get first page + response = idx.list_namespaces_paginated(limit=2) + assert len(response.namespaces) == 2 + assert response.pagination.next is not None + + # Get second page + next_response = idx.list_namespaces_paginated( + limit=2, + pagination_token=response.pagination.next + ) + assert len(next_response.namespaces) == 2 + assert next_response.pagination.next is not None + + # Get final page + final_response = idx.list_namespaces_paginated( + limit=2, + pagination_token=next_response.pagination.next + ) + assert len(final_response.namespaces) == 1 + assert final_response.pagination is None + finally: + delete_all_namespaces(idx) \ No newline at end of file diff --git a/tests/integration/data_asyncio/test_namespace_asyncio.py b/tests/integration/data_asyncio/test_namespace_asyncio.py new file mode 100644 index 00000000..0a509df0 --- /dev/null +++ b/tests/integration/data_asyncio/test_namespace_asyncio.py @@ -0,0 +1,170 @@ +import pytest +import asyncio +import logging + +from pinecone import NamespaceDescription +from tests.integration.data_asyncio.conftest import build_asyncioindex_client + +logger = logging.getLogger(__name__) + +async def setup_namespace_data(index, namespace: str, num_vectors: int = 2): + """Helper function to set up test data in a namespace""" + vectors = [(f"id_{i}", [0.1, 0.2]) for i in range(num_vectors)] + await index.upsert(vectors=vectors, namespace=namespace) + # Wait for vectors to be upserted + await asyncio.sleep(5) + + +async def verify_namespace_exists(index, namespace: str) -> bool: + """Helper function to verify if a namespace exists""" + try: + await index.describe_namespace(namespace=namespace) + return True + except Exception: + return False + + +async def delete_all_namespaces(index): + """Helper function to delete all namespaces in an index""" + try: + # Get all namespaces + namespaces = await index.list_namespaces_paginated() + + # Delete each namespace + for namespace in namespaces.namespaces: + try: + await index.delete_namespace(namespace=namespace.name) + except Exception as e: + logger.error(f"Error deleting namespace {namespace.name}: {e}") + + # Wait for deletions to complete + await asyncio.sleep(5) + except Exception as e: + logger.error(f"Error in delete_all_namespaces: {e}") + + +class TestNamespaceOperationsAsyncio: + @pytest.mark.asyncio + async def test_describe_namespace(self, index_host): + """Test describing a namespace""" + asyncio_idx = build_asyncioindex_client(index_host) + + # Setup test data + test_namespace = "test_describe_namespace_async" + await setup_namespace_data(asyncio_idx, test_namespace) + + try: + # Test describe + description = await asyncio_idx.describe_namespace(namespace=test_namespace) + assert isinstance(description, NamespaceDescription) + assert description.name == test_namespace + finally: + # Delete all namespaces before next test is run + await delete_all_namespaces(asyncio_idx) + + @pytest.mark.asyncio + async def test_delete_namespace(self, index_host): + """Test deleting a namespace""" + asyncio_idx = build_asyncioindex_client(index_host) + # Setup test data + test_namespace = "test_delete_namespace_async" + await setup_namespace_data(asyncio_idx, test_namespace) + + # Verify namespace exists + assert await verify_namespace_exists(asyncio_idx, test_namespace) + + # Delete namespace + await asyncio_idx.delete_namespace(namespace=test_namespace) + + # Wait for namespace to be deleted + await asyncio.sleep(10) + + # Verify namespace is deleted + assert not await verify_namespace_exists(asyncio_idx, test_namespace) + + @pytest.mark.asyncio + async def test_list_namespaces(self, index_host): + """Test listing namespaces""" + asyncio_idx = build_asyncioindex_client(index_host) + # Create multiple test namespaces + test_namespaces = ["test_list_1_async", "test_list_2_async", "test_list_3_async"] + for ns in test_namespaces: + await setup_namespace_data(asyncio_idx, ns) + + try: + # Get all namespaces + namespaces = [] + async for ns in asyncio_idx.list_namespaces(): + namespaces.append(ns) + + # Verify results + assert len(namespaces) >= len(test_namespaces) + namespace_names = [ns.name for ns in namespaces] + for test_ns in test_namespaces: + assert test_ns in namespace_names + + # Verify each namespace has correct structure + for ns in namespaces: + assert isinstance(ns, NamespaceDescription) + assert hasattr(ns, 'name') + assert hasattr(ns, 'vector_count') + finally: + # Delete all namespaces before next test is run + await delete_all_namespaces(asyncio_idx) + + @pytest.mark.asyncio + async def test_list_namespaces_with_limit(self, index_host): + """Test listing namespaces with limit""" + asyncio_idx = build_asyncioindex_client(index_host) + # Create multiple test namespaces + test_namespaces = [f"test_limit_async_{i}" for i in range(5)] + for ns in test_namespaces: + await setup_namespace_data(asyncio_idx, ns) + + try: + # Get namespaces with limit + namespaces = await asyncio_idx.list_namespaces_paginated(limit=2) + + # Verify results + assert len(namespaces.namespaces) == 2 # Should get exactly 2 namespaces + for ns in namespaces.namespaces: + assert isinstance(ns, NamespaceDescription) + assert hasattr(ns, 'name') + assert hasattr(ns, 'vector_count') + finally: + # Delete all namespaces before next test is run + await delete_all_namespaces(asyncio_idx) + + @pytest.mark.asyncio + async def test_list_namespaces_paginated(self, index_host): + """Test listing namespaces with pagination""" + asyncio_idx = build_asyncioindex_client(index_host) + # Create multiple test namespaces + test_namespaces = [f"test_paginated_async_{i}" for i in range(5)] + for ns in test_namespaces: + await setup_namespace_data(asyncio_idx, ns) + + try: + # Get first page + response = await asyncio_idx.list_namespaces_paginated(limit=2) + assert len(response.namespaces) == 2 + assert response.pagination.next is not None + + # Get second page + next_response = await asyncio_idx.list_namespaces_paginated( + limit=2, + pagination_token=response.pagination.next + ) + assert len(next_response.namespaces) == 2 + assert next_response.pagination.next is not None + + # Get final page + final_response = await asyncio_idx.list_namespaces_paginated( + limit=2, + pagination_token=next_response.pagination.next + ) + assert len(final_response.namespaces) == 1 + assert final_response.pagination is None + finally: + # Delete all namespaces before next test is run + await delete_all_namespaces(asyncio_idx) \ No newline at end of file diff --git a/tests/integration/helpers/helpers.py b/tests/integration/helpers/helpers.py index 8cb069dd..dc648fd4 100644 --- a/tests/integration/helpers/helpers.py +++ b/tests/integration/helpers/helpers.py @@ -280,11 +280,7 @@ async def asyncio_wait_until( def default_create_index_params(request, run_id): - github_actor = os.getenv("GITHUB_ACTOR", None) - user = os.getenv("USER", None) - index_owner = github_actor or user or "unknown" - - index_name = f"{index_owner}-{str(uuid.uuid4())}" + index_name = f"{str(uuid.uuid4())}" tags = index_tags(request, run_id) cloud = get_environment_var("SERVERLESS_CLOUD", "aws") region = get_environment_var("SERVERLESS_REGION", "us-west-2")