From 173f2b367d68b5f5c7edf793a09fe00334a8791f Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Fri, 28 Mar 2025 10:34:12 +0530 Subject: [PATCH 1/4] Collection Maintenance (#367) * Adding missing truncate parameters * Removing unused parameter * Minor adjustments * Removing note that is no longer relevant. * Update arango/collection.py Co-authored-by: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> * Update arango/collection.py Co-authored-by: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> --------- Co-authored-by: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> --- arango/collection.py | 85 +++++++++------------------------------- arango/utils.py | 8 ++-- tests/test_collection.py | 2 + 3 files changed, 24 insertions(+), 71 deletions(-) diff --git a/arango/collection.py b/arango/collection.py index e2dfcd2a..7f79fb3f 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -566,15 +566,31 @@ def response_handler(resp: Response) -> bool: return self._execute(request, response_handler) - def truncate(self) -> Result[bool]: + def truncate( + self, + sync: Optional[bool] = None, + compact: Optional[bool] = None, + ) -> Result[bool]: """Delete all documents in the collection. + :param sync: Block until deletion operation is synchronized to disk. + :type sync: bool | None + :param compact: Whether to compact the collection after truncation. + :type compact: bool | None :return: True if collection was truncated successfully. :rtype: bool :raise arango.exceptions.CollectionTruncateError: If operation fails. """ + params: Json = {} + if sync is not None: + params["waitForSync"] = sync + if compact is not None: + params["compact"] = compact + request = Request( - method="put", endpoint=f"/_api/collection/{self.name}/truncate" + method="put", + endpoint=f"/_api/collection/{self.name}/truncate", + params=params, ) def response_handler(resp: Response) -> bool: @@ -1747,14 +1763,6 @@ def insert_many( successfully (returns document metadata) and which were not (returns exception object). - .. note:: - - In edge/vertex collections, this method does NOT provide the - transactional guarantees and validations that single insert - operation does for graphs. If these properties are required, see - :func:`arango.database.StandardDatabase.begin_batch_execution` - for an alternative approach. - :param documents: List of new documents to insert. If they contain the "_key" or "_id" fields, the values are used as the keys of the new documents (auto-generated otherwise). Any "_rev" field is ignored. @@ -1876,14 +1884,6 @@ def update_many( (returns exception object). Alternatively, you can rely on setting **raise_on_document_error** to True (defaults to False). - .. note:: - - In edge/vertex collections, this method does NOT provide the - transactional guarantees and validations that single update - operation does for graphs. If these properties are required, see - :func:`arango.database.StandardDatabase.begin_batch_execution` - for an alternative approach. - :param documents: Partial or full documents with the updated values. They must contain the "_id" or "_key" fields. :type documents: [dict] @@ -1995,14 +1995,6 @@ def update_match( ) -> Result[int]: """Update matching documents. - .. note:: - - In edge/vertex collections, this method does NOT provide the - transactional guarantees and validations that single update - operation does for graphs. If these properties are required, see - :func:`arango.database.StandardDatabase.begin_batch_execution` - for an alternative approach. - :param filters: Document filters. :type filters: dict :param body: Full or partial document body with the updates. @@ -2085,14 +2077,6 @@ def replace_many( successfully (returns document metadata) and which were not (returns exception object). - .. note:: - - In edge/vertex collections, this method does NOT provide the - transactional guarantees and validations that single replace - operation does for graphs. If these properties are required, see - :func:`arango.database.StandardDatabase.begin_batch_execution` - for an alternative approach. - :param documents: New documents to replace the old ones with. They must contain the "_id" or "_key" fields. Edge documents must also have "_from" and "_to" fields. @@ -2187,14 +2171,6 @@ def replace_match( ) -> Result[int]: """Replace matching documents. - .. note:: - - In edge/vertex collections, this method does NOT provide the - transactional guarantees and validations that single replace - operation does for graphs. If these properties are required, see - :func:`arango.database.StandardDatabase.begin_batch_execution` - for an alternative approach. - :param filters: Document filters. :type filters: dict :param body: New document body. @@ -2263,14 +2239,6 @@ def delete_many( successfully (returns document metadata) and which were not (returns exception object). - .. note:: - - In edge/vertex collections, this method does NOT provide the - transactional guarantees and validations that single delete - operation does for graphs. If these properties are required, see - :func:`arango.database.StandardDatabase.begin_batch_execution` - for an alternative approach. - :param documents: Document IDs, keys or bodies. Document bodies must contain the "_id" or "_key" fields. :type documents: [str | dict] @@ -2354,14 +2322,6 @@ def delete_match( ) -> Result[int]: """Delete matching documents. - .. note:: - - In edge/vertex collections, this method does NOT provide the - transactional guarantees and validations that single delete - operation does for graphs. If these properties are required, see - :func:`arango.database.StandardDatabase.begin_batch_execution` - for an alternative approach. - :param filters: Document filters. :type filters: dict :param limit: Max number of documents to delete. If the limit is lower @@ -2428,14 +2388,6 @@ def import_bulk( This method is faster than :func:`arango.collection.Collection.insert_many` but does not return as many details. - .. note:: - - In edge/vertex collections, this method does NOT provide the - transactional guarantees and validations that single insert - operation does for graphs. If these properties are required, see - :func:`arango.database.StandardDatabase.begin_batch_execution` - for an alternative approach. - :param documents: List of new documents to insert. If they contain the "_key" or "_id" fields, the values are used as the keys of the new documents (auto-generated otherwise). Any "_rev" field is ignored. @@ -2757,7 +2709,6 @@ def update( "returnNew": return_new, "returnOld": return_old, "ignoreRevs": not check_rev, - "overwrite": not check_rev, "silent": silent, } if sync is not None: diff --git a/arango/utils.py b/arango/utils.py index 89a0eca5..822bc736 100644 --- a/arango/utils.py +++ b/arango/utils.py @@ -64,11 +64,11 @@ def get_doc_id(doc: Union[str, Json]) -> str: def is_none_or_int(obj: Any) -> bool: - """Check if obj is None or an integer. + """Check if obj is None or a positive integer. :param obj: Object to check. :type obj: Any - :return: True if object is None or an integer. + :return: True if object is None or a positive integer. :rtype: bool """ return obj is None or (isinstance(obj, int) and obj >= 0) @@ -128,11 +128,11 @@ def build_filter_conditions(filters: Json) -> str: return "FILTER " + " AND ".join(conditions) -def validate_sort_parameters(sort: Sequence[Json]) -> bool: +def validate_sort_parameters(sort: Jsons) -> bool: """Validate sort parameters for an AQL query. :param sort: Document sort parameters. - :type sort: Sequence[Json] + :type sort: Jsons :return: Validation success. :rtype: bool :raise arango.exceptions.SortValidationError: If sort parameters are invalid. diff --git a/tests/test_collection.py b/tests/test_collection.py index 7ab72800..c11a6541 100644 --- a/tests/test_collection.py +++ b/tests/test_collection.py @@ -136,6 +136,8 @@ def test_collection_misc_methods(col, bad_col, cluster): # Test truncate collection assert col.truncate() is True assert len(col) == 0 + assert col.truncate(sync=True, compact=False) is True + assert len(col) == 0 # Test truncate with bad collection with assert_raises(CollectionTruncateError) as err: From 6e5f8f12c66efe2db9068fc5323c0bc7fca32c6c Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Sun, 30 Mar 2025 05:36:08 +0530 Subject: [PATCH 2/4] Mentioning async driver (#368) --- README.md | 2 ++ arango/request.py | 2 +- docs/index.rst | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f76c608e..d4b995fd 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,8 @@ Python driver for [ArangoDB](https://www.arangodb.com), a scalable multi-model database natively supporting documents, graphs and search. +If you're interested in using asyncio, please check [python-arango-async](https://github.com/arangodb/python-arango-async). + ## Requirements - ArangoDB version 3.11+ diff --git a/arango/request.py b/arango/request.py index 41a0ac66..abb2b0db 100644 --- a/arango/request.py +++ b/arango/request.py @@ -12,7 +12,7 @@ def normalize_headers( if driver_flags is not None: for flag in driver_flags: flags = flags + flag + ";" - driver_version = "8.1.5" + driver_version = "8.1.7" driver_header = "python-arango/" + driver_version + " (" + flags + ")" normalized_headers: Headers = { "charset": "utf-8", diff --git a/docs/index.rst b/docs/index.rst index 06955e0d..4856e1b9 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,6 +7,8 @@ Python-Arango Welcome to the documentation for **python-arango**, a Python driver for ArangoDB_. +If you're interested in using asyncio, please check python-arango-async_ driver. + Requirements ============= @@ -96,3 +98,4 @@ Development specs .. _ArangoDB: https://www.arangodb.com +.. _python-arango-async: https://python-arango-async.readthedocs.io From c6e923b0fda3cf797b83197e2965fd385e311971 Mon Sep 17 00:00:00 2001 From: Anthony Mahanna <43019056+aMahanna@users.noreply.github.com> Date: Mon, 31 Mar 2025 10:01:30 -0400 Subject: [PATCH 3/4] new: `raise_on_document_error` for `insert_many` & `delete_many` (#369) --- arango/collection.py | 26 ++++++++++++++++++++++++-- tests/test_document.py | 12 ++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/arango/collection.py b/arango/collection.py index 7f79fb3f..a996dc5c 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -1752,6 +1752,7 @@ def insert_many( merge: Optional[bool] = None, refill_index_caches: Optional[bool] = None, version_attribute: Optional[str] = None, + raise_on_document_error: bool = False, ) -> Result[Union[bool, List[Union[Json, ArangoServerError]]]]: """Insert multiple documents. @@ -1761,7 +1762,8 @@ def insert_many( returned as an object in the result list. It is up to you to inspect the list to determine which documents were inserted successfully (returns document metadata) and which were not - (returns exception object). + (returns exception object). Alternatively, you can rely on + setting **raise_on_document_error** to True (defaults to False). :param documents: List of new documents to insert. If they contain the "_key" or "_id" fields, the values are used as the keys of the new @@ -1801,6 +1803,11 @@ def insert_many( :param version_attribute: support for simple external versioning to document operations. :type version_attribute: str + :param raise_on_document_error: Whether to raise if a DocumentRevisionError + or a DocumentInsertError is encountered on an individual document, + as opposed to returning the error as an object in the result list. + Defaults to False. + :type raise_on_document_error: bool :return: List of document metadata (e.g. document keys, revisions) and any exception, or True if parameter **silent** was set to True. :rtype: [dict | ArangoServerError] | bool @@ -1853,7 +1860,12 @@ def response_handler( results.append(body) else: sub_resp = self._conn.prep_bulk_err_response(resp, body) - results.append(DocumentInsertError(sub_resp, request)) + error = DocumentInsertError(sub_resp, request) + + if raise_on_document_error: + raise error + + results.append(error) return results @@ -2228,6 +2240,7 @@ def delete_many( sync: Optional[bool] = None, silent: bool = False, refill_index_caches: Optional[bool] = None, + raise_on_document_error: bool = False, ) -> Result[Union[bool, List[Union[Json, ArangoServerError]]]]: """Delete multiple documents. @@ -2256,6 +2269,11 @@ def delete_many( index caches if document operations affect the edge index or cache-enabled persistent indexes. :type refill_index_caches: bool | None + :param raise_on_document_error: Whether to raise if a DocumentRevisionError + or a DocumentDeleteError is encountered on an individual document, + as opposed to returning the error as an object in the result list. + Defaults to False. + :type raise_on_document_error: bool :return: List of document metadata (e.g. document keys, revisions) and any exceptions, or True if parameter **silent** was set to True. :rtype: [dict | ArangoServerError] | bool @@ -2307,6 +2325,10 @@ def response_handler( error = DocumentRevisionError(sub_resp, request) else: error = DocumentDeleteError(sub_resp, request) + + if raise_on_document_error: + raise error + results.append(error) return results diff --git a/tests/test_document.py b/tests/test_document.py index 7cb0a435..0dbca038 100644 --- a/tests/test_document.py +++ b/tests/test_document.py @@ -239,6 +239,10 @@ def test_document_insert_many(col, bad_col, docs): assert isinstance(result["old"], dict) assert isinstance(result["_old_rev"], str) + # Test insert_many with raise_on_document_error set to True + with assert_raises(DocumentInsertError) as err: + col.insert_many(docs, raise_on_document_error=True) + # Test get with bad database with assert_raises(DocumentInsertError) as err: bad_col.insert_many(docs) @@ -1092,6 +1096,10 @@ def test_document_delete_many(col, bad_col, docs): assert "[HTTP 202][ERR 1200]" in error.message assert len(col) == 6 + # Test delete_many with raise_on_document_error set to True + with assert_raises(DocumentRevisionError) as err: + col.delete_many(docs, raise_on_document_error=True) + # Test delete_many (documents) with missing documents empty_collection(col) results = col.delete_many( @@ -1109,6 +1117,10 @@ def test_document_delete_many(col, bad_col, docs): assert "[HTTP 202][ERR 1202]" in error.message assert len(col) == 0 + # Test delete_many with raise_on_document_error set to True + with assert_raises(DocumentDeleteError) as err: + col.delete_many(docs, raise_on_document_error=True) + # Test delete_many with bad database with assert_raises(DocumentDeleteError) as err: bad_col.delete_many(docs) From 6aa9986d89be9bbe4a245e925c981c22fe273fcc Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 12 May 2025 20:12:38 +0000 Subject: [PATCH 4/4] Wait for sync upon graph creation (#370) * Wait for sync upon graph creation * Using graph properties instead of list search --- arango/database.py | 25 ++++++++++++++++++++----- tests/test_graph.py | 2 +- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/arango/database.py b/arango/database.py index 17d7a124..8a145910 100644 --- a/arango/database.py +++ b/arango/database.py @@ -17,6 +17,7 @@ from arango.cluster import Cluster from arango.collection import StandardCollection from arango.connection import Connection +from arango.errno import HTTP_NOT_FOUND from arango.exceptions import ( AnalyzerCreateError, AnalyzerDeleteError, @@ -1644,12 +1645,14 @@ def has_graph(self, name: str) -> Result[bool]: :return: True if graph exists, False otherwise. :rtype: bool """ - request = Request(method="get", endpoint="/_api/gharial") + request = Request(method="get", endpoint=f"/_api/gharial/{name}") def response_handler(resp: Response) -> bool: - if not resp.is_success: - raise GraphListError(resp, request) - return any(name == graph["_key"] for graph in resp.body["graphs"]) + if resp.is_success: + return True + if resp.status_code == HTTP_NOT_FOUND: + return False + raise GraphListError(resp, request) return self._execute(request, response_handler) @@ -1699,6 +1702,7 @@ def create_graph( replication_factor: Optional[int] = None, write_concern: Optional[int] = None, satellite_collections: Optional[Sequence[str]] = None, + sync: Optional[bool] = None, ) -> Result[Graph]: """Create a new graph. @@ -1753,6 +1757,8 @@ def create_graph( element must be a string and a valid collection name. The collection type cannot be modified later. :type satellite_collections: [str] | None + :param sync: Wait until everything is synced to disk. + :type sync: bool | None :return: Graph API wrapper. :rtype: arango.graph.Graph :raise arango.exceptions.GraphCreateError: If create fails. @@ -1796,7 +1802,16 @@ def create_graph( if satellite_collections is not None: # pragma: no cover data["options"]["satellites"] = satellite_collections - request = Request(method="post", endpoint="/_api/gharial", data=data) + params: Params = {} + if sync is not None: + params["waitForSync"] = sync + + request = Request( + method="post", + endpoint="/_api/gharial", + data=data, + params=params, + ) def response_handler(resp: Response) -> Graph: if resp.is_success: diff --git a/tests/test_graph.py b/tests/test_graph.py index 4d2588cb..fe63455d 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -51,7 +51,7 @@ def test_graph_properties(graph, bad_graph, db): bad_graph.properties() new_graph_name = generate_graph_name() - new_graph = db.create_graph(new_graph_name) + new_graph = db.create_graph(new_graph_name, sync=True) properties = new_graph.properties() assert properties["id"] == f"_graphs/{new_graph_name}" assert properties["name"] == new_graph_name