From 173f2b367d68b5f5c7edf793a09fe00334a8791f Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Fri, 28 Mar 2025 10:34:12 +0530 Subject: [PATCH 1/6] 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/6] 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/6] 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/6] 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 From 863d4cd7573a0a6406e3d1a6de969314f05a502b Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Sun, 1 Jun 2025 10:49:39 +0000 Subject: [PATCH 5/6] Updated graphs link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d4b995fd..d90d8458 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ cursor = db.aql.execute("FOR doc IN students RETURN doc") student_names = [document["name"] for document in cursor] ``` -Another example with [graphs](https://www.arangodb.com/docs/stable/graphs.html): +Another example with [graphs](https://docs.arangodb.com/stable/graphs/): ```python from arango import ArangoClient From 3769989166098b05849223bdc3eb90d934cb12ba Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Wed, 4 Jun 2025 06:14:06 +0300 Subject: [PATCH 6/6] API Updates (#372) * Updated API * Bumping up driver version * Updating docs --- arango/collection.py | 111 ++++++++------------------- arango/exceptions.py | 4 + arango/graph.py | 179 ++++++++++++++++++++++++------------------- arango/request.py | 2 +- docs/graph.rst | 3 +- tests/test_graph.py | 20 ++--- 6 files changed, 149 insertions(+), 170 deletions(-) diff --git a/arango/collection.py b/arango/collection.py index a996dc5c..a48bfd2a 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -3008,9 +3008,8 @@ def insert( self, vertex: Json, sync: Optional[bool] = None, - silent: bool = False, return_new: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Insert a new vertex document. :param vertex: New vertex document to insert. If it has "_key" or "_id" @@ -3019,20 +3018,16 @@ def insert( :type vertex: dict :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool :param return_new: Include body of the new document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_new: bool - :return: Document metadata (e.g. document key, revision), or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentInsertError: If insert fails. """ vertex = self._ensure_key_from_id(vertex) - params: Params = {"silent": silent, "returnNew": return_new} + params: Params = {"returnNew": return_new} if sync is not None: params["waitForSync"] = sync @@ -3044,11 +3039,9 @@ def insert( write=self.name, ) - def response_handler(resp: Response) -> Union[bool, Json]: + def response_handler(resp: Response) -> Json: if not resp.is_success: raise DocumentInsertError(resp, request) - if silent: - return True return format_vertex(resp.body) return self._execute(request, response_handler) @@ -3059,10 +3052,9 @@ def update( check_rev: bool = True, keep_none: bool = True, sync: Optional[bool] = None, - silent: bool = False, return_old: bool = False, return_new: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Update a vertex document. :param vertex: Partial or full vertex document with updated values. It @@ -3076,18 +3068,14 @@ def update( :type keep_none: bool | None :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool :param return_old: Include body of the old document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_old: bool :param return_new: Include body of the new document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_new: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentUpdateError: If update fails. :raise arango.exceptions.DocumentRevisionError: If revisions mismatch. """ @@ -3096,7 +3084,6 @@ def update( params: Params = { "keepNull": keep_none, "overwrite": not check_rev, - "silent": silent, "returnNew": return_new, "returnOld": return_old, } @@ -3112,13 +3099,11 @@ def update( write=self.name, ) - def response_handler(resp: Response) -> Union[bool, Json]: + def response_handler(resp: Response) -> Json: if resp.status_code == 412: # pragma: no cover raise DocumentRevisionError(resp, request) elif not resp.is_success: raise DocumentUpdateError(resp, request) - if silent is True: - return True return format_vertex(resp.body) return self._execute(request, response_handler) @@ -3128,10 +3113,9 @@ def replace( vertex: Json, check_rev: bool = True, sync: Optional[bool] = None, - silent: bool = False, return_old: bool = False, return_new: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Replace a vertex document. :param vertex: New vertex document to replace the old one with. It must @@ -3142,25 +3126,20 @@ def replace( :type check_rev: bool :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool :param return_old: Include body of the old document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_old: bool :param return_new: Include body of the new document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_new: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentReplaceError: If replace fails. :raise arango.exceptions.DocumentRevisionError: If revisions mismatch. """ vertex_id, headers = self._prep_from_body(vertex, check_rev) params: Params = { - "silent": silent, "returnNew": return_new, "returnOld": return_old, } @@ -3176,13 +3155,11 @@ def replace( write=self.name, ) - def response_handler(resp: Response) -> Union[bool, Json]: + def response_handler(resp: Response) -> Json: if resp.status_code == 412: # pragma: no cover raise DocumentRevisionError(resp, request) elif not resp.is_success: raise DocumentReplaceError(resp, request) - if silent is True: - return True return format_vertex(resp.body) return self._execute(request, response_handler) @@ -3326,9 +3303,8 @@ def insert( self, edge: Json, sync: Optional[bool] = None, - silent: bool = False, return_new: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Insert a new edge document. :param edge: New edge document to insert. It must contain "_from" and @@ -3338,20 +3314,16 @@ def insert( :type edge: dict :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool :param return_new: Include body of the new document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_new: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentInsertError: If insert fails. """ edge = self._ensure_key_from_id(edge) - params: Params = {"silent": silent, "returnNew": return_new} + params: Params = {"returnNew": return_new} if sync is not None: params["waitForSync"] = sync @@ -3363,11 +3335,9 @@ def insert( write=self.name, ) - def response_handler(resp: Response) -> Union[bool, Json]: + def response_handler(resp: Response) -> Json: if not resp.is_success: raise DocumentInsertError(resp, request) - if silent: - return True return format_edge(resp.body) return self._execute(request, response_handler) @@ -3378,10 +3348,9 @@ def update( check_rev: bool = True, keep_none: bool = True, sync: Optional[bool] = None, - silent: bool = False, return_old: bool = False, return_new: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Update an edge document. :param edge: Partial or full edge document with updated values. It must @@ -3395,18 +3364,14 @@ def update( :type keep_none: bool | None :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool :param return_old: Include body of the old document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_old: bool :param return_new: Include body of the new document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_new: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentUpdateError: If update fails. :raise arango.exceptions.DocumentRevisionError: If revisions mismatch. """ @@ -3415,7 +3380,6 @@ def update( params: Params = { "keepNull": keep_none, "overwrite": not check_rev, - "silent": silent, "returnNew": return_new, "returnOld": return_old, } @@ -3431,13 +3395,11 @@ def update( write=self.name, ) - def response_handler(resp: Response) -> Union[bool, Json]: + def response_handler(resp: Response) -> Json: if resp.status_code == 412: # pragma: no cover raise DocumentRevisionError(resp, request) if not resp.is_success: raise DocumentUpdateError(resp, request) - if silent is True: - return True return format_edge(resp.body) return self._execute(request, response_handler) @@ -3447,10 +3409,9 @@ def replace( edge: Json, check_rev: bool = True, sync: Optional[bool] = None, - silent: bool = False, return_old: bool = False, return_new: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Replace an edge document. :param edge: New edge document to replace the old one with. It must @@ -3462,25 +3423,20 @@ def replace( :type check_rev: bool :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool :param return_old: Include body of the old document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_old: bool :param return_new: Include body of the new document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_new: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentReplaceError: If replace fails. :raise arango.exceptions.DocumentRevisionError: If revisions mismatch. """ edge_id, headers = self._prep_from_body(edge, check_rev) params: Params = { - "silent": silent, "returnNew": return_new, "returnOld": return_old, } @@ -3496,13 +3452,11 @@ def replace( write=self.name, ) - def response_handler(resp: Response) -> Union[bool, Json]: + def response_handler(resp: Response) -> Json: if resp.status_code == 412: # pragma: no cover raise DocumentRevisionError(resp, request) if not resp.is_success: raise DocumentReplaceError(resp, request) - if silent is True: - return True return format_edge(resp.body) return self._execute(request, response_handler) @@ -3575,9 +3529,8 @@ def link( to_vertex: Union[str, Json], data: Optional[Json] = None, sync: Optional[bool] = None, - silent: bool = False, return_new: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Insert a new edge document linking the given vertices. :param from_vertex: "From" vertex document ID or body with "_id" field. @@ -3590,21 +3543,17 @@ def link( :type data: dict | None :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool :param return_new: Include body of the new document in the returned metadata. Ignored if parameter **silent** is set to True. :type return_new: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentInsertError: If insert fails. """ edge = {"_from": get_doc_id(from_vertex), "_to": get_doc_id(to_vertex)} if data is not None: edge.update(self._ensure_key_from_id(data)) - return self.insert(edge, sync=sync, silent=silent, return_new=return_new) + return self.insert(edge, sync=sync, return_new=return_new) def edges( self, diff --git a/arango/exceptions.py b/arango/exceptions.py index 29bcdc17..cdea90c5 100644 --- a/arango/exceptions.py +++ b/arango/exceptions.py @@ -543,6 +543,10 @@ class VertexCollectionDeleteError(ArangoServerError): """Failed to delete vertex collection.""" +class EdgeCollectionListError(ArangoServerError): + """Failed to retrieve edge collections.""" + + class EdgeDefinitionListError(ArangoServerError): """Failed to retrieve edge definitions.""" diff --git a/arango/graph.py b/arango/graph.py index 3279129f..589e9c44 100644 --- a/arango/graph.py +++ b/arango/graph.py @@ -7,6 +7,7 @@ from arango.collection import EdgeCollection, VertexCollection from arango.connection import Connection from arango.exceptions import ( + EdgeCollectionListError, EdgeDefinitionCreateError, EdgeDefinitionDeleteError, EdgeDefinitionListError, @@ -136,19 +137,28 @@ def vertex_collection(self, name: str) -> VertexCollection: """ return VertexCollection(self._conn, self._executor, self._name, name) - def create_vertex_collection(self, name: str) -> Result[VertexCollection]: + def create_vertex_collection( + self, + name: str, + options: Optional[Json] = None, + ) -> Result[VertexCollection]: """Create a vertex collection in the graph. :param name: Vertex collection name. :type name: str + :param options: Additional options for creating vertex collections. + :type options: dict | None :return: Vertex collection API wrapper. :rtype: arango.collection.VertexCollection :raise arango.exceptions.VertexCollectionCreateError: If create fails. """ + data: Json = {"collection": name} + if options is not None: + data["options"] = options request = Request( method="post", endpoint=f"/_api/gharial/{self._name}/vertex", - data={"collection": name}, + data=data, ) def response_handler(resp: Response) -> VertexCollection: @@ -259,6 +269,7 @@ def create_edge_definition( edge_collection: str, from_vertex_collections: Sequence[str], to_vertex_collections: Sequence[str], + options: Optional[Json] = None, ) -> Result[EdgeCollection]: """Create a new edge definition. @@ -279,18 +290,24 @@ def create_edge_definition( :type from_vertex_collections: [str] :param to_vertex_collections: Names of "to" vertex collections. :type to_vertex_collections: [str] + :param options: Additional options for creating edge definitions. + :type options: dict | None :return: Edge collection API wrapper. :rtype: arango.collection.EdgeCollection :raise arango.exceptions.EdgeDefinitionCreateError: If create fails. """ + data: Json = { + "collection": edge_collection, + "from": from_vertex_collections, + "to": to_vertex_collections, + } + if options is not None: + data["options"] = options + request = Request( method="post", endpoint=f"/_api/gharial/{self._name}/edge", - data={ - "collection": edge_collection, - "from": from_vertex_collections, - "to": to_vertex_collections, - }, + data=data, ) def response_handler(resp: Response) -> EdgeCollection: @@ -300,11 +317,32 @@ def response_handler(resp: Response) -> EdgeCollection: return self._execute(request, response_handler) + def edge_collections(self) -> Result[List[str]]: + """Get the names of all edge collections in the graph. + + :return: Edge collections in the graph. + :rtype: list + :raise: arango.exceptions.EdgeCollectionListError: If retrieval fails. + """ + request = Request( + method="get", + endpoint=f"/_api/gharial/{self._name}/edge", + ) + + def response_handler(resp: Response) -> List[str]: + if not resp.is_success: + raise EdgeCollectionListError(resp, request) + return list(sorted(resp.body["collections"])) + + return self._execute(request, response_handler) + def replace_edge_definition( self, edge_collection: str, from_vertex_collections: Sequence[str], to_vertex_collections: Sequence[str], + sync: Optional[bool] = None, + purge: bool = False, ) -> Result[EdgeCollection]: """Replace an edge definition. @@ -314,18 +352,28 @@ def replace_edge_definition( :type from_vertex_collections: [str] :param to_vertex_collections: Names of "to" vertex collections. :type to_vertex_collections: [str] + :param sync: Block until operation is synchronized to disk. + :type sync: bool | None + :param purge: Drop the edge collection in addition to removing it from + the graph. The collection is only dropped if it is not used in other graphs. + :type purge: bool :return: Edge collection API wrapper. :rtype: arango.collection.EdgeCollection :raise arango.exceptions.EdgeDefinitionReplaceError: If replace fails. """ + data: Json = { + "collection": edge_collection, + "from": from_vertex_collections, + "to": to_vertex_collections, + "purge": purge, + } + if sync is not None: + data["waitForSync"] = sync + request = Request( method="put", endpoint=f"/_api/gharial/{self._name}/edge/{edge_collection}", - data={ - "collection": edge_collection, - "from": from_vertex_collections, - "to": to_vertex_collections, - }, + data=data, ) def response_handler(resp: Response) -> EdgeCollection: @@ -335,7 +383,12 @@ def response_handler(resp: Response) -> EdgeCollection: return self._execute(request, response_handler) - def delete_edge_definition(self, name: str, purge: bool = False) -> Result[bool]: + def delete_edge_definition( + self, + name: str, + purge: bool = False, + sync: Optional[bool] = None, + ) -> Result[bool]: """Delete an edge definition from the graph. :param name: Edge collection name. @@ -344,14 +397,20 @@ def delete_edge_definition(self, name: str, purge: bool = False) -> Result[bool] from the graph but the edge collection is also deleted completely from the database. :type purge: bool + :sync: Block until operation is synchronized to disk. + :type sync: bool | None :return: True if edge definition was deleted successfully. :rtype: bool :raise arango.exceptions.EdgeDefinitionDeleteError: If delete fails. """ + params: Json = {"dropCollections": purge} + if sync is not None: + params["waitForSync"] = sync + request = Request( method="delete", endpoint=f"/_api/gharial/{self._name}/edge/{name}", - params={"dropCollections": purge}, + params=params, ) def response_handler(resp: Response) -> bool: @@ -549,8 +608,7 @@ def insert_vertex( collection: str, vertex: Json, sync: Optional[bool] = None, - silent: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Insert a new vertex document. :param collection: Vertex collection name. @@ -561,15 +619,11 @@ def insert_vertex( :type vertex: dict :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentInsertError: If insert fails. """ - return self.vertex_collection(collection).insert(vertex, sync, silent) + return self.vertex_collection(collection).insert(vertex, sync) def update_vertex( self, @@ -577,8 +631,7 @@ def update_vertex( check_rev: bool = True, keep_none: bool = True, sync: Optional[bool] = None, - silent: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Update a vertex document. :param vertex: Partial or full vertex document with updated values. It @@ -592,12 +645,8 @@ def update_vertex( :type keep_none: bool :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentUpdateError: If update fails. :raise arango.exceptions.DocumentRevisionError: If revisions mismatch. """ @@ -606,7 +655,6 @@ def update_vertex( check_rev=check_rev, keep_none=keep_none, sync=sync, - silent=silent, ) def replace_vertex( @@ -614,8 +662,7 @@ def replace_vertex( vertex: Json, check_rev: bool = True, sync: Optional[bool] = None, - silent: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Replace a vertex document. :param vertex: New vertex document to replace the old one with. It must @@ -626,17 +673,15 @@ def replace_vertex( :type check_rev: bool :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentReplaceError: If replace fails. :raise arango.exceptions.DocumentRevisionError: If revisions mismatch. """ return self._get_col_by_vertex(vertex).replace( - vertex=vertex, check_rev=check_rev, sync=sync, silent=silent + vertex=vertex, + check_rev=check_rev, + sync=sync, ) def delete_vertex( @@ -727,8 +772,7 @@ def insert_edge( collection: str, edge: Json, sync: Optional[bool] = None, - silent: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Insert a new edge document. :param collection: Edge collection name. @@ -740,15 +784,11 @@ def insert_edge( :type edge: dict :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentInsertError: If insert fails. """ - return self.edge_collection(collection).insert(edge, sync, silent) + return self.edge_collection(collection).insert(edge, sync) def update_edge( self, @@ -756,8 +796,7 @@ def update_edge( check_rev: bool = True, keep_none: bool = True, sync: Optional[bool] = None, - silent: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Update an edge document. :param edge: Partial or full edge document with updated values. It must @@ -771,12 +810,8 @@ def update_edge( :type keep_none: bool | None :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentUpdateError: If update fails. :raise arango.exceptions.DocumentRevisionError: If revisions mismatch. """ @@ -785,7 +820,6 @@ def update_edge( check_rev=check_rev, keep_none=keep_none, sync=sync, - silent=silent, ) def replace_edge( @@ -793,8 +827,7 @@ def replace_edge( edge: Json, check_rev: bool = True, sync: Optional[bool] = None, - silent: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Replace an edge document. :param edge: New edge document to replace the old one with. It must @@ -806,17 +839,15 @@ def replace_edge( :type check_rev: bool :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentReplaceError: If replace fails. :raise arango.exceptions.DocumentRevisionError: If revisions mismatch. """ return self._get_col_by_edge(edge).replace( - edge=edge, check_rev=check_rev, sync=sync, silent=silent + edge=edge, + check_rev=check_rev, + sync=sync, ) def delete_edge( @@ -865,8 +896,7 @@ def link( to_vertex: Union[str, Json], data: Optional[Json] = None, sync: Optional[bool] = None, - silent: bool = False, - ) -> Result[Union[bool, Json]]: + ) -> Result[Json]: """Insert a new edge document linking the given vertices. :param collection: Edge collection name. @@ -881,12 +911,8 @@ def link( :type data: dict :param sync: Block until operation is synchronized to disk. :type sync: bool | None - :param silent: If set to True, no document metadata is returned. This - can be used to save resources. - :type silent: bool - :return: Document metadata (e.g. document key, revision) or True if - parameter **silent** was set to True. - :rtype: bool | dict + :return: Document metadata (e.g. document key, revision). + :rtype: dict :raise arango.exceptions.DocumentInsertError: If insert fails. """ return self.edge_collection(collection).link( @@ -894,7 +920,6 @@ def link( to_vertex=to_vertex, data=data, sync=sync, - silent=silent, ) def edges( diff --git a/arango/request.py b/arango/request.py index abb2b0db..11ff91c6 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.7" + driver_version = "8.2.0" driver_header = "python-arango/" + driver_version + " (" + flags + ")" normalized_headers: Headers = { "charset": "utf-8", diff --git a/docs/graph.rst b/docs/graph.rst index 0b37154f..0645d5b6 100644 --- a/docs/graph.rst +++ b/docs/graph.rst @@ -83,6 +83,7 @@ Here is an example showing how edge definitions are managed: # Create an edge definition named "teach". This creates any missing # collections and returns an API wrapper for "teach" edge collection. + # At first, create a wrong teachers->teachers mapping intentionally. if not school.has_edge_definition('teach'): teach = school.create_edge_definition( edge_collection='teach', @@ -93,7 +94,7 @@ Here is an example showing how edge definitions are managed: # List edge definitions. school.edge_definitions() - # Replace the edge definition. + # Replace with the correct edge definition. school.replace_edge_definition( edge_collection='teach', from_vertex_collections=['teachers'], diff --git a/tests/test_graph.py b/tests/test_graph.py index fe63455d..fd3242fe 100644 --- a/tests/test_graph.py +++ b/tests/test_graph.py @@ -334,6 +334,7 @@ def test_create_graph_with_edge_definition(db): ) assert edge_definition in new_graph.edge_definitions() assert ovcol_name in new_graph.vertex_collections() + assert edge_definition["edge_collection"] in new_graph.edge_collections() def test_vertex_management(fvcol, bad_fvcol, fvdocs): @@ -394,7 +395,7 @@ def test_vertex_management(fvcol, bad_fvcol, fvdocs): key = vertex["_key"] # Test insert third valid vertex with silent set to True - assert fvcol.insert(vertex, silent=True) is True + assert isinstance(fvcol.insert(vertex), dict) assert len(fvcol) == 3 assert fvcol[key]["val"] == vertex["val"] @@ -444,7 +445,7 @@ def test_vertex_management(fvcol, bad_fvcol, fvdocs): # Test update vertex with silent set to True assert "bar" not in fvcol[vertex] - assert fvcol.update({"_key": key, "bar": 200}, silent=True) is True + assert isinstance(fvcol.update({"_key": key, "bar": 200}), dict) assert fvcol[vertex]["bar"] == 200 assert fvcol[vertex]["_rev"] != old_rev old_rev = fvcol[key]["_rev"] @@ -519,7 +520,7 @@ def test_vertex_management(fvcol, bad_fvcol, fvdocs): assert "vertex" in result # Test replace vertex with silent set to True - assert fvcol.replace({"_key": key, "bar": 200}, silent=True) is True + assert isinstance(fvcol.replace({"_key": key, "bar": 200}), dict) assert "foo" not in fvcol[key] assert "baz" not in fvcol[vertex] assert fvcol[vertex]["bar"] == 200 @@ -698,7 +699,7 @@ def test_edge_management(ecol, bad_ecol, edocs, fvcol, fvdocs, tvcol, tvdocs): key = edge["_key"] # Test insert second valid edge with silent set to True - assert ecol.insert(edge, sync=True, silent=True) is True + assert isinstance(ecol.insert(edge, sync=True), dict) assert edge in ecol and key in ecol assert len(ecol) == 2 assert ecol[key]["_from"] == edge["_from"] @@ -714,15 +715,14 @@ def test_edge_management(ecol, bad_ecol, edocs, fvcol, fvdocs, tvcol, tvdocs): # Test insert fourth valid edge using link method from_vertex = fvcol.get(fvdocs[2]) to_vertex = tvcol.get(tvdocs[0]) - assert ( + assert isinstance( ecol.link( from_vertex["_id"], to_vertex["_id"], {"_id": ecol.name + "/foo"}, sync=True, - silent=True, - ) - is True + ), + dict, ) assert "foo" in ecol assert len(ecol) == 4 @@ -816,7 +816,7 @@ def test_edge_management(ecol, bad_ecol, edocs, fvcol, fvdocs, tvcol, tvdocs): old_rev = result["_rev"] # Test update edge with silent option - assert ecol.update({"_key": key, "bar": 600}, silent=True) is True + assert isinstance(ecol.update({"_key": key, "bar": 600}), dict) assert ecol[key]["foo"] == 200 assert ecol[key]["bar"] == 600 assert ecol[key]["_rev"] != old_rev @@ -852,7 +852,7 @@ def test_edge_management(ecol, bad_ecol, edocs, fvcol, fvdocs, tvcol, tvdocs): # Test replace edge with silent set to True edge["bar"] = 200 - assert ecol.replace(edge, silent=True) is True + assert isinstance(ecol.replace(edge), dict) assert ecol[key]["foo"] == 100 assert ecol[key]["bar"] == 200 assert ecol[key]["_rev"] != old_rev