From 173f2b367d68b5f5c7edf793a09fe00334a8791f Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Fri, 28 Mar 2025 10:34:12 +0530 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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 05/11] 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 06/11] 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 From 7fc1037dcfbe4b65115f7ebd396f1f3dc8f6cfac Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Wed, 9 Jul 2025 04:46:46 +0300 Subject: [PATCH 07/11] Skip _db prefix on /_open/auth (#374) * Updating example version * No longer use _db/... prefix on /_open/auth * Fixing test dependant on version --- arango/connection.py | 20 ++++++++++++++++---- starter.sh | 2 +- tests/test_database.py | 11 ++++++++--- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/arango/connection.py b/arango/connection.py index 8de2643a..9384aef1 100644 --- a/arango/connection.py +++ b/arango/connection.py @@ -125,7 +125,11 @@ def prep_response(self, resp: Response, deserialize: bool = True) -> Response: return resp def process_request( - self, host_index: int, request: Request, auth: Optional[Tuple[str, str]] = None + self, + host_index: int, + request: Request, + auth: Optional[Tuple[str, str]] = None, + skip_db_prefix: bool = False, ) -> Response: """Execute a request until a valid response has been returned. @@ -133,6 +137,10 @@ def process_request( :type host_index: int :param request: HTTP request. :type request: arango.request.Request + :param auth: HTTP basic authentication tuple (username, password). + :type auth: tuple[str, str] | None + :param skip_db_prefix: Skip the database prefix in the URL. + :type skip_db_prefix: bool :return: HTTP response. :rtype: arango.response.Response """ @@ -152,11 +160,16 @@ def process_request( request.headers["accept-encoding"] = self._response_compression while tries < self._host_resolver.max_tries: + if skip_db_prefix: + url = self._hosts[host_index] + request.endpoint + else: + url = self._url_prefixes[host_index] + request.endpoint + try: resp = self._http.send_request( session=self._sessions[host_index], method=request.method, - url=self._url_prefixes[host_index] + request.endpoint, + url=url, params=request.params, data=data, headers=request.headers, @@ -165,7 +178,6 @@ def process_request( return self.prep_response(resp, request.deserialize) except ConnectionError: - url = self._url_prefixes[host_index] + request.endpoint logging.debug(f"ConnectionError: {url}") if len(indexes_to_filter) == self._host_resolver.host_count - 1: @@ -425,7 +437,7 @@ def refresh_token(self) -> None: host_index = self._host_resolver.get_host_index() - resp = self.process_request(host_index, request) + resp = self.process_request(host_index, request, skip_db_prefix=True) if not resp.is_success: raise JWTAuthError(resp, request) diff --git a/starter.sh b/starter.sh index b4e39f24..a5126f54 100755 --- a/starter.sh +++ b/starter.sh @@ -6,7 +6,7 @@ # Usage: # ./starter.sh [single|cluster] [community|enterprise] [version] # Example: -# ./starter.sh cluster enterprise 3.12.1 +# ./starter.sh cluster enterprise 3.12.5 setup="${1:-single}" license="${2:-community}" diff --git a/tests/test_database.py b/tests/test_database.py index 0b1d9752..d6595a4d 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -439,18 +439,23 @@ def test_database_utf8(sys_db, special_db_names): assert sys_db.delete_database(name) -def test_license(sys_db, enterprise): +def test_license(sys_db, enterprise, db_version): license = sys_db.license() assert isinstance(license, dict) - if enterprise: - assert set(license.keys()) == { + if db_version >= version.parse("3.12.5"): + expected_keys = {"diskUsage", "upgrading"} + else: + expected_keys = { "upgrading", "features", "license", "version", "status", } + + if enterprise: + assert set(license.keys()) == expected_keys else: assert license == {"license": "none"} with pytest.raises(ServerLicenseSetError): From 3dfbe2584bfb5f5b512e87e83a7c19b6b0d4af58 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Wed, 9 Jul 2025 01:48:16 +0000 Subject: [PATCH 08/11] Bump driver version --- arango/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arango/request.py b/arango/request.py index 11ff91c6..8735afe6 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.2.0" + driver_version = "8.2.1" driver_header = "python-arango/" + driver_version + " (" + flags + ")" normalized_headers: Headers = { "charset": "utf-8", From 0366a1e2a1d22269db887a4ff4d55959a67a44a1 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Tue, 15 Jul 2025 04:17:13 +0300 Subject: [PATCH 09/11] Adding support for /key-generators (#375) --- arango/database.py | 18 ++++++++++++++++++ arango/exceptions.py | 4 ++++ tests/test_database.py | 7 +++++++ 3 files changed, 29 insertions(+) diff --git a/arango/database.py b/arango/database.py index 8a145910..20e771d2 100644 --- a/arango/database.py +++ b/arango/database.py @@ -27,6 +27,7 @@ AsyncJobListError, CollectionCreateError, CollectionDeleteError, + CollectionKeyGeneratorsError, CollectionListError, DatabaseCompactError, DatabaseCreateError, @@ -1623,6 +1624,23 @@ def response_handler(resp: Response) -> bool: return self._execute(request, response_handler) + def key_generators(self) -> Result[List[str]]: + """Returns the available key generators for collections. + + :return: List of available key generators. + :rtype: [str] + :raise arango.exceptions.CollectionKeyGeneratorsError: If retrieval fails. + """ # noqa: E501 + request = Request(method="get", endpoint="/_api/key-generators") + + def response_handler(resp: Response) -> List[str]: + if not resp.is_success: + raise CollectionKeyGeneratorsError(resp, request) + result: List[str] = resp.body["keyGenerators"] + return result + + return self._execute(request, response_handler) + #################### # Graph Management # #################### diff --git a/arango/exceptions.py b/arango/exceptions.py index cdea90c5..789468ed 100644 --- a/arango/exceptions.py +++ b/arango/exceptions.py @@ -298,6 +298,10 @@ class CollectionTruncateError(ArangoServerError): """Failed to truncate collection.""" +class CollectionKeyGeneratorsError(ArangoServerError): + """Failed to retrieve key generators.""" + + class CollectionLoadError(ArangoServerError): """Failed to load collection.""" diff --git a/tests/test_database.py b/tests/test_database.py index d6595a4d..014f0235 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -13,6 +13,7 @@ USE_SYSTEM_DATABASE, ) from arango.exceptions import ( + CollectionKeyGeneratorsError, DatabaseCompactError, DatabaseCreateError, DatabaseDeleteError, @@ -348,6 +349,12 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret, db_v result = db_superuser.compact() assert result == {} + if db_version >= version.parse("3.12.0"): + key_generators = db.key_generators() + assert isinstance(key_generators, list) + with pytest.raises(CollectionKeyGeneratorsError): + bad_db.key_generators() + def test_database_management(db, sys_db, bad_db): # Test list databases From 08a2e54a69d7b0603bed7552fe0dc715d8632ef0 Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 4 Aug 2025 13:15:43 +0800 Subject: [PATCH 10/11] Deprecate load/unload methods (#376) * Deprecating load method * Deprecating unload method * Minor comment correction --- arango/backup.py | 2 +- arango/collection.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/arango/backup.py b/arango/backup.py index c06e4e15..d82e8e2c 100644 --- a/arango/backup.py +++ b/arango/backup.py @@ -33,7 +33,7 @@ def get(self, backup_id: Optional[str] = None) -> Result[Json]: :type backup_id: str :return: Backup details. :rtype: dict - :raise arango.exceptions.BackupGetError: If delete fails. + :raise arango.exceptions.BackupGetError: If the operation fails. """ request = Request( method="post", diff --git a/arango/collection.py b/arango/collection.py index a48bfd2a..2b13884a 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -537,10 +537,18 @@ def response_handler(resp: Response) -> Json: def load(self) -> Result[bool]: """Load the collection into memory. + .. note:: + The load function is deprecated from version 3.8.0 onwards and is a + no-op from version 3.9.0 onwards. It should no longer be used, as it + may be removed in a future version of ArangoDB. + :return: True if collection was loaded successfully. :rtype: bool :raise arango.exceptions.CollectionLoadError: If operation fails. """ + m = "The load function is deprecated from version 3.8.0 onwards and is a no-op from version 3.9.0 onwards." # noqa: E501 + warn(m, DeprecationWarning, stacklevel=2) + request = Request(method="put", endpoint=f"/_api/collection/{self.name}/load") def response_handler(resp: Response) -> bool: @@ -553,10 +561,18 @@ def response_handler(resp: Response) -> bool: def unload(self) -> Result[bool]: """Unload the collection from memory. + .. note:: + The unload function is deprecated from version 3.8.0 onwards and is a + no-op from version 3.9.0 onwards. It should no longer be used, as it + may be removed in a future version of ArangoDB. + :return: True if collection was unloaded successfully. :rtype: bool :raise arango.exceptions.CollectionUnloadError: If operation fails. """ + m = "The unload function is deprecated from version 3.8.0 onwards and is a no-op from version 3.9.0 onwards." # noqa: E501 + warn(m, DeprecationWarning, stacklevel=2) + request = Request(method="put", endpoint=f"/_api/collection/{self.name}/unload") def response_handler(resp: Response) -> bool: From 453f74b667c4d634196bed15cba2531957dfa7dd Mon Sep 17 00:00:00 2001 From: Alex Petenchea Date: Mon, 18 Aug 2025 14:18:03 +0800 Subject: [PATCH 11/11] Getting the driver up-to-date (#377) * Adding missing parts * Bumping driver version --- arango/cluster.py | 2 +- arango/database.py | 23 ++++++++++++++++++++++- arango/exceptions.py | 4 ++++ arango/request.py | 2 +- docs/foxx.rst | 4 ++-- tests/test_database.py | 6 ++++++ 6 files changed, 36 insertions(+), 5 deletions(-) diff --git a/arango/cluster.py b/arango/cluster.py index ea13279d..78fd3ac9 100644 --- a/arango/cluster.py +++ b/arango/cluster.py @@ -261,7 +261,7 @@ def endpoints(self) -> Result[List[str]]: :return: List of endpoints. :rtype: [str] - :raise arango.exceptions.ServerEndpointsError: If retrieval fails. + :raise arango.exceptions.ClusterEndpointsError: If retrieval fails. """ request = Request(method="get", endpoint="/_api/cluster/endpoints") diff --git a/arango/database.py b/arango/database.py index 20e771d2..766161df 100644 --- a/arango/database.py +++ b/arango/database.py @@ -45,6 +45,7 @@ PermissionResetError, PermissionUpdateError, ServerAvailableOptionsGetError, + ServerCheckAvailabilityError, ServerCurrentOptionsGetError, ServerDetailsError, ServerEchoError, @@ -445,7 +446,7 @@ def set_license(self, license: str, force: bool = False) -> Result[Json]: :type force: bool :return: Server license. :rtype: dict - :raise arango.exceptions.ServerLicenseError: If retrieval fails. + :raise arango.exceptions.ServerLicenseSetError: If retrieval fails. """ request = Request( method="put", @@ -481,6 +482,25 @@ def response_handler(resp: Response) -> Json: return self._execute(request, response_handler) + def check_availability(self) -> Result[str]: + """Return ArangoDB server availability mode. + + :return: Server availability mode ("readonly" or "default"). + :rtype: str + :raise arango.exceptions.ServerCheckAvailabilityError: If retrieval fails. + """ + request = Request( + method="get", + endpoint="/_admin/server/availability", + ) + + def response_handler(resp: Response) -> str: + if not resp.is_success: + raise ServerCheckAvailabilityError(resp, request) + return str(resp.body["mode"]) + + return self._execute(request, response_handler) + def compact( self, change_level: Optional[bool] = None, @@ -1069,6 +1089,7 @@ def metrics(self) -> Result[str]: :return: Server metrics in Prometheus format. :rtype: str + :raise arango.exceptions.ServerMetricsError: If operation fails. """ request = Request(method="get", endpoint="/_admin/metrics/v2") diff --git a/arango/exceptions.py b/arango/exceptions.py index 789468ed..891c813e 100644 --- a/arango/exceptions.py +++ b/arango/exceptions.py @@ -654,6 +654,10 @@ class ServerTimeError(ArangoServerError): """Failed to retrieve server system time.""" +class ServerCheckAvailabilityError(ArangoServerError): + """Failed to retrieve server availability mode.""" + + class ServerEchoError(ArangoServerError): """Failed to retrieve details on last request.""" diff --git a/arango/request.py b/arango/request.py index 8735afe6..4bb135a5 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.2.1" + driver_version = "8.2.2" driver_header = "python-arango/" + driver_version + " (" + flags + ")" normalized_headers: Headers = { "charset": "utf-8", diff --git a/docs/foxx.rst b/docs/foxx.rst index 4f6ce35e..734a3168 100644 --- a/docs/foxx.rst +++ b/docs/foxx.rst @@ -83,9 +83,9 @@ information, refer to `ArangoDB manual`_. foxx.readme(service_mount) foxx.swagger(service_mount) foxx.download(service_mount) - foxx.commit(service_mount) + foxx.commit() foxx.scripts(service_mount) - foxx.run_script(service_mount, 'setup', []) + foxx.run_script(service_mount, 'setup', {}) foxx.run_tests(service_mount, reporter='xunit', output_format='xml') # Delete a service. diff --git a/tests/test_database.py b/tests/test_database.py index 014f0235..4e8a160e 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -20,6 +20,7 @@ DatabaseListError, DatabasePropertiesError, DatabaseSupportInfoError, + ServerCheckAvailabilityError, ServerDetailsError, ServerEchoError, ServerEngineError, @@ -355,6 +356,11 @@ def test_database_misc_methods(client, sys_db, db, bad_db, cluster, secret, db_v with pytest.raises(CollectionKeyGeneratorsError): bad_db.key_generators() + with pytest.raises(ServerCheckAvailabilityError): + bad_db.check_availability() + availability = db.check_availability() + assert isinstance(availability, str) + def test_database_management(db, sys_db, bad_db): # Test list databases