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):