From 1dc4dc75adfbed01c62a14ad6db1692e5b4c932e Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 14 Jul 2025 08:03:45 +0000 Subject: [PATCH 01/11] Adding /figures support --- arangoasync/collection.py | 31 ++++++++- arangoasync/exceptions.py | 12 ++-- arangoasync/typings.py | 142 +++++++++++++++++++++++++++++++++++++- tests/test_collection.py | 6 ++ tests/test_typings.py | 60 ++++++++++++++++ 5 files changed, 244 insertions(+), 7 deletions(-) diff --git a/arangoasync/collection.py b/arangoasync/collection.py index 810ee06..f8a777f 100644 --- a/arangoasync/collection.py +++ b/arangoasync/collection.py @@ -17,6 +17,7 @@ ) from arangoasync.exceptions import ( CollectionPropertiesError, + CollectionStatisticsError, CollectionTruncateError, DocumentCountError, DocumentDeleteError, @@ -41,6 +42,7 @@ from arangoasync.serialization import Deserializer, Serializer from arangoasync.typings import ( CollectionProperties, + CollectionStatistics, IndexProperties, Json, Jsons, @@ -552,7 +554,10 @@ async def count(self) -> Result[int]: Raises: DocumentCountError: If retrieval fails. - """ + + References: + - `get-the-document-count-of-a-collection `__ + """ # noqa: E501 request = Request( method=Method.GET, endpoint=f"/_api/collection/{self.name}/count" ) @@ -565,6 +570,30 @@ def response_handler(resp: Response) -> int: return await self._executor.execute(request, response_handler) + async def statistics(self) -> Result[CollectionStatistics]: + """Get additional statistical information about the collection. + + Returns: + CollectionStatistics: Collection statistics. + + Raises: + CollectionStatisticsError: If retrieval fails. + + References: + - `get-the-collection-statistics `__ + """ # noqa: E501 + request = Request( + method=Method.GET, + endpoint=f"/_api/collection/{self.name}/figures", + ) + + def response_handler(resp: Response) -> CollectionStatistics: + if not resp.is_success: + raise CollectionStatisticsError(resp, request) + return CollectionStatistics(self.deserializer.loads(resp.raw_body)) + + return await self._executor.execute(request, response_handler) + async def has( self, document: str | Json, diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index e052fd4..c1fd86d 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -195,6 +195,14 @@ class CollectionPropertiesError(ArangoServerError): """Failed to retrieve collection properties.""" +class CollectionStatisticsError(ArangoServerError): + """Failed to retrieve collection statistics.""" + + +class CollectionTruncateError(ArangoServerError): + """Failed to truncate collection.""" + + class ClientConnectionAbortedError(ArangoClientError): """The connection was aborted.""" @@ -203,10 +211,6 @@ class ClientConnectionError(ArangoClientError): """The request was unable to reach the server.""" -class CollectionTruncateError(ArangoServerError): - """Failed to truncate collection.""" - - class CursorCloseError(ArangoServerError): """Failed to delete the cursor result from server.""" diff --git a/arangoasync/typings.py b/arangoasync/typings.py index 280e27e..d49411d 100644 --- a/arangoasync/typings.py +++ b/arangoasync/typings.py @@ -791,8 +791,6 @@ def compatibility_formatter(data: Json) -> Json: result["deleted"] = data["deleted"] if "syncByRevision" in data: result["sync_by_revision"] = data["syncByRevision"] - if "tempObjectId" in data: - result["temp_object_id"] = data["tempObjectId"] if "usesRevisionsAsDocumentIds" in data: result["rev_as_id"] = data["usesRevisionsAsDocumentIds"] if "isDisjoint" in data: @@ -819,6 +817,146 @@ def format(self, formatter: Optional[Formatter] = None) -> Json: return self.compatibility_formatter(self._data) +class CollectionStatistics(JsonWrapper): + """Statistical information about the collection. + + Example: + .. code-block:: json + + { + "figures" : { + "indexes" : { + "count" : 1, + "size" : 1234 + }, + "documentsSize" : 5601, + "cacheInUse" : false, + "cacheSize" : 0, + "cacheUsage" : 0, + "engine" : { + "documents" : 1, + "indexes" : [ + { + "type" : "primary", + "id" : 0, + "count" : 1 + } + ] + } + }, + "writeConcern" : 1, + "waitForSync" : false, + "usesRevisionsAsDocumentIds" : true, + "syncByRevision" : true, + "statusString" : "loaded", + "id" : "69123", + "isSmartChild" : false, + "schema" : null, + "name" : "products", + "type" : 2, + "status" : 3, + "count" : 1, + "cacheEnabled" : false, + "isSystem" : false, + "internalValidatorType" : 0, + "globallyUniqueId" : "hB7C02EE43DCE/69123", + "keyOptions" : { + "allowUserKeys" : true, + "type" : "traditional", + "lastValue" : 69129 + }, + "computedValues" : null, + "objectId" : "69124" + } + + References: + - `get-the-collection-statistics `__ + """ # noqa: E501 + + def __init__(self, data: Json) -> None: + super().__init__(data) + + @property + def figures(self) -> Json: + return cast(Json, self._data.get("figures")) + + @property + def write_concern(self) -> Optional[int]: + return self._data.get("writeConcern") + + @property + def wait_for_sync(self) -> Optional[bool]: + return self._data.get("waitForSync") + + @property + def use_revisions_as_document_ids(self) -> Optional[bool]: + return self._data.get("usesRevisionsAsDocumentIds") + + @property + def sync_by_revision(self) -> Optional[bool]: + return self._data.get("syncByRevision") + + @property + def status_string(self) -> Optional[str]: + return self._data.get("statusString") + + @property + def id(self) -> str: + return self._data["id"] # type: ignore[no-any-return] + + @property + def is_smart_child(self) -> bool: + return self._data["isSmartChild"] # type: ignore[no-any-return] + + @property + def schema(self) -> Optional[Json]: + return self._data.get("schema") + + @property + def name(self) -> str: + return self._data["name"] # type: ignore[no-any-return] + + @property + def type(self) -> CollectionType: + return CollectionType.from_int(self._data["type"]) + + @property + def status(self) -> CollectionStatus: + return CollectionStatus.from_int(self._data["status"]) + + @property + def count(self) -> int: + return self._data["count"] # type: ignore[no-any-return] + + @property + def cache_enabled(self) -> Optional[bool]: + return self._data.get("cacheEnabled") + + @property + def is_system(self) -> bool: + return self._data["isSystem"] # type: ignore[no-any-return] + + @property + def internal_validator_type(self) -> Optional[int]: + return self._data.get("internalValidatorType") + + @property + def globally_unique_id(self) -> str: + return self._data["globallyUniqueId"] # type: ignore[no-any-return] + + @property + def key_options(self) -> KeyOptions: + return KeyOptions(self._data["keyOptions"]) + + @property + def computed_values(self) -> Optional[Json]: + return self._data.get("computedValues") + + @property + def object_id(self) -> str: + return self._data["objectId"] # type: ignore[no-any-return] + + class IndexProperties(JsonWrapper): """Properties of an index. diff --git a/tests/test_collection.py b/tests/test_collection.py index d9214dd..dc56f81 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -5,6 +5,7 @@ from arangoasync.errno import DATA_SOURCE_NOT_FOUND, INDEX_NOT_FOUND from arangoasync.exceptions import ( CollectionPropertiesError, + CollectionStatisticsError, CollectionTruncateError, DocumentCountError, IndexCreateError, @@ -30,6 +31,11 @@ async def test_collection_misc_methods(doc_col, bad_col): assert len(properties.format()) > 1 with pytest.raises(CollectionPropertiesError): await bad_col.properties() + statistics = await doc_col.statistics() + assert statistics.name == doc_col.name + assert "figures" in statistics + with pytest.raises(CollectionStatisticsError): + await bad_col.statistics() @pytest.mark.asyncio diff --git a/tests/test_typings.py b/tests/test_typings.py index fd04fa1..3b4e5e2 100644 --- a/tests/test_typings.py +++ b/tests/test_typings.py @@ -2,6 +2,7 @@ from arangoasync.typings import ( CollectionInfo, + CollectionStatistics, CollectionStatus, CollectionType, EdgeDefinitionOptions, @@ -386,3 +387,62 @@ def test_EdgeDefinitionOptions(): ) assert options.satellites == ["col1", "col2"] + + +def test_CollectionStatistics(): + data = { + "figures": { + "indexes": {"count": 1, "size": 1234}, + "documentsSize": 5601, + "cacheInUse": False, + "cacheSize": 0, + "cacheUsage": 0, + }, + "writeConcern": 1, + "waitForSync": False, + "usesRevisionsAsDocumentIds": True, + "syncByRevision": True, + "statusString": "loaded", + "id": "69123", + "isSmartChild": False, + "schema": None, + "name": "products", + "type": 2, + "status": 3, + "count": 1, + "cacheEnabled": False, + "isSystem": False, + "internalValidatorType": 0, + "globallyUniqueId": "hB7C02EE43DCE/69123", + "keyOptions": { + "allowUserKeys": True, + "type": "traditional", + "lastValue": 69129, + }, + "computedValues": None, + "objectId": "69124", + } + + stats = CollectionStatistics(data) + + assert stats.figures == data["figures"] + assert stats.write_concern == 1 + assert stats.wait_for_sync is False + assert stats.use_revisions_as_document_ids is True + assert stats.sync_by_revision is True + assert stats.status_string == "loaded" + assert stats.id == "69123" + assert stats.is_smart_child is False + assert stats.schema is None + assert stats.name == "products" + assert stats.type == CollectionType.DOCUMENT + assert stats.status == CollectionStatus.LOADED + assert stats.count == 1 + assert stats.cache_enabled is False + assert stats.is_system is False + assert stats.internal_validator_type == 0 + assert stats.globally_unique_id == "hB7C02EE43DCE/69123" + assert isinstance(stats.key_options, KeyOptions) + assert stats.key_options["type"] == "traditional" + assert stats.computed_values is None + assert stats.object_id == "69124" From 01d2b9634cc5452b5909dd13558554b4bc458182 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 14 Jul 2025 08:38:49 +0000 Subject: [PATCH 02/11] Adding support for /responsibleShard --- arangoasync/collection.py | 33 +++++++++++++++++++++++++++++++++ arangoasync/exceptions.py | 4 ++++ tests/test_collection.py | 12 +++++++++++- 3 files changed, 48 insertions(+), 1 deletion(-) diff --git a/arangoasync/collection.py b/arangoasync/collection.py index f8a777f..270f3a5 100644 --- a/arangoasync/collection.py +++ b/arangoasync/collection.py @@ -17,6 +17,7 @@ ) from arangoasync.exceptions import ( CollectionPropertiesError, + CollectionResponsibleShardError, CollectionStatisticsError, CollectionTruncateError, DocumentCountError, @@ -594,6 +595,38 @@ def response_handler(resp: Response) -> CollectionStatistics: return await self._executor.execute(request, response_handler) + async def responsible_shard(self, document: Json) -> Result[str]: + """Return the ID of the shard responsible for given document. + + If the document does not exist, return the shard that would be + responsible. + + Args: + document (dict): Document body with "_key" field. + + Returns: + str: Shard ID. + + Raises: + CollectionResponsibleShardError: If retrieval fails. + + References: + - `get-the-responsible-shard-for-a-document `__ + """ # noqa: E501 + request = Request( + method=Method.PUT, + endpoint=f"/_api/collection/{self.name}/responsibleShard", + data=self.serializer.dumps(document), + ) + + def response_handler(resp: Response) -> str: + if resp.is_success: + body = self.deserializer.loads(resp.raw_body) + return cast(str, body["shardId"]) + raise CollectionResponsibleShardError(resp, request) + + return await self._executor.execute(request, response_handler) + async def has( self, document: str | Json, diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index c1fd86d..4f6268e 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -195,6 +195,10 @@ class CollectionPropertiesError(ArangoServerError): """Failed to retrieve collection properties.""" +class CollectionResponsibleShardError(ArangoServerError): + """Failed to retrieve responsible shard.""" + + class CollectionStatisticsError(ArangoServerError): """Failed to retrieve collection statistics.""" diff --git a/tests/test_collection.py b/tests/test_collection.py index dc56f81..04455d0 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -5,6 +5,7 @@ from arangoasync.errno import DATA_SOURCE_NOT_FOUND, INDEX_NOT_FOUND from arangoasync.exceptions import ( CollectionPropertiesError, + CollectionResponsibleShardError, CollectionStatisticsError, CollectionTruncateError, DocumentCountError, @@ -23,7 +24,7 @@ def test_collection_attributes(db, doc_col): @pytest.mark.asyncio -async def test_collection_misc_methods(doc_col, bad_col): +async def test_collection_misc_methods(doc_col, bad_col, docs): # Properties properties = await doc_col.properties() assert properties.name == doc_col.name @@ -31,12 +32,21 @@ async def test_collection_misc_methods(doc_col, bad_col): assert len(properties.format()) > 1 with pytest.raises(CollectionPropertiesError): await bad_col.properties() + + # Statistics statistics = await doc_col.statistics() assert statistics.name == doc_col.name assert "figures" in statistics with pytest.raises(CollectionStatisticsError): await bad_col.statistics() + # Shards + doc = await doc_col.insert(docs[0]) + shard = await doc_col.responsible_shard(doc) + assert isinstance(shard, str) + with pytest.raises(CollectionResponsibleShardError): + await bad_col.responsible_shard(doc) + @pytest.mark.asyncio async def test_collection_index(doc_col, bad_col, cluster): From 3ea1d35b0ffbe6fb416796510791fb6a383a1b8b Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 14 Jul 2025 09:14:44 +0000 Subject: [PATCH 03/11] Adding support for /shards --- arangoasync/collection.py | 36 ++++++++++++++++++++++++++++++++++++ arangoasync/exceptions.py | 4 ++++ tests/test_collection.py | 18 ++++++++++++------ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/arangoasync/collection.py b/arangoasync/collection.py index 270f3a5..0bfee56 100644 --- a/arangoasync/collection.py +++ b/arangoasync/collection.py @@ -18,6 +18,7 @@ from arangoasync.exceptions import ( CollectionPropertiesError, CollectionResponsibleShardError, + CollectionShardsError, CollectionStatisticsError, CollectionTruncateError, DocumentCountError, @@ -627,6 +628,41 @@ def response_handler(resp: Response) -> str: return await self._executor.execute(request, response_handler) + async def shards(self, details: Optional[bool] = None) -> Result[Json]: + """Return collection shards and properties. + + Available only in a cluster setup. + + Args: + details (bool | None): If set to `True`, include responsible + servers for these shards. + + Returns: + dict: Collection shards and properties. + + Raises: + CollectionShardsError: If retrieval fails. + + References: + - `get-the-shard-ids-of-a-collection `__ + """ # noqa: E501 + params: Params = {} + if details is not None: + params["details"] = details + + request = Request( + method=Method.GET, + endpoint=f"/_api/collection/{self.name}/shards", + params=params, + ) + + def response_handler(resp: Response) -> Json: + if not resp.is_success: + raise CollectionShardsError(resp, request) + return Response.format_body(self.deserializer.loads(resp.raw_body)) + + return await self._executor.execute(request, response_handler) + async def has( self, document: str | Json, diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index 4f6268e..f1fda18 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -199,6 +199,10 @@ class CollectionResponsibleShardError(ArangoServerError): """Failed to retrieve responsible shard.""" +class CollectionShardsError(ArangoServerError): + """Failed to retrieve collection shards.""" + + class CollectionStatisticsError(ArangoServerError): """Failed to retrieve collection statistics.""" diff --git a/tests/test_collection.py b/tests/test_collection.py index 04455d0..1c924b2 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -6,6 +6,7 @@ from arangoasync.exceptions import ( CollectionPropertiesError, CollectionResponsibleShardError, + CollectionShardsError, CollectionStatisticsError, CollectionTruncateError, DocumentCountError, @@ -24,7 +25,7 @@ def test_collection_attributes(db, doc_col): @pytest.mark.asyncio -async def test_collection_misc_methods(doc_col, bad_col, docs): +async def test_collection_misc_methods(doc_col, bad_col, docs, cluster): # Properties properties = await doc_col.properties() assert properties.name == doc_col.name @@ -41,11 +42,16 @@ async def test_collection_misc_methods(doc_col, bad_col, docs): await bad_col.statistics() # Shards - doc = await doc_col.insert(docs[0]) - shard = await doc_col.responsible_shard(doc) - assert isinstance(shard, str) - with pytest.raises(CollectionResponsibleShardError): - await bad_col.responsible_shard(doc) + if cluster: + doc = await doc_col.insert(docs[0]) + shard = await doc_col.responsible_shard(doc) + assert isinstance(shard, str) + with pytest.raises(CollectionResponsibleShardError): + await bad_col.responsible_shard(doc) + shards = await doc_col.shards(details=True) + assert isinstance(shards, dict) + with pytest.raises(CollectionShardsError): + await bad_col.shards() @pytest.mark.asyncio From 73e54cfe5b52322d80d588d2cba1eca44b702502 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 14 Jul 2025 09:25:20 +0000 Subject: [PATCH 04/11] Adding support for /revision --- arangoasync/collection.py | 29 +++++++++++++++++++++++++++-- arangoasync/exceptions.py | 4 ++++ tests/test_collection.py | 7 +++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/arangoasync/collection.py b/arangoasync/collection.py index 0bfee56..3cb96f2 100644 --- a/arangoasync/collection.py +++ b/arangoasync/collection.py @@ -18,6 +18,7 @@ from arangoasync.exceptions import ( CollectionPropertiesError, CollectionResponsibleShardError, + CollectionRevisionError, CollectionShardsError, CollectionStatisticsError, CollectionTruncateError, @@ -638,7 +639,7 @@ async def shards(self, details: Optional[bool] = None) -> Result[Json]: servers for these shards. Returns: - dict: Collection shards and properties. + dict: Collection shards. Raises: CollectionShardsError: If retrieval fails. @@ -659,7 +660,31 @@ async def shards(self, details: Optional[bool] = None) -> Result[Json]: def response_handler(resp: Response) -> Json: if not resp.is_success: raise CollectionShardsError(resp, request) - return Response.format_body(self.deserializer.loads(resp.raw_body)) + return cast(Json, self.deserializer.loads(resp.raw_body)["shards"]) + + return await self._executor.execute(request, response_handler) + + async def revision(self) -> Result[str]: + """Return collection revision. + + Returns: + str: Collection revision. + + Raises: + CollectionRevisionError: If retrieval fails. + + References: + - `get-the-collection-revision-id `__ + """ # noqa: E501 + request = Request( + method=Method.GET, + endpoint=f"/_api/collection/{self.name}/revision", + ) + + def response_handler(resp: Response) -> str: + if not resp.is_success: + raise CollectionRevisionError(resp, request) + return cast(str, self.deserializer.loads(resp.raw_body)["revision"]) return await self._executor.execute(request, response_handler) diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index f1fda18..c793873 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -199,6 +199,10 @@ class CollectionResponsibleShardError(ArangoServerError): """Failed to retrieve responsible shard.""" +class CollectionRevisionError(ArangoServerError): + """Failed to retrieve collection revision.""" + + class CollectionShardsError(ArangoServerError): """Failed to retrieve collection shards.""" diff --git a/tests/test_collection.py b/tests/test_collection.py index 1c924b2..bed8787 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -6,6 +6,7 @@ from arangoasync.exceptions import ( CollectionPropertiesError, CollectionResponsibleShardError, + CollectionRevisionError, CollectionShardsError, CollectionStatisticsError, CollectionTruncateError, @@ -53,6 +54,12 @@ async def test_collection_misc_methods(doc_col, bad_col, docs, cluster): with pytest.raises(CollectionShardsError): await bad_col.shards() + # Revision + revision = await doc_col.revision() + assert isinstance(revision, str) + with pytest.raises(CollectionRevisionError): + await bad_col.revision() + @pytest.mark.asyncio async def test_collection_index(doc_col, bad_col, cluster): From ef0eca18c4a8eb90b556a394709421ddf850893d Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 14 Jul 2025 09:53:22 +0000 Subject: [PATCH 05/11] Adding support for /checksum --- arangoasync/collection.py | 38 ++++++++++++++++++++++++++++++++++++++ arangoasync/exceptions.py | 4 ++++ tests/test_collection.py | 10 +++++++++- 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/arangoasync/collection.py b/arangoasync/collection.py index 3cb96f2..11259a2 100644 --- a/arangoasync/collection.py +++ b/arangoasync/collection.py @@ -16,6 +16,7 @@ HTTP_PRECONDITION_FAILED, ) from arangoasync.exceptions import ( + CollectionChecksumError, CollectionPropertiesError, CollectionResponsibleShardError, CollectionRevisionError, @@ -688,6 +689,43 @@ def response_handler(resp: Response) -> str: return await self._executor.execute(request, response_handler) + async def checksum( + self, with_rev: Optional[bool] = None, with_data: Optional[bool] = None + ) -> Result[str]: + """Calculate collection checksum. + + Args: + with_rev (bool | None): Include document revisions in checksum calculation. + with_data (bool | None): Include document data in checksum calculation. + + Returns: + str: Collection checksum. + + Raises: + CollectionChecksumError: If retrieval fails. + + References: + - `get-the-collection-checksum `__ + """ # noqa: E501 + params: Params = {} + if with_rev is not None: + params["withRevision"] = with_rev + if with_data is not None: + params["withData"] = with_data + + request = Request( + method=Method.GET, + endpoint=f"/_api/collection/{self.name}/checksum", + params=params, + ) + + def response_handler(resp: Response) -> str: + if not resp.is_success: + raise CollectionChecksumError(resp, request) + return cast(str, self.deserializer.loads(resp.raw_body)["checksum"]) + + return await self._executor.execute(request, response_handler) + async def has( self, document: str | Json, diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index c793873..50c6a1c 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -183,6 +183,10 @@ class CollectionCreateError(ArangoServerError): """Failed to create collection.""" +class CollectionChecksumError(ArangoServerError): + """Failed to retrieve collection checksum.""" + + class CollectionDeleteError(ArangoServerError): """Failed to delete collection.""" diff --git a/tests/test_collection.py b/tests/test_collection.py index bed8787..b9b69da 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -4,6 +4,7 @@ from arangoasync.errno import DATA_SOURCE_NOT_FOUND, INDEX_NOT_FOUND from arangoasync.exceptions import ( + CollectionChecksumError, CollectionPropertiesError, CollectionResponsibleShardError, CollectionRevisionError, @@ -27,6 +28,8 @@ def test_collection_attributes(db, doc_col): @pytest.mark.asyncio async def test_collection_misc_methods(doc_col, bad_col, docs, cluster): + doc = await doc_col.insert(docs[0]) + # Properties properties = await doc_col.properties() assert properties.name == doc_col.name @@ -44,7 +47,6 @@ async def test_collection_misc_methods(doc_col, bad_col, docs, cluster): # Shards if cluster: - doc = await doc_col.insert(docs[0]) shard = await doc_col.responsible_shard(doc) assert isinstance(shard, str) with pytest.raises(CollectionResponsibleShardError): @@ -60,6 +62,12 @@ async def test_collection_misc_methods(doc_col, bad_col, docs, cluster): with pytest.raises(CollectionRevisionError): await bad_col.revision() + # Checksum + checksum = await doc_col.checksum(with_rev=True, with_data=True) + assert isinstance(checksum, str) + with pytest.raises(CollectionChecksumError): + await bad_col.checksum() + @pytest.mark.asyncio async def test_collection_index(doc_col, bad_col, cluster): From 173ae49a0fcc34d243aa936ff874708e9b1bc740 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 14 Jul 2025 10:19:12 +0000 Subject: [PATCH 06/11] Added support for /key-generators --- arangoasync/database.py | 24 ++++++++++++++++++++++++ arangoasync/exceptions.py | 4 ++++ tests/test_database.py | 7 +++++++ 3 files changed, 35 insertions(+) diff --git a/arangoasync/database.py b/arangoasync/database.py index c188290..578222f 100644 --- a/arangoasync/database.py +++ b/arangoasync/database.py @@ -22,6 +22,7 @@ AsyncJobListError, CollectionCreateError, CollectionDeleteError, + CollectionKeyGeneratorsError, CollectionListError, DatabaseCreateError, DatabaseDeleteError, @@ -695,6 +696,29 @@ def response_handler(resp: Response) -> bool: return await self._executor.execute(request, response_handler) + async def key_generators(self) -> Result[List[str]]: + """Returns the available key generators for collections. + + Returns: + list: List of available key generators. + + Raises: + CollectionKeyGeneratorsError: If retrieval fails. + + References: + - `get-the-available-key-generators `__ + """ # noqa: E501 + request = Request(method=Method.GET, endpoint="/_api/key-generators") + + def response_handler(resp: Response) -> List[str]: + if not resp.is_success: + raise CollectionKeyGeneratorsError(resp, request) + return cast( + List[str], self.deserializer.loads(resp.raw_body)["keyGenerators"] + ) + + return await self._executor.execute(request, response_handler) + async def has_document( self, document: str | Json, diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index 50c6a1c..00d480f 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -191,6 +191,10 @@ class CollectionDeleteError(ArangoServerError): """Failed to delete collection.""" +class CollectionKeyGeneratorsError(ArangoServerError): + """Failed to retrieve key generators.""" + + class CollectionListError(ArangoServerError): """Failed to retrieve collections.""" diff --git a/tests/test_database.py b/tests/test_database.py index eb7daa3..a616734 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -6,6 +6,7 @@ from arangoasync.exceptions import ( CollectionCreateError, CollectionDeleteError, + CollectionKeyGeneratorsError, CollectionListError, DatabaseCreateError, DatabaseDeleteError, @@ -55,6 +56,12 @@ async def test_database_misc_methods(sys_db, db, bad_db, cluster): with pytest.raises(ServerVersionError): await bad_db.version() + # key generators + key_generators = await db.key_generators() + assert isinstance(key_generators, list) + with pytest.raises(CollectionKeyGeneratorsError): + await bad_db.key_generators() + @pytest.mark.asyncio async def test_create_drop_database( From c4024c5ec578c98dcdc590788469cb6e5399bc0f Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 14 Jul 2025 10:28:46 +0000 Subject: [PATCH 07/11] Skipping part of test in 3.11 --- tests/test_database.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/test_database.py b/tests/test_database.py index a616734..7058ac1 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -1,6 +1,7 @@ import asyncio import pytest +from packaging import version from arangoasync.collection import StandardCollection from arangoasync.exceptions import ( @@ -22,7 +23,7 @@ @pytest.mark.asyncio -async def test_database_misc_methods(sys_db, db, bad_db, cluster): +async def test_database_misc_methods(sys_db, db, bad_db, cluster, db_version): # Status status = await sys_db.status() assert status["server"] == "arango" @@ -51,16 +52,17 @@ async def test_database_misc_methods(sys_db, db, bad_db, cluster): await bad_db.reload_jwt_secrets() # Version - version = await sys_db.version() - assert version["version"].startswith("3.") + v = await sys_db.version() + assert v["version"].startswith("3.") with pytest.raises(ServerVersionError): await bad_db.version() # key generators - key_generators = await db.key_generators() - assert isinstance(key_generators, list) - with pytest.raises(CollectionKeyGeneratorsError): - await bad_db.key_generators() + if db_version >= version.parse("3.12.0"): + key_generators = await db.key_generators() + assert isinstance(key_generators, list) + with pytest.raises(CollectionKeyGeneratorsError): + await bad_db.key_generators() @pytest.mark.asyncio From 1e79243d2a28dc78d9ce275e708cbd71e6a27c14 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Tue, 22 Jul 2025 19:20:02 +0000 Subject: [PATCH 08/11] Adding configure method --- arangoasync/collection.py | 77 ++++++++++++++++++++++++++++++++++++--- arangoasync/exceptions.py | 4 ++ tests/test_collection.py | 8 ++++ 3 files changed, 83 insertions(+), 6 deletions(-) diff --git a/arangoasync/collection.py b/arangoasync/collection.py index 11259a2..8312df7 100644 --- a/arangoasync/collection.py +++ b/arangoasync/collection.py @@ -17,6 +17,7 @@ ) from arangoasync.exceptions import ( CollectionChecksumError, + CollectionConfigureError, CollectionPropertiesError, CollectionResponsibleShardError, CollectionRevisionError, @@ -507,7 +508,71 @@ async def properties(self) -> Result[CollectionProperties]: def response_handler(resp: Response) -> CollectionProperties: if not resp.is_success: raise CollectionPropertiesError(resp, request) - return CollectionProperties(self._executor.deserialize(resp.raw_body)) + return CollectionProperties(self.deserializer.loads(resp.raw_body)) + + return await self._executor.execute(request, response_handler) + + async def configure( + self, + cache_enabled: Optional[bool] = None, + computed_values: Optional[Jsons] = None, + replication_factor: Optional[int | str] = None, + schema: Optional[Json] = None, + wait_for_sync: Optional[bool] = None, + write_concern: Optional[int] = None, + ) -> Result[CollectionProperties]: + """Changes the properties of a collection. + + Only the provided attributes are updated. + + Args: + cache_enabled (bool | None): Whether the in-memory hash cache + for documents should be enabled for this collection. + computed_values (list | None): An optional list of objects, each + representing a computed value. + replication_factor (int | None): In a cluster, this attribute determines + how many copies of each shard are kept on different DB-Servers. + For SatelliteCollections, it needs to be the string "satellite". + schema (dict | None): The configuration of the collection-level schema + validation for documents. + wait_for_sync (bool | None): If set to `True`, the data is synchronized + to disk before returning from a document create, update, replace or + removal operation. + write_concern (int | None): Determines how many copies of each shard are + required to be in sync on the different DB-Servers. + + Returns: + CollectionProperties: Properties. + + Raises: + CollectionConfigureError: If configuration fails. + + References: + - `change-the-properties-of-a-collection `__ + """ # noqa: E501 + data: Json = {} + if cache_enabled is not None: + data["cacheEnabled"] = cache_enabled + if computed_values is not None: + data["computedValues"] = computed_values + if replication_factor is not None: + data["replicationFactor"] = replication_factor + if schema is not None: + data["schema"] = schema + if wait_for_sync is not None: + data["waitForSync"] = wait_for_sync + if write_concern is not None: + data["writeConcern"] = write_concern + request = Request( + method=Method.PUT, + endpoint=f"/_api/collection/{self.name}/properties", + data=self.serializer.dumps(data), + ) + + def response_handler(resp: Response) -> CollectionProperties: + if not resp.is_success: + raise CollectionConfigureError(resp, request) + return CollectionProperties(self.deserializer.loads(resp.raw_body)) return await self._executor.execute(request, response_handler) @@ -1605,9 +1670,9 @@ async def insert( def response_handler(resp: Response) -> bool | Json: if resp.is_success: - if silent is True: + if silent: return True - return self._executor.deserialize(resp.raw_body) + return self.deserializer.loads(resp.raw_body) msg: Optional[str] = None if resp.status_code == HTTP_BAD_PARAMETER: msg = ( @@ -1712,7 +1777,7 @@ def response_handler(resp: Response) -> bool | Json: if resp.is_success: if silent is True: return True - return self._executor.deserialize(resp.raw_body) + return self.deserializer.loads(resp.raw_body) msg: Optional[str] = None if resp.status_code == HTTP_PRECONDITION_FAILED: raise DocumentRevisionError(resp, request) @@ -1802,7 +1867,7 @@ def response_handler(resp: Response) -> bool | Json: if resp.is_success: if silent is True: return True - return self._executor.deserialize(resp.raw_body) + return self.deserializer.loads(resp.raw_body) msg: Optional[str] = None if resp.status_code == HTTP_PRECONDITION_FAILED: raise DocumentRevisionError(resp, request) @@ -1887,7 +1952,7 @@ def response_handler(resp: Response) -> bool | Json: if resp.is_success: if silent is True: return True - return self._executor.deserialize(resp.raw_body) + return self.deserializer.loads(resp.raw_body) msg: Optional[str] = None if resp.status_code == HTTP_PRECONDITION_FAILED: raise DocumentRevisionError(resp, request) diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index 00d480f..14a955e 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -187,6 +187,10 @@ class CollectionChecksumError(ArangoServerError): """Failed to retrieve collection checksum.""" +class CollectionConfigureError(ArangoServerError): + """Failed to configure collection properties.""" + + class CollectionDeleteError(ArangoServerError): """Failed to delete collection.""" diff --git a/tests/test_collection.py b/tests/test_collection.py index b9b69da..d0a7065 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -5,6 +5,7 @@ from arangoasync.errno import DATA_SOURCE_NOT_FOUND, INDEX_NOT_FOUND from arangoasync.exceptions import ( CollectionChecksumError, + CollectionConfigureError, CollectionPropertiesError, CollectionResponsibleShardError, CollectionRevisionError, @@ -38,6 +39,13 @@ async def test_collection_misc_methods(doc_col, bad_col, docs, cluster): with pytest.raises(CollectionPropertiesError): await bad_col.properties() + # Configure + wfs = not properties.wait_for_sync + new_properties = await doc_col.configure(wait_for_sync=wfs) + assert new_properties.wait_for_sync == wfs + with pytest.raises(CollectionConfigureError): + await bad_col.configure(wait_for_sync=wfs) + # Statistics statistics = await doc_col.statistics() assert statistics.name == doc_col.name From 76c58c3ad164c4be206ee37d16c3d99f732fdcea Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 28 Jul 2025 16:41:13 +0000 Subject: [PATCH 09/11] Adding renaming method --- arangoasync/collection.py | 39 +++++++++++++++++++++++++++++++++++++++ arangoasync/exceptions.py | 4 ++++ tests/test_collection.py | 23 +++++++++++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/arangoasync/collection.py b/arangoasync/collection.py index 8312df7..2b75512 100644 --- a/arangoasync/collection.py +++ b/arangoasync/collection.py @@ -19,6 +19,7 @@ CollectionChecksumError, CollectionConfigureError, CollectionPropertiesError, + CollectionRenameError, CollectionResponsibleShardError, CollectionRevisionError, CollectionShardsError, @@ -576,6 +577,44 @@ def response_handler(resp: Response) -> CollectionProperties: return await self._executor.execute(request, response_handler) + async def rename(self, new_name: str) -> Result[bool]: + """Rename the collection. + + Renames may not be reflected immediately in async execution, batch + execution or transactions. It is recommended to initialize new API + wrappers after a rename. + + Note: + Renaming collections is not supported in cluster deployments. + + Args: + new_name (str): New collection name. + + Returns: + bool: `True` if the collection was renamed successfully. + + Raises: + CollectionRenameError: If rename fails. + + References: + - `rename-a-collection `__ + """ # noqa: E501 + data: Json = {"name": new_name} + request = Request( + method=Method.PUT, + endpoint=f"/_api/collection/{self.name}/rename", + data=self.serializer.dumps(data), + ) + + def response_handler(resp: Response) -> bool: + if not resp.is_success: + raise CollectionRenameError(resp, request) + self._name = new_name + self._id_prefix = f"{new_name}/" + return True + + return await self._executor.execute(request, response_handler) + async def truncate( self, wait_for_sync: Optional[bool] = None, diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index 14a955e..5fe3249 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -207,6 +207,10 @@ class CollectionPropertiesError(ArangoServerError): """Failed to retrieve collection properties.""" +class CollectionRenameError(ArangoServerError): + """Failed to rename collection.""" + + class CollectionResponsibleShardError(ArangoServerError): """Failed to retrieve responsible shard.""" diff --git a/tests/test_collection.py b/tests/test_collection.py index d0a7065..beec5af 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -7,6 +7,7 @@ CollectionChecksumError, CollectionConfigureError, CollectionPropertiesError, + CollectionRenameError, CollectionResponsibleShardError, CollectionRevisionError, CollectionShardsError, @@ -19,6 +20,7 @@ IndexListError, IndexLoadError, ) +from tests.helpers import generate_col_name def test_collection_attributes(db, doc_col): @@ -77,6 +79,27 @@ async def test_collection_misc_methods(doc_col, bad_col, docs, cluster): await bad_col.checksum() +@pytest.mark.asyncio +async def test_collection_rename(cluster, db, bad_col, docs): + if cluster: + pytest.skip("Renaming collections is not supported in cluster deployments.") + + with pytest.raises(CollectionRenameError): + await bad_col.rename("new_name") + + col_name = generate_col_name() + new_name = generate_col_name() + try: + await db.create_collection(col_name) + col = db.collection(col_name) + await col.rename(new_name) + assert col.name == new_name + doc = await col.insert(docs[0]) + assert col.get_col_name(doc) == new_name + finally: + db.delete_collection(new_name, ignore_missing=True) + + @pytest.mark.asyncio async def test_collection_index(doc_col, bad_col, cluster): # Create indexes From 514bc28024de95fcb44e69043e25d4571023d3c4 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 28 Jul 2025 17:03:21 +0000 Subject: [PATCH 10/11] recalculate-the-document-count-of-a-collection --- arangoasync/collection.py | 31 ++++++++++++++++++++++++------- arangoasync/exceptions.py | 4 ++++ tests/test_collection.py | 6 ++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/arangoasync/collection.py b/arangoasync/collection.py index 2b75512..3a6f8bc 100644 --- a/arangoasync/collection.py +++ b/arangoasync/collection.py @@ -19,6 +19,7 @@ CollectionChecksumError, CollectionConfigureError, CollectionPropertiesError, + CollectionRecalculateCountError, CollectionRenameError, CollectionResponsibleShardError, CollectionRevisionError, @@ -489,6 +490,26 @@ def response_handler(resp: Response) -> bool: return await self._executor.execute(request, response_handler) + async def recalculate_count(self) -> None: + """Recalculate the document count. + + Raises: + CollectionRecalculateCountError: If re-calculation fails. + + References: + - `recalculate-the-document-count-of-a-collection `__ + """ # noqa: E501 + request = Request( + method=Method.PUT, + endpoint=f"/_api/collection/{self.name}/recalculateCount", + ) + + def response_handler(resp: Response) -> None: + if not resp.is_success: + raise CollectionRecalculateCountError(resp, request) + + await self._executor.execute(request, response_handler) + async def properties(self) -> Result[CollectionProperties]: """Return the full properties of the current collection. @@ -577,7 +598,7 @@ def response_handler(resp: Response) -> CollectionProperties: return await self._executor.execute(request, response_handler) - async def rename(self, new_name: str) -> Result[bool]: + async def rename(self, new_name: str) -> None: """Rename the collection. Renames may not be reflected immediately in async execution, batch @@ -590,9 +611,6 @@ async def rename(self, new_name: str) -> Result[bool]: Args: new_name (str): New collection name. - Returns: - bool: `True` if the collection was renamed successfully. - Raises: CollectionRenameError: If rename fails. @@ -606,14 +624,13 @@ async def rename(self, new_name: str) -> Result[bool]: data=self.serializer.dumps(data), ) - def response_handler(resp: Response) -> bool: + def response_handler(resp: Response) -> None: if not resp.is_success: raise CollectionRenameError(resp, request) self._name = new_name self._id_prefix = f"{new_name}/" - return True - return await self._executor.execute(request, response_handler) + await self._executor.execute(request, response_handler) async def truncate( self, diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index 5fe3249..07c14e3 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -207,6 +207,10 @@ class CollectionPropertiesError(ArangoServerError): """Failed to retrieve collection properties.""" +class CollectionRecalculateCountError(ArangoServerError): + """Failed to recalculate document count.""" + + class CollectionRenameError(ArangoServerError): """Failed to rename collection.""" diff --git a/tests/test_collection.py b/tests/test_collection.py index beec5af..c25e156 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -7,6 +7,7 @@ CollectionChecksumError, CollectionConfigureError, CollectionPropertiesError, + CollectionRecalculateCountError, CollectionRenameError, CollectionResponsibleShardError, CollectionRevisionError, @@ -78,6 +79,11 @@ async def test_collection_misc_methods(doc_col, bad_col, docs, cluster): with pytest.raises(CollectionChecksumError): await bad_col.checksum() + # Recalculate count + with pytest.raises(CollectionRecalculateCountError): + await bad_col.recalculate_count() + await doc_col.recalculate_count() + @pytest.mark.asyncio async def test_collection_rename(cluster, db, bad_col, docs): From 67a8e6569b185b34c62c66c92074ffd10349fbc0 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 28 Jul 2025 17:36:50 +0000 Subject: [PATCH 11/11] compact-a-collection --- arangoasync/collection.py | 26 ++++++++++++++++++++++++++ arangoasync/exceptions.py | 4 ++++ tests/test_collection.py | 7 +++++++ 3 files changed, 37 insertions(+) diff --git a/arangoasync/collection.py b/arangoasync/collection.py index 3a6f8bc..e3d12ee 100644 --- a/arangoasync/collection.py +++ b/arangoasync/collection.py @@ -17,6 +17,7 @@ ) from arangoasync.exceptions import ( CollectionChecksumError, + CollectionCompactError, CollectionConfigureError, CollectionPropertiesError, CollectionRecalculateCountError, @@ -48,6 +49,7 @@ from arangoasync.result import Result from arangoasync.serialization import Deserializer, Serializer from arangoasync.typings import ( + CollectionInfo, CollectionProperties, CollectionStatistics, IndexProperties, @@ -632,6 +634,30 @@ def response_handler(resp: Response) -> None: await self._executor.execute(request, response_handler) + async def compact(self) -> Result[CollectionInfo]: + """Compact a collection. + + Returns: + CollectionInfo: Collection information. + + Raises: + CollectionCompactError: If compaction fails. + + References: + - `compact-a-collection `__ + """ # noqa: E501 + request = Request( + method=Method.PUT, + endpoint=f"/_api/collection/{self.name}/compact", + ) + + def response_handler(resp: Response) -> CollectionInfo: + if not resp.is_success: + raise CollectionCompactError(resp, request) + return CollectionInfo(self.deserializer.loads(resp.raw_body)) + + return await self._executor.execute(request, response_handler) + async def truncate( self, wait_for_sync: Optional[bool] = None, diff --git a/arangoasync/exceptions.py b/arangoasync/exceptions.py index 07c14e3..5de6ea4 100644 --- a/arangoasync/exceptions.py +++ b/arangoasync/exceptions.py @@ -191,6 +191,10 @@ class CollectionConfigureError(ArangoServerError): """Failed to configure collection properties.""" +class CollectionCompactError(ArangoServerError): + """Failed to compact collection.""" + + class CollectionDeleteError(ArangoServerError): """Failed to delete collection.""" diff --git a/tests/test_collection.py b/tests/test_collection.py index c25e156..fb8d7ba 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -5,6 +5,7 @@ from arangoasync.errno import DATA_SOURCE_NOT_FOUND, INDEX_NOT_FOUND from arangoasync.exceptions import ( CollectionChecksumError, + CollectionCompactError, CollectionConfigureError, CollectionPropertiesError, CollectionRecalculateCountError, @@ -84,6 +85,12 @@ async def test_collection_misc_methods(doc_col, bad_col, docs, cluster): await bad_col.recalculate_count() await doc_col.recalculate_count() + # Compact + with pytest.raises(CollectionCompactError): + await bad_col.compact() + res = await doc_col.compact() + assert res.name == doc_col.name + @pytest.mark.asyncio async def test_collection_rename(cluster, db, bad_col, docs): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy