diff --git a/astrapy/db.py b/astrapy/db.py index e1461451..ca18a233 100644 --- a/astrapy/db.py +++ b/astrapy/db.py @@ -930,6 +930,26 @@ def delete_one(self, id: str) -> API_RESPONSE: return response + def delete_one_filter(self, filter: Dict[str, Any]) -> API_RESPONSE: + """ + Delete a single document from the collection based on a filter clause + Args: + filter: any filter dictionary + Returns: + dict: The response from the database after the delete operation. + """ + json_query = { + "deleteOne": { + "filter": filter, + } + } + + response = self._request( + method=http_methods.POST, path=f"{self.base_path}", json_data=json_query + ) + + return response + def delete_many(self, filter: Dict[str, Any]) -> API_RESPONSE: """ Delete many documents from the collection based on a filter condition @@ -1909,6 +1929,26 @@ async def delete_one(self, id: str) -> API_RESPONSE: return response + async def delete_one_filter(self, filter: Dict[str, Any]) -> API_RESPONSE: + """ + Delete a single document from the collection based on a filter clause + Args: + filter: any filter dictionary + Returns: + dict: The response from the database after the delete operation. + """ + json_query = { + "deleteOne": { + "filter": filter, + } + } + + response = await self._request( + method=http_methods.POST, path=f"{self.base_path}", json_data=json_query + ) + + return response + async def delete_many(self, filter: Dict[str, Any]) -> API_RESPONSE: """ Delete many documents from the collection based on a filter condition diff --git a/astrapy/idiomatic/__init__.py b/astrapy/idiomatic/__init__.py index 595e60c6..d94ccb2a 100644 --- a/astrapy/idiomatic/__init__.py +++ b/astrapy/idiomatic/__init__.py @@ -17,7 +17,7 @@ __all__ = [ "AsyncCollection", - "Collection", "AsyncDatabase", + "Collection", "Database", ] diff --git a/astrapy/idiomatic/collection.py b/astrapy/idiomatic/collection.py index 862db9ed..f8f32dea 100644 --- a/astrapy/idiomatic/collection.py +++ b/astrapy/idiomatic/collection.py @@ -11,12 +11,16 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from __future__ import annotations -from typing import Any, Optional, TypedDict +import json +from typing import Any, Dict, Optional, TypedDict + from astrapy.db import AstraDBCollection, AsyncAstraDBCollection -from astrapy.idiomatic.utils import unsupported +from astrapy.idiomatic.utils import raise_unsupported_parameter, unsupported from astrapy.idiomatic.database import AsyncDatabase, Database +from astrapy.idiomatic.results import DeleteResult, InsertOneResult class CollectionConstructorParams(TypedDict): @@ -90,6 +94,128 @@ def set_caller( caller_version=caller_version, ) + def insert_one( + self, + document: Dict[str, Any], + *, + bypass_document_validation: Optional[bool] = None, + ) -> InsertOneResult: + if bypass_document_validation: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="insert_one", + parameter_name="bypass_document_validation", + ) + io_response = self._astra_db_collection.insert_one(document) + if "insertedIds" in io_response.get("status", {}): + if io_response["status"]["insertedIds"]: + inserted_id = io_response["status"]["insertedIds"][0] + return InsertOneResult( + inserted_id=inserted_id, + ) + else: + raise ValueError( + "Could not complete a insert_one operation. " + f"(gotten '${json.dumps(io_response)}')" + ) + else: + raise ValueError( + "Could not complete a insert_one operation. " + f"(gotten '${json.dumps(io_response)}')" + ) + + def count_documents( + self, + filter: Dict[str, Any], + *, + skip: Optional[int] = None, + limit: Optional[int] = None, + ) -> int: + if skip: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="count_documents", + parameter_name="skip", + ) + if limit: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="count_documents", + parameter_name="limit", + ) + cd_response = self._astra_db_collection.count_documents(filter=filter) + if "count" in cd_response.get("status", {}): + return cd_response["status"]["count"] # type: ignore[no-any-return] + else: + raise ValueError( + "Could not complete a count_documents operation. " + f"(gotten '${json.dumps(cd_response)}')" + ) + + def delete_one( + self, + filter: Dict[str, Any], + *, + let: Optional[int] = None, + ) -> DeleteResult: + if let: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="delete_one", + parameter_name="let", + ) + do_response = self._astra_db_collection.delete_one_filter(filter=filter) + if "deletedCount" in do_response.get("status", {}): + deleted_count = do_response["status"]["deletedCount"] + if deleted_count == -1: + return DeleteResult( + deleted_count=None, + raw_result=do_response, + ) + else: + # expected a non-negative integer: + return DeleteResult( + deleted_count=deleted_count, + raw_result=do_response, + ) + else: + raise ValueError( + "Could not complete a delete_one operation. " + f"(gotten '${json.dumps(do_response)}')" + ) + + def delete_many( + self, + filter: Dict[str, Any], + *, + let: Optional[int] = None, + ) -> DeleteResult: + if let: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="delete_many", + parameter_name="let", + ) + dm_response = self._astra_db_collection.delete_many(filter=filter) + if "deletedCount" in dm_response.get("status", {}): + deleted_count = dm_response["status"]["deletedCount"] + if deleted_count == -1: + return DeleteResult( + deleted_count=None, + raw_result=dm_response, + ) + else: + # expected a non-negative integer: + return DeleteResult( + deleted_count=deleted_count, + raw_result=dm_response, + ) + else: + raise ValueError( + "Could not complete a delete_many operation. " + f"(gotten '${json.dumps(dm_response)}')" + ) + @unsupported def find_raw_batches(*pargs: Any, **kwargs: Any) -> Any: ... @@ -197,6 +323,128 @@ def set_caller( caller_version=caller_version, ) + async def insert_one( + self, + document: Dict[str, Any], + *, + bypass_document_validation: Optional[bool] = None, + ) -> InsertOneResult: + if bypass_document_validation: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="insert_one", + parameter_name="bypass_document_validation", + ) + io_response = await self._astra_db_collection.insert_one(document) + if "insertedIds" in io_response.get("status", {}): + if io_response["status"]["insertedIds"]: + inserted_id = io_response["status"]["insertedIds"][0] + return InsertOneResult( + inserted_id=inserted_id, + ) + else: + raise ValueError( + "Could not complete a insert_one operation. " + f"(gotten '${json.dumps(io_response)}')" + ) + else: + raise ValueError( + "Could not complete a insert_one operation. " + f"(gotten '${json.dumps(io_response)}')" + ) + + async def count_documents( + self, + filter: Dict[str, Any], + *, + skip: Optional[int] = None, + limit: Optional[int] = None, + ) -> int: + if skip: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="count_documents", + parameter_name="skip", + ) + if limit: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="count_documents", + parameter_name="limit", + ) + cd_response = await self._astra_db_collection.count_documents(filter=filter) + if "count" in cd_response.get("status", {}): + return cd_response["status"]["count"] # type: ignore[no-any-return] + else: + raise ValueError( + "Could not complete a count_documents operation. " + f"(gotten '${json.dumps(cd_response)}')" + ) + + async def delete_one( + self, + filter: Dict[str, Any], + *, + let: Optional[int] = None, + ) -> DeleteResult: + if let: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="delete_one", + parameter_name="let", + ) + do_response = await self._astra_db_collection.delete_one_filter(filter=filter) + if "deletedCount" in do_response.get("status", {}): + deleted_count = do_response["status"]["deletedCount"] + if deleted_count == -1: + return DeleteResult( + deleted_count=None, + raw_result=do_response, + ) + else: + # expected a non-negative integer: + return DeleteResult( + deleted_count=deleted_count, + raw_result=do_response, + ) + else: + raise ValueError( + "Could not complete a delete_one operation. " + f"(gotten '${json.dumps(do_response)}')" + ) + + async def delete_many( + self, + filter: Dict[str, Any], + *, + let: Optional[int] = None, + ) -> DeleteResult: + if let: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="delete_many", + parameter_name="let", + ) + dm_response = await self._astra_db_collection.delete_many(filter=filter) + if "deletedCount" in dm_response.get("status", {}): + deleted_count = dm_response["status"]["deletedCount"] + if deleted_count == -1: + return DeleteResult( + deleted_count=None, + raw_result=dm_response, + ) + else: + # expected a non-negative integer: + return DeleteResult( + deleted_count=deleted_count, + raw_result=dm_response, + ) + else: + raise ValueError( + "Could not complete a delete_many operation. " + f"(gotten '${json.dumps(dm_response)}')" + ) + @unsupported async def find_raw_batches(*pargs: Any, **kwargs: Any) -> Any: ... diff --git a/astrapy/idiomatic/database.py b/astrapy/idiomatic/database.py index aa16e516..541e0a05 100644 --- a/astrapy/idiomatic/database.py +++ b/astrapy/idiomatic/database.py @@ -11,12 +11,15 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from __future__ import annotations +import json from types import TracebackType -from typing import Any, Dict, Optional, Type, TypedDict, Union, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Type, TypedDict, Union, TYPE_CHECKING + from astrapy.db import AstraDB, AsyncAstraDB -from astrapy.idiomatic.utils import unsupported +from astrapy.idiomatic.utils import raise_unsupported_parameter, unsupported if TYPE_CHECKING: from astrapy.idiomatic.collection import AsyncCollection, Collection @@ -169,6 +172,32 @@ def drop_collection(self, name_or_collection: Union[str, Collection]) -> None: _name = name_or_collection self._astra_db.delete_collection(_name) + def list_collection_names( + self, + *, + namespace: Optional[str] = None, + filter: Optional[Dict[str, Any]] = None, + ) -> List[str]: + if filter: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="list_collection_names", + parameter_name="filter", + ) + if namespace: + _client = self._astra_db.copy(namespace=namespace) + else: + _client = self._astra_db + gc_response = _client.get_collections() + if "collections" not in gc_response.get("status", {}): + raise ValueError( + "Could not complete a get_collections operation. " + f"(gotten '${json.dumps(gc_response)}')" + ) + else: + # we know this is a list of strings + return gc_response["status"]["collections"] # type: ignore[no-any-return] + @unsupported def aggregate(*pargs: Any, **kwargs: Any) -> Any: ... @@ -313,6 +342,28 @@ async def drop_collection( _name = name_or_collection await self._astra_db.delete_collection(_name) + async def list_collection_names( + self, + *, + namespace: Optional[str] = None, + filter: Optional[Dict[str, Any]] = None, + ) -> List[str]: + if filter: + raise_unsupported_parameter( + class_name=self.__class__.__name__, + method_name="list_collection_names", + parameter_name="filter", + ) + gc_response = await self._astra_db.copy(namespace=namespace).get_collections() + if "collections" not in gc_response.get("status", {}): + raise ValueError( + "Could not complete a get_collections operation. " + f"(gotten '${json.dumps(gc_response)}')" + ) + else: + # we know this is a list of strings + return gc_response["status"]["collections"] # type: ignore[no-any-return] + @unsupported async def aggregate(*pargs: Any, **kwargs: Any) -> Any: ... diff --git a/astrapy/idiomatic/results.py b/astrapy/idiomatic/results.py new file mode 100644 index 00000000..70fa8dd2 --- /dev/null +++ b/astrapy/idiomatic/results.py @@ -0,0 +1,31 @@ +# Copyright DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any, Dict, Optional + + +@dataclass +class DeleteResult: + deleted_count: Optional[int] + raw_result: Dict[str, Any] + acknowledged: bool = True + + +@dataclass +class InsertOneResult: + inserted_id: Any + acknowledged: bool = True diff --git a/astrapy/idiomatic/utils.py b/astrapy/idiomatic/utils.py index 994997ac..f8ac2532 100644 --- a/astrapy/idiomatic/utils.py +++ b/astrapy/idiomatic/utils.py @@ -11,11 +11,13 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + from __future__ import annotations from functools import wraps from typing import Any, Callable DEFAULT_NOT_SUPPORTED_MESSAGE = "Operation not supported." +DEFAULT_NOT_SUPPORTED_PARAMETER_MESSAGE_TEMPLATE = "Parameter `{parameter_name}` not supported for method `{method_name}` of class `{class_name}`." def unsupported(func: Callable[..., Any]) -> Callable[..., Any]: @@ -24,3 +26,17 @@ def unsupported_func(*args: Any, **kwargs: Any) -> Any: raise TypeError(DEFAULT_NOT_SUPPORTED_MESSAGE) return unsupported_func + + +def raise_unsupported_parameter( + *, + class_name: str, + method_name: str, + parameter_name: str, +) -> None: + message = DEFAULT_NOT_SUPPORTED_PARAMETER_MESSAGE_TEMPLATE.format( + class_name=class_name, + method_name=method_name, + parameter_name=parameter_name, + ) + raise TypeError(message) diff --git a/astrapy/results.py b/astrapy/results.py new file mode 100644 index 00000000..dd67f027 --- /dev/null +++ b/astrapy/results.py @@ -0,0 +1,23 @@ +# Copyright DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Idiomatic "results" subpackage.""" + +from astrapy.idiomatic.results import DeleteResult, InsertOneResult + + +__all__ = [ + "DeleteResult", + "InsertOneResult", +] diff --git a/tests/idiomatic/conftest.py b/tests/idiomatic/conftest.py index 50dec1d1..cf5b4e68 100644 --- a/tests/idiomatic/conftest.py +++ b/tests/idiomatic/conftest.py @@ -1,12 +1,16 @@ """Fixtures specific to the idiomatic-side testing, if any.""" -from typing import Iterable +import os +from typing import AsyncIterable, Iterable import pytest from ..conftest import AstraDBCredentials from astrapy import AsyncCollection, AsyncDatabase, Collection, Database -TEST_COLLECTION_NAME = "test_coll_sync" +TEST_COLLECTION_INSTANCE_NAME = "test_coll_instance" +TEST_COLLECTION_NAME = "id_test_collection" + +ASTRA_DB_SECONDARY_KEYSPACE = os.environ.get("ASTRA_DB_SECONDARY_KEYSPACE") @pytest.fixture(scope="session") @@ -24,27 +28,69 @@ def async_database( @pytest.fixture(scope="session") -def sync_collection( +def sync_collection_instance( astra_db_credentials_kwargs: AstraDBCredentials, sync_database: Database, ) -> Iterable[Collection]: + """Just an instance of the class, no DB-level stuff.""" yield Collection( sync_database, - TEST_COLLECTION_NAME, - namespace=astra_db_credentials_kwargs["namespace"], + TEST_COLLECTION_INSTANCE_NAME, + # namespace=astra_db_credentials_kwargs["namespace"], ) @pytest.fixture(scope="session") -def async_collection( +def async_collection_instance( astra_db_credentials_kwargs: AstraDBCredentials, async_database: AsyncDatabase, ) -> Iterable[AsyncCollection]: + """Just an instance of the class, no DB-level stuff.""" yield AsyncCollection( async_database, + TEST_COLLECTION_INSTANCE_NAME, + # namespace=astra_db_credentials_kwargs["namespace"], + ) + + +@pytest.fixture(scope="session") +def sync_collection( + astra_db_credentials_kwargs: AstraDBCredentials, + sync_database: Database, +) -> Iterable[Collection]: + """An actual collection on DB, in the main namespace""" + collection = sync_database.create_collection( TEST_COLLECTION_NAME, - namespace=astra_db_credentials_kwargs["namespace"], + dimension=2, + metric="dot_product", + indexing={"deny": ["not_indexed"]}, ) + yield collection + + sync_database.drop_collection(TEST_COLLECTION_NAME) + + +@pytest.fixture(scope="function") +def sync_empty_collection(sync_collection: Collection) -> Iterable[Collection]: + """Emptied for each test function""" + sync_collection.delete_many(filter={}) + yield sync_collection + + +@pytest.fixture(scope="session") +def async_collection( + sync_collection: Collection, +) -> Iterable[AsyncCollection]: + """An actual collection on DB, the same as the sync counterpart""" + yield sync_collection.to_async() + + +@pytest.fixture(scope="function") +async def async_empty_collection( + sync_empty_collection: Collection, +) -> AsyncIterable[AsyncCollection]: + """Emptied for each test function""" + yield sync_empty_collection.to_async() __all__ = [ diff --git a/tests/idiomatic/integration/test_collections_async.py b/tests/idiomatic/integration/test_collections_async.py index b547408e..8fb05961 100644 --- a/tests/idiomatic/integration/test_collections_async.py +++ b/tests/idiomatic/integration/test_collections_async.py @@ -77,39 +77,39 @@ async def test_collection_set_caller_async( @pytest.mark.describe("test errors for unsupported Collection methods, async") async def test_collection_unsupported_methods_async( self, - async_collection: AsyncCollection, + async_collection_instance: AsyncCollection, ) -> None: with pytest.raises(TypeError): - await async_collection.find_raw_batches(1, "x") + await async_collection_instance.find_raw_batches(1, "x") with pytest.raises(TypeError): - await async_collection.aggregate(1, "x") + await async_collection_instance.aggregate(1, "x") with pytest.raises(TypeError): - await async_collection.aggregate_raw_batches(1, "x") + await async_collection_instance.aggregate_raw_batches(1, "x") with pytest.raises(TypeError): - await async_collection.watch(1, "x") + await async_collection_instance.watch(1, "x") with pytest.raises(TypeError): - await async_collection.rename(1, "x") + await async_collection_instance.rename(1, "x") with pytest.raises(TypeError): - await async_collection.create_index(1, "x") + await async_collection_instance.create_index(1, "x") with pytest.raises(TypeError): - await async_collection.create_indexes(1, "x") + await async_collection_instance.create_indexes(1, "x") with pytest.raises(TypeError): - await async_collection.drop_index(1, "x") + await async_collection_instance.drop_index(1, "x") with pytest.raises(TypeError): - await async_collection.drop_indexes(1, "x") + await async_collection_instance.drop_indexes(1, "x") with pytest.raises(TypeError): - await async_collection.list_indexes(1, "x") + await async_collection_instance.list_indexes(1, "x") with pytest.raises(TypeError): - await async_collection.index_information(1, "x") + await async_collection_instance.index_information(1, "x") with pytest.raises(TypeError): - await async_collection.create_search_index(1, "x") + await async_collection_instance.create_search_index(1, "x") with pytest.raises(TypeError): - await async_collection.create_search_indexes(1, "x") + await async_collection_instance.create_search_indexes(1, "x") with pytest.raises(TypeError): - await async_collection.drop_search_index(1, "x") + await async_collection_instance.drop_search_index(1, "x") with pytest.raises(TypeError): - await async_collection.list_search_indexes(1, "x") + await async_collection_instance.list_search_indexes(1, "x") with pytest.raises(TypeError): - await async_collection.update_search_index(1, "x") + await async_collection_instance.update_search_index(1, "x") with pytest.raises(TypeError): - await async_collection.distinct(1, "x") + await async_collection_instance.distinct(1, "x") diff --git a/tests/idiomatic/integration/test_collections_sync.py b/tests/idiomatic/integration/test_collections_sync.py index 7eb5266f..6a8a3b20 100644 --- a/tests/idiomatic/integration/test_collections_sync.py +++ b/tests/idiomatic/integration/test_collections_sync.py @@ -77,39 +77,39 @@ def test_collection_set_caller_sync( @pytest.mark.describe("test errors for unsupported Collection methods, sync") def test_collection_unsupported_methods_sync( self, - sync_collection: Collection, + sync_collection_instance: Collection, ) -> None: with pytest.raises(TypeError): - sync_collection.find_raw_batches(1, "x") + sync_collection_instance.find_raw_batches(1, "x") with pytest.raises(TypeError): - sync_collection.aggregate(1, "x") + sync_collection_instance.aggregate(1, "x") with pytest.raises(TypeError): - sync_collection.aggregate_raw_batches(1, "x") + sync_collection_instance.aggregate_raw_batches(1, "x") with pytest.raises(TypeError): - sync_collection.watch(1, "x") + sync_collection_instance.watch(1, "x") with pytest.raises(TypeError): - sync_collection.rename(1, "x") + sync_collection_instance.rename(1, "x") with pytest.raises(TypeError): - sync_collection.create_index(1, "x") + sync_collection_instance.create_index(1, "x") with pytest.raises(TypeError): - sync_collection.create_indexes(1, "x") + sync_collection_instance.create_indexes(1, "x") with pytest.raises(TypeError): - sync_collection.drop_index(1, "x") + sync_collection_instance.drop_index(1, "x") with pytest.raises(TypeError): - sync_collection.drop_indexes(1, "x") + sync_collection_instance.drop_indexes(1, "x") with pytest.raises(TypeError): - sync_collection.list_indexes(1, "x") + sync_collection_instance.list_indexes(1, "x") with pytest.raises(TypeError): - sync_collection.index_information(1, "x") + sync_collection_instance.index_information(1, "x") with pytest.raises(TypeError): - sync_collection.create_search_index(1, "x") + sync_collection_instance.create_search_index(1, "x") with pytest.raises(TypeError): - sync_collection.create_search_indexes(1, "x") + sync_collection_instance.create_search_indexes(1, "x") with pytest.raises(TypeError): - sync_collection.drop_search_index(1, "x") + sync_collection_instance.drop_search_index(1, "x") with pytest.raises(TypeError): - sync_collection.list_search_indexes(1, "x") + sync_collection_instance.list_search_indexes(1, "x") with pytest.raises(TypeError): - sync_collection.update_search_index(1, "x") + sync_collection_instance.update_search_index(1, "x") with pytest.raises(TypeError): - sync_collection.distinct(1, "x") + sync_collection_instance.distinct(1, "x") diff --git a/tests/idiomatic/integration/test_databases_async.py b/tests/idiomatic/integration/test_databases_async.py index 700b799f..b085783c 100644 --- a/tests/idiomatic/integration/test_databases_async.py +++ b/tests/idiomatic/integration/test_databases_async.py @@ -14,7 +14,7 @@ import pytest -from ..conftest import AstraDBCredentials, TEST_COLLECTION_NAME +from ..conftest import AstraDBCredentials, TEST_COLLECTION_INSTANCE_NAME from astrapy import AsyncCollection, AsyncDatabase @@ -90,16 +90,16 @@ async def test_database_unsupported_methods_async( async def test_database_get_collection_async( self, async_database: AsyncDatabase, - async_collection: AsyncCollection, + async_collection_instance: AsyncCollection, ) -> None: - collection = await async_database.get_collection(TEST_COLLECTION_NAME) - assert collection == async_collection + collection = await async_database.get_collection(TEST_COLLECTION_INSTANCE_NAME) + assert collection == async_collection_instance NAMESPACE_2 = "other_namespace" collection_ns2 = await async_database.get_collection( - TEST_COLLECTION_NAME, namespace=NAMESPACE_2 + TEST_COLLECTION_INSTANCE_NAME, namespace=NAMESPACE_2 ) assert collection_ns2 == AsyncCollection( - async_database, TEST_COLLECTION_NAME, namespace=NAMESPACE_2 + async_database, TEST_COLLECTION_INSTANCE_NAME, namespace=NAMESPACE_2 ) assert collection_ns2._astra_db_collection.astra_db.namespace == NAMESPACE_2 diff --git a/tests/idiomatic/integration/test_databases_sync.py b/tests/idiomatic/integration/test_databases_sync.py index e300186a..35b417c4 100644 --- a/tests/idiomatic/integration/test_databases_sync.py +++ b/tests/idiomatic/integration/test_databases_sync.py @@ -14,7 +14,7 @@ import pytest -from ..conftest import AstraDBCredentials, TEST_COLLECTION_NAME +from ..conftest import AstraDBCredentials, TEST_COLLECTION_INSTANCE_NAME from astrapy import Collection, Database @@ -90,17 +90,17 @@ def test_database_unsupported_methods_sync( def test_database_get_collection_sync( self, sync_database: Database, - sync_collection: Collection, + sync_collection_instance: Collection, astra_db_credentials_kwargs: AstraDBCredentials, ) -> None: - collection = sync_database.get_collection(TEST_COLLECTION_NAME) - assert collection == sync_collection + collection = sync_database.get_collection(TEST_COLLECTION_INSTANCE_NAME) + assert collection == sync_collection_instance NAMESPACE_2 = "other_namespace" collection_ns2 = sync_database.get_collection( - TEST_COLLECTION_NAME, namespace=NAMESPACE_2 + TEST_COLLECTION_INSTANCE_NAME, namespace=NAMESPACE_2 ) assert collection_ns2 == Collection( - sync_database, TEST_COLLECTION_NAME, namespace=NAMESPACE_2 + sync_database, TEST_COLLECTION_INSTANCE_NAME, namespace=NAMESPACE_2 ) assert collection_ns2._astra_db_collection.astra_db.namespace == NAMESPACE_2 diff --git a/tests/idiomatic/integration/test_ddl_async.py b/tests/idiomatic/integration/test_ddl_async.py index 69f67631..d053f652 100644 --- a/tests/idiomatic/integration/test_ddl_async.py +++ b/tests/idiomatic/integration/test_ddl_async.py @@ -14,7 +14,8 @@ import pytest -from astrapy import AsyncDatabase +from ..conftest import ASTRA_DB_SECONDARY_KEYSPACE, TEST_COLLECTION_NAME +from astrapy import AsyncCollection, AsyncDatabase class TestDDLAsync: @@ -23,13 +24,45 @@ async def test_collection_lifecycle_async( self, async_database: AsyncDatabase, ) -> None: - TEST_COLLECTION_NAME = "test_coll" + TEST_LOCAL_COLLECTION_NAME = "test_local_coll" col1 = await async_database.create_collection( - TEST_COLLECTION_NAME, + TEST_LOCAL_COLLECTION_NAME, dimension=123, metric="euclidean", indexing={"deny": ["a", "b", "c"]}, ) - col2 = await async_database.get_collection(TEST_COLLECTION_NAME) + col2 = await async_database.get_collection(TEST_LOCAL_COLLECTION_NAME) assert col1 == col2 - await async_database.drop_collection(TEST_COLLECTION_NAME) + await async_database.drop_collection(TEST_LOCAL_COLLECTION_NAME) + + @pytest.mark.describe("test of Database list_collections, async") + async def test_database_list_collections_async( + self, + async_database: AsyncDatabase, + async_collection: AsyncCollection, + ) -> None: + assert TEST_COLLECTION_NAME in await async_database.list_collection_names() + + @pytest.mark.describe("test of Database list_collections unsupported filter, async") + async def test_database_list_collections_filter_async( + self, + async_database: AsyncDatabase, + async_collection: AsyncCollection, + ) -> None: + with pytest.raises(TypeError): + await async_database.list_collection_names(filter={"k": "v"}) + + @pytest.mark.skipif( + ASTRA_DB_SECONDARY_KEYSPACE is None, reason="No secondary keyspace provided" + ) + @pytest.mark.describe( + "test of Database list_collections on cross-namespaces, async" + ) + async def test_database_list_collections_cross_namespace_async( + self, + async_database: AsyncDatabase, + async_collection: AsyncCollection, + ) -> None: + assert TEST_COLLECTION_NAME not in await async_database.list_collection_names( + namespace=ASTRA_DB_SECONDARY_KEYSPACE + ) diff --git a/tests/idiomatic/integration/test_ddl_sync.py b/tests/idiomatic/integration/test_ddl_sync.py index 94937dcd..3f7d60ca 100644 --- a/tests/idiomatic/integration/test_ddl_sync.py +++ b/tests/idiomatic/integration/test_ddl_sync.py @@ -14,7 +14,8 @@ import pytest -from astrapy import Database +from ..conftest import ASTRA_DB_SECONDARY_KEYSPACE, TEST_COLLECTION_NAME +from astrapy import Collection, Database class TestDDLSync: @@ -23,13 +24,43 @@ def test_collection_lifecycle_sync( self, sync_database: Database, ) -> None: - TEST_COLLECTION_NAME = "test_coll" + TEST_LOCAL_COLLECTION_NAME = "test_local_coll" col1 = sync_database.create_collection( - TEST_COLLECTION_NAME, + TEST_LOCAL_COLLECTION_NAME, dimension=123, metric="euclidean", indexing={"deny": ["a", "b", "c"]}, ) - col2 = sync_database.get_collection(TEST_COLLECTION_NAME) + col2 = sync_database.get_collection(TEST_LOCAL_COLLECTION_NAME) assert col1 == col2 - sync_database.drop_collection(TEST_COLLECTION_NAME) + sync_database.drop_collection(TEST_LOCAL_COLLECTION_NAME) + + @pytest.mark.describe("test of Database list_collections, sync") + def test_database_list_collections_sync( + self, + sync_database: Database, + sync_collection: Collection, + ) -> None: + assert TEST_COLLECTION_NAME in sync_database.list_collection_names() + + @pytest.mark.describe("test of Database list_collections unsupported filter, sync") + def test_database_list_collections_filter_sync( + self, + sync_database: Database, + sync_collection: Collection, + ) -> None: + with pytest.raises(TypeError): + sync_database.list_collection_names(filter={"k": "v"}) + + @pytest.mark.skipif( + ASTRA_DB_SECONDARY_KEYSPACE is None, reason="No secondary keyspace provided" + ) + @pytest.mark.describe("test of Database list_collections on cross-namespaces, sync") + def test_database_list_collections_cross_namespace_sync( + self, + sync_database: Database, + sync_collection: Collection, + ) -> None: + assert TEST_COLLECTION_NAME not in sync_database.list_collection_names( + namespace=ASTRA_DB_SECONDARY_KEYSPACE + ) diff --git a/tests/idiomatic/integration/test_dml_async.py b/tests/idiomatic/integration/test_dml_async.py new file mode 100644 index 00000000..b4f76c82 --- /dev/null +++ b/tests/idiomatic/integration/test_dml_async.py @@ -0,0 +1,76 @@ +# Copyright DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from astrapy import AsyncCollection +from astrapy.results import DeleteResult, InsertOneResult + + +class TestDMLAsync: + @pytest.mark.describe("test of collection count_documents, async") + async def test_collection_count_documents_async( + self, + async_empty_collection: AsyncCollection, + ) -> None: + assert await async_empty_collection.count_documents(filter={}) == 0 + await async_empty_collection.insert_one({"doc": 1, "group": "A"}) + await async_empty_collection.insert_one({"doc": 2, "group": "B"}) + await async_empty_collection.insert_one({"doc": 3, "group": "A"}) + assert await async_empty_collection.count_documents(filter={}) == 3 + assert await async_empty_collection.count_documents(filter={"group": "A"}) == 2 + + @pytest.mark.describe("test of collection insert_one, async") + async def test_collection_insert_one_async( + self, + async_empty_collection: AsyncCollection, + ) -> None: + io_result1 = await async_empty_collection.insert_one({"doc": 1, "group": "A"}) + assert isinstance(io_result1, InsertOneResult) + assert io_result1.acknowledged is True + io_result2 = await async_empty_collection.insert_one( + {"_id": "xxx", "doc": 2, "group": "B"} + ) + assert io_result2.inserted_id == "xxx" + assert await async_empty_collection.count_documents(filter={"group": "A"}) == 1 + + @pytest.mark.describe("test of collection delete_one, async") + async def test_collection_delete_one_async( + self, + async_empty_collection: AsyncCollection, + ) -> None: + await async_empty_collection.insert_one({"doc": 1, "group": "A"}) + await async_empty_collection.insert_one({"doc": 2, "group": "B"}) + await async_empty_collection.insert_one({"doc": 3, "group": "A"}) + assert await async_empty_collection.count_documents(filter={}) == 3 + do_result1 = await async_empty_collection.delete_one({"group": "A"}) + assert isinstance(do_result1, DeleteResult) + assert do_result1.acknowledged is True + assert do_result1.deleted_count == 1 + assert await async_empty_collection.count_documents(filter={}) == 2 + + @pytest.mark.describe("test of collection delete_many, async") + async def test_collection_delete_many_async( + self, + async_empty_collection: AsyncCollection, + ) -> None: + await async_empty_collection.insert_one({"doc": 1, "group": "A"}) + await async_empty_collection.insert_one({"doc": 2, "group": "B"}) + await async_empty_collection.insert_one({"doc": 3, "group": "A"}) + assert await async_empty_collection.count_documents(filter={}) == 3 + do_result1 = await async_empty_collection.delete_many({"group": "A"}) + assert isinstance(do_result1, DeleteResult) + assert do_result1.acknowledged is True + assert do_result1.deleted_count == 2 + assert await async_empty_collection.count_documents(filter={}) == 1 diff --git a/tests/idiomatic/integration/test_dml_sync.py b/tests/idiomatic/integration/test_dml_sync.py new file mode 100644 index 00000000..7a286391 --- /dev/null +++ b/tests/idiomatic/integration/test_dml_sync.py @@ -0,0 +1,76 @@ +# Copyright DataStax, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +from astrapy import Collection +from astrapy.results import DeleteResult, InsertOneResult + + +class TestDMLSync: + @pytest.mark.describe("test of collection count_documents, sync") + def test_collection_count_documents_sync( + self, + sync_empty_collection: Collection, + ) -> None: + assert sync_empty_collection.count_documents(filter={}) == 0 + sync_empty_collection.insert_one({"doc": 1, "group": "A"}) + sync_empty_collection.insert_one({"doc": 2, "group": "B"}) + sync_empty_collection.insert_one({"doc": 3, "group": "A"}) + assert sync_empty_collection.count_documents(filter={}) == 3 + assert sync_empty_collection.count_documents(filter={"group": "A"}) == 2 + + @pytest.mark.describe("test of collection insert_one, sync") + def test_collection_insert_one_sync( + self, + sync_empty_collection: Collection, + ) -> None: + io_result1 = sync_empty_collection.insert_one({"doc": 1, "group": "A"}) + assert isinstance(io_result1, InsertOneResult) + assert io_result1.acknowledged is True + io_result2 = sync_empty_collection.insert_one( + {"_id": "xxx", "doc": 2, "group": "B"} + ) + assert io_result2.inserted_id == "xxx" + assert sync_empty_collection.count_documents(filter={"group": "A"}) == 1 + + @pytest.mark.describe("test of collection delete_one, sync") + def test_collection_delete_one_sync( + self, + sync_empty_collection: Collection, + ) -> None: + sync_empty_collection.insert_one({"doc": 1, "group": "A"}) + sync_empty_collection.insert_one({"doc": 2, "group": "B"}) + sync_empty_collection.insert_one({"doc": 3, "group": "A"}) + assert sync_empty_collection.count_documents(filter={}) == 3 + do_result1 = sync_empty_collection.delete_one({"group": "A"}) + assert isinstance(do_result1, DeleteResult) + assert do_result1.acknowledged is True + assert do_result1.deleted_count == 1 + assert sync_empty_collection.count_documents(filter={}) == 2 + + @pytest.mark.describe("test of collection delete_many, sync") + def test_collection_delete_many_sync( + self, + sync_empty_collection: Collection, + ) -> None: + sync_empty_collection.insert_one({"doc": 1, "group": "A"}) + sync_empty_collection.insert_one({"doc": 2, "group": "B"}) + sync_empty_collection.insert_one({"doc": 3, "group": "A"}) + assert sync_empty_collection.count_documents(filter={}) == 3 + do_result1 = sync_empty_collection.delete_many({"group": "A"}) + assert isinstance(do_result1, DeleteResult) + assert do_result1.acknowledged is True + assert do_result1.deleted_count == 2 + assert sync_empty_collection.count_documents(filter={}) == 1