diff --git a/README.md b/README.md index f76c608..d4b995f 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/collection.py b/arango/collection.py index e2dfcd2..a996dc5 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: @@ -1736,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. @@ -1745,15 +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). - - .. 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. + (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 @@ -1793,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 @@ -1845,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 @@ -1876,14 +1896,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 +2007,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 +2089,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 +2183,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. @@ -2252,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. @@ -2263,14 +2252,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] @@ -2288,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 @@ -2339,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 @@ -2354,14 +2344,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 +2410,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 +2731,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/database.py b/arango/database.py index 17d7a12..8a14591 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/arango/request.py b/arango/request.py index 41a0ac6..abb2b0d 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/arango/utils.py b/arango/utils.py index 89a0eca..822bc73 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/docs/index.rst b/docs/index.rst index 06955e0..4856e1b 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 diff --git a/tests/test_collection.py b/tests/test_collection.py index 7ab7280..c11a654 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: diff --git a/tests/test_document.py b/tests/test_document.py index 7cb0a43..0dbca03 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) diff --git a/tests/test_graph.py b/tests/test_graph.py index 4d2588c..fe63455 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