diff --git a/arango/aql.py b/arango/aql.py index 141d1fb7..8d445f2a 100644 --- a/arango/aql.py +++ b/arango/aql.py @@ -266,6 +266,7 @@ def execute( skip_inaccessible_cols: Optional[bool] = None, max_runtime: Optional[Number] = None, fill_block_cache: Optional[bool] = None, + allow_dirty_read: bool = False, ) -> Result[Cursor]: """Execute the query and return the result cursor. @@ -358,6 +359,8 @@ def execute( query will not make it into the RocksDB block cache if not already in there, thus leaving more room for the actual hot set. :type fill_block_cache: bool + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :return: Result cursor. :rtype: arango.cursor.Cursor :raise arango.exceptions.AQLQueryExecuteError: If execute fails. @@ -408,7 +411,12 @@ def execute( data["options"] = options data.update(options) - request = Request(method="post", endpoint="/_api/cursor", data=data) + request = Request( + method="post", + endpoint="/_api/cursor", + data=data, + headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None, + ) def response_handler(resp: Response) -> Cursor: if not resp.is_success: diff --git a/arango/collection.py b/arango/collection.py index 6bde79e3..603eebc5 100644 --- a/arango/collection.py +++ b/arango/collection.py @@ -511,6 +511,7 @@ def has( document: Union[str, Json], rev: Optional[str] = None, check_rev: bool = True, + allow_dirty_read: bool = False, ) -> Result[bool]: """Check if a document exists in the collection. @@ -523,6 +524,8 @@ def has( :param check_rev: If set to True, revision of **document** (if given) is compared against the revision of target document. :type check_rev: bool + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :return: True if document exists, False otherwise. :rtype: bool :raise arango.exceptions.DocumentInError: If check fails. @@ -530,6 +533,9 @@ def has( """ handle, body, headers = self._prep_from_doc(document, rev, check_rev) + if allow_dirty_read: + headers["x-arango-allow-dirty-read"] = "true" + request = Request( method="get", endpoint=f"/_api/document/{handle}", @@ -662,7 +668,11 @@ def response_handler(resp: Response) -> Cursor: return self._execute(request, response_handler) def find_near( - self, latitude: Number, longitude: Number, limit: Optional[int] = None + self, + latitude: Number, + longitude: Number, + limit: Optional[int] = None, + allow_dirty_read: bool = False, ) -> Result[Cursor]: """Return documents near a given coordinate. @@ -677,6 +687,8 @@ def find_near( :type longitude: int | float :param limit: Max number of documents returned. :type limit: int | None + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :returns: Document cursor. :rtype: arango.cursor.Cursor :raises arango.exceptions.DocumentGetError: If retrieval fails. @@ -705,6 +717,7 @@ def find_near( endpoint="/_api/cursor", data={"query": query, "bindVars": bind_vars, "count": True}, read=self.name, + headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None, ) def response_handler(resp: Response) -> Cursor: @@ -721,6 +734,7 @@ def find_in_range( upper: int, skip: Optional[int] = None, limit: Optional[int] = None, + allow_dirty_read: bool = False, ) -> Result[Cursor]: """Return documents within a given range in a random order. @@ -736,6 +750,8 @@ def find_in_range( :type skip: int | None :param limit: Max number of documents returned. :type limit: int | None + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :returns: Document cursor. :rtype: arango.cursor.Cursor :raises arango.exceptions.DocumentGetError: If retrieval fails. @@ -764,6 +780,7 @@ def find_in_range( endpoint="/_api/cursor", data={"query": query, "bindVars": bind_vars, "count": True}, read=self.name, + headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None, ) def response_handler(resp: Response) -> Cursor: @@ -779,6 +796,7 @@ def find_in_radius( longitude: Number, radius: Number, distance_field: Optional[str] = None, + allow_dirty_read: bool = False, ) -> Result[Cursor]: """Return documents within a given radius around a coordinate. @@ -793,6 +811,8 @@ def find_in_radius( :param distance_field: Document field used to indicate the distance to the given coordinate. This parameter is ignored in transactions. :type distance_field: str + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :returns: Document cursor. :rtype: arango.cursor.Cursor :raises arango.exceptions.DocumentGetError: If retrieval fails. @@ -823,6 +843,7 @@ def find_in_radius( endpoint="/_api/cursor", data={"query": query, "bindVars": bind_vars, "count": True}, read=self.name, + headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None, ) def response_handler(resp: Response) -> Cursor: @@ -899,7 +920,11 @@ def response_handler(resp: Response) -> Cursor: return self._execute(request, response_handler) def find_by_text( - self, field: str, query: str, limit: Optional[int] = None + self, + field: str, + query: str, + limit: Optional[int] = None, + allow_dirty_read: bool = False, ) -> Result[Cursor]: """Return documents that match the given fulltext query. @@ -909,6 +934,8 @@ def find_by_text( :type query: str :param limit: Max number of documents returned. :type limit: int | None + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :returns: Document cursor. :rtype: arango.cursor.Cursor :raises arango.exceptions.DocumentGetError: If retrieval fails. @@ -935,6 +962,7 @@ def find_by_text( endpoint="/_api/cursor", data={"query": aql, "bindVars": bind_vars, "count": True}, read=self.name, + headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None, ) def response_handler(resp: Response) -> Cursor: @@ -944,12 +972,18 @@ def response_handler(resp: Response) -> Cursor: return self._execute(request, response_handler) - def get_many(self, documents: Sequence[Union[str, Json]]) -> Result[List[Json]]: + def get_many( + self, + documents: Sequence[Union[str, Json]], + allow_dirty_read: bool = False, + ) -> Result[List[Json]]: """Return multiple documents ignoring any missing ones. :param documents: List of document keys, IDs or bodies. Document bodies must contain the "_id" or "_key" fields. :type documents: [str | dict] + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :return: Documents. Missing ones are not included. :rtype: [dict] :raise arango.exceptions.DocumentGetError: If retrieval fails. @@ -964,6 +998,7 @@ def get_many(self, documents: Sequence[Union[str, Json]]) -> Result[List[Json]]: params=params, data=handles, read=self.name, + headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None, ) def response_handler(resp: Response) -> List[Json]: @@ -2054,6 +2089,7 @@ def get( document: Union[str, Json], rev: Optional[str] = None, check_rev: bool = True, + allow_dirty_read: bool = False, ) -> Result[Optional[Json]]: """Return a document. @@ -2066,6 +2102,8 @@ def get( :param check_rev: If set to True, revision of **document** (if given) is compared against the revision of target document. :type check_rev: bool + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :return: Document, or None if not found. :rtype: dict | None :raise arango.exceptions.DocumentGetError: If retrieval fails. @@ -2073,6 +2111,9 @@ def get( """ handle, body, headers = self._prep_from_doc(document, rev, check_rev) + if allow_dirty_read: + headers["x-arango-allow-dirty-read"] = "true" + request = Request( method="get", endpoint=f"/_api/document/{handle}", @@ -3075,7 +3116,10 @@ def link( return self.insert(edge, sync=sync, silent=silent, return_new=return_new) def edges( - self, vertex: Union[str, Json], direction: Optional[str] = None + self, + vertex: Union[str, Json], + direction: Optional[str] = None, + allow_dirty_read: bool = False, ) -> Result[Json]: """Return the edge documents coming in and/or out of the vertex. @@ -3084,6 +3128,8 @@ def edges( :param direction: The direction of the edges. Allowed values are "in" and "out". If not set, edges in both directions are returned. :type direction: str + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :return: List of edges and statistics. :rtype: dict :raise arango.exceptions.EdgeListError: If retrieval fails. @@ -3097,6 +3143,7 @@ def edges( endpoint=f"/_api/edges/{self.name}", params=params, read=self.name, + headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None, ) def response_handler(resp: Response) -> Json: diff --git a/arango/database.py b/arango/database.py index 289bfdc9..d3939396 100644 --- a/arango/database.py +++ b/arango/database.py @@ -224,6 +224,7 @@ def execute_transaction( allow_implicit: Optional[bool] = None, intermediate_commit_count: Optional[int] = None, intermediate_commit_size: Optional[int] = None, + allow_dirty_read: bool = False, ) -> Result[Any]: """Execute raw Javascript command in transaction. @@ -256,6 +257,8 @@ def execute_transaction( :param intermediate_commit_size: Max size of operations in bytes after which an intermediate commit is performed automatically. :type intermediate_commit_size: int | None + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :return: Return value of **command**. :rtype: Any :raise arango.exceptions.TransactionExecuteError: If execution fails. @@ -282,7 +285,12 @@ def execute_transaction( if intermediate_commit_size is not None: data["intermediateCommitSize"] = intermediate_commit_size - request = Request(method="post", endpoint="/_api/transaction", data=data) + request = Request( + method="post", + endpoint="/_api/transaction", + data=data, + headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None, + ) def response_handler(resp: Response) -> Any: if not resp.is_success: diff --git a/arango/executor.py b/arango/executor.py index 722a1bdd..a340dcbd 100644 --- a/arango/executor.py +++ b/arango/executor.py @@ -293,6 +293,8 @@ class TransactionApiExecutor: :type lock_timeout: int :param max_size: Max transaction size in bytes. :type max_size: int + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None """ def __init__( @@ -305,6 +307,7 @@ def __init__( allow_implicit: Optional[bool] = None, lock_timeout: Optional[int] = None, max_size: Optional[int] = None, + allow_dirty_read: bool = False, ) -> None: self._conn = connection @@ -326,7 +329,12 @@ def __init__( if max_size is not None: data["maxTransactionSize"] = max_size - request = Request(method="post", endpoint="/_api/transaction/begin", data=data) + request = Request( + method="post", + endpoint="/_api/transaction/begin", + data=data, + headers={"x-arango-allow-dirty-read": "true"} if allow_dirty_read else None, + ) resp = self._conn.send_request(request) if not resp.is_success: @@ -348,16 +356,25 @@ def id(self) -> str: """ return self._id - def execute(self, request: Request, response_handler: Callable[[Response], T]) -> T: + def execute( + self, + request: Request, + response_handler: Callable[[Response], T], + allow_dirty_read: bool = False, + ) -> T: """Execute API request in a transaction and return the result. :param request: HTTP request. :type request: arango.request.Request :param response_handler: HTTP response handler. :type response_handler: callable + :param allow_dirty_read: Allow reads from followers in a cluster. + :type allow_dirty_read: bool | None :return: API execution result. """ request.headers["x-arango-trx-id"] = self._id + if allow_dirty_read: + request.headers["x-arango-allow-dirty-read"] = "true" resp = self._conn.send_request(request) return response_handler(resp)