python-arango-async 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1688 @@
1
+ __all__ = ["Collection", "StandardCollection"]
2
+
3
+
4
+ from typing import Any, Generic, List, Optional, Sequence, Tuple, TypeVar, cast
5
+
6
+ from arangoasync.cursor import Cursor
7
+ from arangoasync.errno import (
8
+ DOCUMENT_NOT_FOUND,
9
+ HTTP_BAD_PARAMETER,
10
+ HTTP_NOT_FOUND,
11
+ HTTP_PRECONDITION_FAILED,
12
+ )
13
+ from arangoasync.exceptions import (
14
+ CollectionPropertiesError,
15
+ CollectionTruncateError,
16
+ DocumentCountError,
17
+ DocumentDeleteError,
18
+ DocumentGetError,
19
+ DocumentInsertError,
20
+ DocumentParseError,
21
+ DocumentReplaceError,
22
+ DocumentRevisionError,
23
+ DocumentUpdateError,
24
+ IndexCreateError,
25
+ IndexDeleteError,
26
+ IndexGetError,
27
+ IndexListError,
28
+ IndexLoadError,
29
+ SortValidationError,
30
+ )
31
+ from arangoasync.executor import ApiExecutor, DefaultApiExecutor, NonAsyncExecutor
32
+ from arangoasync.request import Method, Request
33
+ from arangoasync.response import Response
34
+ from arangoasync.result import Result
35
+ from arangoasync.serialization import Deserializer, Serializer
36
+ from arangoasync.typings import (
37
+ CollectionProperties,
38
+ IndexProperties,
39
+ Json,
40
+ Jsons,
41
+ Params,
42
+ RequestHeaders,
43
+ )
44
+
45
+ T = TypeVar("T") # Serializer type
46
+ U = TypeVar("U") # Deserializer loads
47
+ V = TypeVar("V") # Deserializer loads_many
48
+
49
+
50
+ class Collection(Generic[T, U, V]):
51
+ """Base class for collection API wrappers.
52
+
53
+ Args:
54
+ executor (ApiExecutor): API executor.
55
+ name (str): Collection name
56
+ doc_serializer (Serializer): Document serializer.
57
+ doc_deserializer (Deserializer): Document deserializer.
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ executor: ApiExecutor,
63
+ name: str,
64
+ doc_serializer: Serializer[T],
65
+ doc_deserializer: Deserializer[U, V],
66
+ ) -> None:
67
+ self._executor = executor
68
+ self._name = name
69
+ self._doc_serializer = doc_serializer
70
+ self._doc_deserializer = doc_deserializer
71
+ self._id_prefix = f"{self._name}/"
72
+
73
+ def _validate_id(self, doc_id: str) -> str:
74
+ """Check the collection name in the document ID.
75
+
76
+ Args:
77
+ doc_id (str): Document ID.
78
+
79
+ Returns:
80
+ str: Verified document ID.
81
+
82
+ Raises:
83
+ DocumentParseError: On bad collection name.
84
+ """
85
+ if not doc_id.startswith(self._id_prefix):
86
+ raise DocumentParseError(f'Bad collection name in document ID "{doc_id}"')
87
+ return doc_id
88
+
89
+ def _extract_id(self, body: Json) -> str:
90
+ """Extract the document ID from document body.
91
+
92
+ Args:
93
+ body (dict): Document body.
94
+
95
+ Returns:
96
+ str: Document ID.
97
+
98
+ Raises:
99
+ DocumentParseError: On missing ID and key.
100
+ """
101
+ try:
102
+ if "_id" in body:
103
+ return self._validate_id(body["_id"])
104
+ else:
105
+ key: str = body["_key"]
106
+ return self._id_prefix + key
107
+ except KeyError:
108
+ raise DocumentParseError('Field "_key" or "_id" required')
109
+
110
+ def _ensure_key_from_id(self, body: Json) -> Json:
111
+ """Return the body with "_key" field if it has "_id" field.
112
+
113
+ Args:
114
+ body (dict): Document body.
115
+
116
+ Returns:
117
+ dict: Document body with "_key" field if it has "_id" field.
118
+ """
119
+ if "_id" in body and "_key" not in body:
120
+ doc_id = self._validate_id(body["_id"])
121
+ body = body.copy()
122
+ body["_key"] = doc_id[len(self._id_prefix) :]
123
+ return body
124
+
125
+ def _prep_from_doc(
126
+ self,
127
+ document: str | Json,
128
+ rev: Optional[str] = None,
129
+ check_rev: bool = False,
130
+ ) -> Tuple[str, Json]:
131
+ """Prepare document ID, body and request headers before a query.
132
+
133
+ Args:
134
+ document (str | dict): Document ID, key or body.
135
+ rev (str | None): Document revision.
136
+ check_rev (bool): Whether to check the revision.
137
+
138
+ Returns:
139
+ Document ID and request headers.
140
+
141
+ Raises:
142
+ DocumentParseError: On missing ID and key.
143
+ TypeError: On bad document type.
144
+ """
145
+ if isinstance(document, dict):
146
+ doc_id = self._extract_id(document)
147
+ rev = rev or document.get("_rev")
148
+ elif isinstance(document, str):
149
+ if "/" in document:
150
+ doc_id = self._validate_id(document)
151
+ else:
152
+ doc_id = self._id_prefix + document
153
+ else:
154
+ raise TypeError("Document must be str or a dict")
155
+
156
+ if not check_rev or rev is None:
157
+ return doc_id, {}
158
+ else:
159
+ return doc_id, {"If-Match": rev}
160
+
161
+ def _build_filter_conditions(self, filters: Optional[Json]) -> str:
162
+ """Build filter conditions for an AQL query.
163
+
164
+ Args:
165
+ filters (dict | None): Document filters.
166
+
167
+ Returns:
168
+ str: The complete AQL filter condition.
169
+ """
170
+ if not filters:
171
+ return ""
172
+
173
+ conditions = []
174
+ for k, v in filters.items():
175
+ field = k if "." in k else f"`{k}`"
176
+ conditions.append(f"doc.{field} == {self.serializer.dumps(v)}")
177
+
178
+ return "FILTER " + " AND ".join(conditions)
179
+
180
+ @staticmethod
181
+ def _is_none_or_int(obj: Any) -> bool:
182
+ """Check if obj is `None` or a positive integer.
183
+
184
+ Args:
185
+ obj: Object to check.
186
+
187
+ Returns:
188
+ bool: `True` if object is `None` or a positive integer.
189
+ """
190
+ return obj is None or isinstance(obj, int) and obj >= 0
191
+
192
+ @staticmethod
193
+ def _is_none_or_dict(obj: Any) -> bool:
194
+ """Check if obj is `None` or a dict.
195
+
196
+ Args:
197
+ obj: Object to check.
198
+
199
+ Returns:
200
+ bool: `True` if object is `None` or a dict.
201
+ """
202
+ return obj is None or isinstance(obj, dict)
203
+
204
+ @staticmethod
205
+ def _validate_sort_parameters(sort: Optional[Jsons]) -> None:
206
+ """Validate sort parameters for an AQL query.
207
+
208
+ Args:
209
+ sort (list | None): Document sort parameters.
210
+
211
+ Raises:
212
+ SortValidationError: If sort parameters are invalid.
213
+ """
214
+ if not sort:
215
+ return
216
+
217
+ for param in sort:
218
+ if "sort_by" not in param or "sort_order" not in param:
219
+ raise SortValidationError(
220
+ "Each sort parameter must have 'sort_by' and 'sort_order'."
221
+ )
222
+ if param["sort_order"].upper() not in ["ASC", "DESC"]:
223
+ raise SortValidationError("'sort_order' must be either 'ASC' or 'DESC'")
224
+
225
+ @staticmethod
226
+ def _build_sort_expression(sort: Optional[Jsons]) -> str:
227
+ """Build a sort condition for an AQL query.
228
+
229
+ Args:
230
+ sort (list | None): Document sort parameters.
231
+
232
+ Returns:
233
+ str: The complete AQL sort condition.
234
+ """
235
+ if not sort:
236
+ return ""
237
+
238
+ sort_chunks = []
239
+ for sort_param in sort:
240
+ chunk = f"doc.{sort_param['sort_by']} {sort_param['sort_order']}"
241
+ sort_chunks.append(chunk)
242
+
243
+ return "SORT " + ", ".join(sort_chunks)
244
+
245
+ @property
246
+ def name(self) -> str:
247
+ """Return the name of the collection.
248
+
249
+ Returns:
250
+ str: Collection name.
251
+ """
252
+ return self._name
253
+
254
+ @property
255
+ def db_name(self) -> str:
256
+ """Return the name of the current database.
257
+
258
+ Returns:
259
+ str: Database name.
260
+ """
261
+ return self._executor.db_name
262
+
263
+ @property
264
+ def serializer(self) -> Serializer[Json]:
265
+ """Return the serializer."""
266
+ return self._executor.serializer
267
+
268
+ @property
269
+ def deserializer(self) -> Deserializer[Json, Jsons]:
270
+ """Return the deserializer."""
271
+ return self._executor.deserializer
272
+
273
+ async def indexes(self) -> Result[List[IndexProperties]]:
274
+ """Fetch all index descriptions for the given collection.
275
+
276
+ Returns:
277
+ list: List of index properties.
278
+
279
+ Raises:
280
+ IndexListError: If retrieval fails.
281
+
282
+ References:
283
+ - `list-all-indexes-of-a-collection <https://docs.arangodb.com/stable/develop/http-api/indexes/#list-all-indexes-of-a-collection>`__
284
+ """ # noqa: E501
285
+ request = Request(
286
+ method=Method.GET,
287
+ endpoint="/_api/index",
288
+ params=dict(collection=self._name),
289
+ )
290
+
291
+ def response_handler(resp: Response) -> List[IndexProperties]:
292
+ if not resp.is_success:
293
+ raise IndexListError(resp, request)
294
+ data = self.deserializer.loads(resp.raw_body)
295
+ return [IndexProperties(item) for item in data["indexes"]]
296
+
297
+ return await self._executor.execute(request, response_handler)
298
+
299
+ async def get_index(self, id: str | int) -> Result[IndexProperties]:
300
+ """Return the properties of an index.
301
+
302
+ Args:
303
+ id (str): Index ID. Could be either the full ID or just the index number.
304
+
305
+ Returns:
306
+ IndexProperties: Index properties.
307
+
308
+ Raises:
309
+ IndexGetError: If retrieval fails.
310
+
311
+ References:
312
+ `get-an-index <https://docs.arangodb.com/stable/develop/http-api/indexes/#get-an-index>`__
313
+ """ # noqa: E501
314
+ if isinstance(id, int):
315
+ full_id = f"{self._name}/{id}"
316
+ else:
317
+ full_id = id if "/" in id else f"{self._name}/{id}"
318
+
319
+ request = Request(
320
+ method=Method.GET,
321
+ endpoint=f"/_api/index/{full_id}",
322
+ )
323
+
324
+ def response_handler(resp: Response) -> IndexProperties:
325
+ if not resp.is_success:
326
+ raise IndexGetError(resp, request)
327
+ return IndexProperties(self.deserializer.loads(resp.raw_body))
328
+
329
+ return await self._executor.execute(request, response_handler)
330
+
331
+ async def add_index(
332
+ self,
333
+ type: str,
334
+ fields: Json | List[str],
335
+ options: Optional[Json] = None,
336
+ ) -> Result[IndexProperties]:
337
+ """Create an index.
338
+
339
+ Args:
340
+ type (str): Type attribute (ex. "persistent", "inverted", "ttl", "mdi",
341
+ "geo").
342
+ fields (dict | list): Fields to index.
343
+ options (dict | None): Additional index options.
344
+
345
+ Returns:
346
+ IndexProperties: New index properties.
347
+
348
+ Raises:
349
+ IndexCreateError: If index creation fails.
350
+
351
+ References:
352
+ - `create-an-index <https://docs.arangodb.com/stable/develop/http-api/indexes/#create-an-index>`__
353
+ - `create-a-persistent-index <https://docs.arangodb.com/stable/develop/http-api/indexes/persistent/#create-a-persistent-index>`__
354
+ - `create-an-inverted-index <https://docs.arangodb.com/stable/develop/http-api/indexes/inverted/#create-an-inverted-index>`__
355
+ - `create-a-ttl-index <https://docs.arangodb.com/stable/develop/http-api/indexes/ttl/#create-a-ttl-index>`__
356
+ - `create-a-multi-dimensional-index <https://docs.arangodb.com/stable/develop/http-api/indexes/multi-dimensional/#create-a-multi-dimensional-index>`__
357
+ - `create-a-geo-spatial-index <https://docs.arangodb.com/stable/develop/http-api/indexes/geo-spatial/#create-a-geo-spatial-index>`__
358
+ """ # noqa: E501
359
+ options = options or {}
360
+ request = Request(
361
+ method=Method.POST,
362
+ endpoint="/_api/index",
363
+ data=self.serializer.dumps(dict(type=type, fields=fields) | options),
364
+ params=dict(collection=self._name),
365
+ )
366
+
367
+ def response_handler(resp: Response) -> IndexProperties:
368
+ if not resp.is_success:
369
+ raise IndexCreateError(resp, request)
370
+ return IndexProperties(self.deserializer.loads(resp.raw_body))
371
+
372
+ return await self._executor.execute(request, response_handler)
373
+
374
+ async def delete_index(
375
+ self, id: str | int, ignore_missing: bool = False
376
+ ) -> Result[bool]:
377
+ """Delete an index.
378
+
379
+ Args:
380
+ id (str): Index ID. Could be either the full ID or just the index number.
381
+ ignore_missing (bool): Do not raise an exception on missing index.
382
+
383
+ Returns:
384
+ bool: `True` if the operation was successful. `False` if the index was not
385
+ found and **ignore_missing** was set to `True`.
386
+
387
+ Raises:
388
+ IndexDeleteError: If deletion fails.
389
+
390
+ References:
391
+ - `delete-an-index <https://docs.arangodb.com/stable/develop/http-api/indexes/#delete-an-index>`__
392
+ """ # noqa: E501
393
+ if isinstance(id, int):
394
+ full_id = f"{self._name}/{id}"
395
+ else:
396
+ full_id = id if "/" in id else f"{self._name}/{id}"
397
+
398
+ request = Request(
399
+ method=Method.DELETE,
400
+ endpoint=f"/_api/index/{full_id}",
401
+ )
402
+
403
+ def response_handler(resp: Response) -> bool:
404
+ if resp.is_success:
405
+ return True
406
+ elif ignore_missing and resp.status_code == HTTP_NOT_FOUND:
407
+ return False
408
+ raise IndexDeleteError(resp, request)
409
+
410
+ return await self._executor.execute(request, response_handler)
411
+
412
+ async def load_indexes(self) -> Result[bool]:
413
+ """Cache this collection’s index entries in the main memory.
414
+
415
+ Returns:
416
+ bool: `True` if the operation was successful.
417
+
418
+ Raises:
419
+ IndexLoadError: If loading fails.
420
+
421
+ References:
422
+ - `load-collection-indexes-into-memory <https://docs.arangodb.com/stable/develop/http-api/collections/#load-collection-indexes-into-memory>`__
423
+ """ # noqa: E501
424
+ request = Request(
425
+ method=Method.PUT,
426
+ endpoint=f"/_api/collection/{self._name}/loadIndexesIntoMemory",
427
+ )
428
+
429
+ def response_handler(resp: Response) -> bool:
430
+ if resp.is_success:
431
+ return True
432
+ raise IndexLoadError(resp, request)
433
+
434
+ return await self._executor.execute(request, response_handler)
435
+
436
+
437
+ class StandardCollection(Collection[T, U, V]):
438
+ """Standard collection API wrapper.
439
+
440
+ Args:
441
+ executor (ApiExecutor): API executor.
442
+ name (str): Collection name
443
+ doc_serializer (Serializer): Document serializer.
444
+ doc_deserializer (Deserializer): Document deserializer.
445
+ """
446
+
447
+ def __init__(
448
+ self,
449
+ executor: ApiExecutor,
450
+ name: str,
451
+ doc_serializer: Serializer[T],
452
+ doc_deserializer: Deserializer[U, V],
453
+ ) -> None:
454
+ super().__init__(executor, name, doc_serializer, doc_deserializer)
455
+
456
+ def __repr__(self) -> str:
457
+ return f"<StandardCollection {self.name}>"
458
+
459
+ async def properties(self) -> Result[CollectionProperties]:
460
+ """Return the full properties of the current collection.
461
+
462
+ Returns:
463
+ CollectionProperties: Properties.
464
+
465
+ Raises:
466
+ CollectionPropertiesError: If retrieval fails.
467
+
468
+ References:
469
+ - `get-the-properties-of-a-collection <https://docs.arangodb.com/stable/develop/http-api/collections/#get-the-properties-of-a-collection>`__
470
+ """ # noqa: E501
471
+ request = Request(
472
+ method=Method.GET,
473
+ endpoint=f"/_api/collection/{self.name}/properties",
474
+ )
475
+
476
+ def response_handler(resp: Response) -> CollectionProperties:
477
+ if not resp.is_success:
478
+ raise CollectionPropertiesError(resp, request)
479
+ return CollectionProperties(self._executor.deserialize(resp.raw_body))
480
+
481
+ return await self._executor.execute(request, response_handler)
482
+
483
+ async def truncate(
484
+ self,
485
+ wait_for_sync: Optional[bool] = None,
486
+ compact: Optional[bool] = None,
487
+ ) -> None:
488
+ """Removes all documents, but leaves indexes intact.
489
+
490
+ Args:
491
+ wait_for_sync (bool | None): If set to `True`, the data is synchronized
492
+ to disk before returning from the truncate operation.
493
+ compact (bool | None): If set to `True`, the storage engine is told to
494
+ start a compaction in order to free up disk space. This can be
495
+ resource intensive. If the only intention is to start over with an
496
+ empty collection, specify `False`.
497
+
498
+ Raises:
499
+ CollectionTruncateError: If truncation fails.
500
+
501
+ References:
502
+ - `truncate-a-collection <https://docs.arangodb.com/stable/develop/http-api/collections/#truncate-a-collection>`__
503
+ """ # noqa: E501
504
+ params: Params = {}
505
+ if wait_for_sync is not None:
506
+ params["waitForSync"] = wait_for_sync
507
+ if compact is not None:
508
+ params["compact"] = compact
509
+
510
+ request = Request(
511
+ method=Method.PUT,
512
+ endpoint=f"/_api/collection/{self.name}/truncate",
513
+ params=params,
514
+ )
515
+
516
+ def response_handler(resp: Response) -> None:
517
+ if not resp.is_success:
518
+ raise CollectionTruncateError(resp, request)
519
+
520
+ await self._executor.execute(request, response_handler)
521
+
522
+ async def count(self) -> Result[int]:
523
+ """Return the total document count.
524
+
525
+ Returns:
526
+ int: Total document count.
527
+
528
+ Raises:
529
+ DocumentCountError: If retrieval fails.
530
+ """
531
+ request = Request(
532
+ method=Method.GET, endpoint=f"/_api/collection/{self.name}/count"
533
+ )
534
+
535
+ def response_handler(resp: Response) -> int:
536
+ if resp.is_success:
537
+ result: int = self.deserializer.loads(resp.raw_body)["count"]
538
+ return result
539
+ raise DocumentCountError(resp, request)
540
+
541
+ return await self._executor.execute(request, response_handler)
542
+
543
+ async def get(
544
+ self,
545
+ document: str | Json,
546
+ allow_dirty_read: bool = False,
547
+ if_match: Optional[str] = None,
548
+ if_none_match: Optional[str] = None,
549
+ ) -> Result[Optional[U]]:
550
+ """Return a document.
551
+
552
+ Args:
553
+ document (str | dict): Document ID, key or body.
554
+ Document body must contain the "_id" or "_key" field.
555
+ allow_dirty_read (bool): Allow reads from followers in a cluster.
556
+ if_match (str | None): The document is returned, if it has the same
557
+ revision as the given ETag.
558
+ if_none_match (str | None): The document is returned, if it has a
559
+ different revision than the given ETag.
560
+
561
+ Returns:
562
+ Document or `None` if not found.
563
+
564
+ Raises:
565
+ DocumentRevisionError: If the revision is incorrect.
566
+ DocumentGetError: If retrieval fails.
567
+
568
+ References:
569
+ - `get-a-document <https://docs.arangodb.com/stable/develop/http-api/documents/#get-a-document>`__
570
+ """ # noqa: E501
571
+ handle, _ = self._prep_from_doc(document)
572
+
573
+ headers: RequestHeaders = {}
574
+ if allow_dirty_read:
575
+ headers["x-arango-allow-dirty-read"] = "true"
576
+ if if_match is not None:
577
+ headers["If-Match"] = if_match
578
+ if if_none_match is not None:
579
+ headers["If-None-Match"] = if_none_match
580
+
581
+ request = Request(
582
+ method=Method.GET,
583
+ endpoint=f"/_api/document/{handle}",
584
+ headers=headers,
585
+ )
586
+
587
+ def response_handler(resp: Response) -> Optional[U]:
588
+ if resp.is_success:
589
+ return self._doc_deserializer.loads(resp.raw_body)
590
+ elif resp.status_code == HTTP_NOT_FOUND:
591
+ if resp.error_code == DOCUMENT_NOT_FOUND:
592
+ return None
593
+ else:
594
+ raise DocumentGetError(resp, request)
595
+ elif resp.status_code == HTTP_PRECONDITION_FAILED:
596
+ raise DocumentRevisionError(resp, request)
597
+ else:
598
+ raise DocumentGetError(resp, request)
599
+
600
+ return await self._executor.execute(request, response_handler)
601
+
602
+ async def has(
603
+ self,
604
+ document: str | Json,
605
+ allow_dirty_read: bool = False,
606
+ if_match: Optional[str] = None,
607
+ if_none_match: Optional[str] = None,
608
+ ) -> Result[bool]:
609
+ """Check if a document exists in the collection.
610
+
611
+ Args:
612
+ document (str | dict): Document ID, key or body.
613
+ Document body must contain the "_id" or "_key" field.
614
+ allow_dirty_read (bool): Allow reads from followers in a cluster.
615
+ if_match (str | None): The document is returned, if it has the same
616
+ revision as the given ETag.
617
+ if_none_match (str | None): The document is returned, if it has a
618
+ different revision than the given ETag.
619
+
620
+ Returns:
621
+ `True` if the document exists, `False` otherwise.
622
+
623
+ Raises:
624
+ DocumentRevisionError: If the revision is incorrect.
625
+ DocumentGetError: If retrieval fails.
626
+
627
+ References:
628
+ - `get-a-document-header <https://docs.arangodb.com/stable/develop/http-api/documents/#get-a-document-header>`__
629
+ """ # noqa: E501
630
+ handle, _ = self._prep_from_doc(document)
631
+
632
+ headers: RequestHeaders = {}
633
+ if allow_dirty_read:
634
+ headers["x-arango-allow-dirty-read"] = "true"
635
+ if if_match is not None:
636
+ headers["If-Match"] = if_match
637
+ if if_none_match is not None:
638
+ headers["If-None-Match"] = if_none_match
639
+
640
+ request = Request(
641
+ method=Method.HEAD,
642
+ endpoint=f"/_api/document/{handle}",
643
+ headers=headers,
644
+ )
645
+
646
+ def response_handler(resp: Response) -> bool:
647
+ if resp.is_success:
648
+ return True
649
+ elif resp.status_code == HTTP_NOT_FOUND:
650
+ return False
651
+ elif resp.status_code == HTTP_PRECONDITION_FAILED:
652
+ raise DocumentRevisionError(resp, request)
653
+ else:
654
+ raise DocumentGetError(resp, request)
655
+
656
+ return await self._executor.execute(request, response_handler)
657
+
658
+ async def insert(
659
+ self,
660
+ document: T,
661
+ wait_for_sync: Optional[bool] = None,
662
+ return_new: Optional[bool] = None,
663
+ return_old: Optional[bool] = None,
664
+ silent: Optional[bool] = None,
665
+ overwrite: Optional[bool] = None,
666
+ overwrite_mode: Optional[str] = None,
667
+ keep_null: Optional[bool] = None,
668
+ merge_objects: Optional[bool] = None,
669
+ refill_index_caches: Optional[bool] = None,
670
+ version_attribute: Optional[str] = None,
671
+ ) -> Result[bool | Json]:
672
+ """Insert a new document.
673
+
674
+ Args:
675
+ document (dict): Document to insert. If it contains the "_key" or "_id"
676
+ field, the value is used as the key of the new document (otherwise
677
+ it is auto-generated). Any "_rev" field is ignored.
678
+ wait_for_sync (bool | None): Wait until document has been synced to disk.
679
+ return_new (bool | None): Additionally return the complete new document
680
+ under the attribute `new` in the result.
681
+ return_old (bool | None): Additionally return the complete old document
682
+ under the attribute `old` in the result. Only available if the
683
+ `overwrite` option is used.
684
+ silent (bool | None): If set to `True`, no document metadata is returned.
685
+ This can be used to save resources.
686
+ overwrite (bool | None): If set to `True`, operation does not fail on
687
+ duplicate key and existing document is overwritten (replace-insert).
688
+ overwrite_mode (str | None): Overwrite mode. Supersedes **overwrite**
689
+ option. May be one of "ignore", "replace", "update" or "conflict".
690
+ keep_null (bool | None): If set to `True`, fields with value None are
691
+ retained in the document. Otherwise, they are removed completely.
692
+ Applies only when **overwrite_mode** is set to "update"
693
+ (update-insert).
694
+ merge_objects (bool | None): If set to `True`, sub-dictionaries are merged
695
+ instead of the new one overwriting the old one. Applies only when
696
+ **overwrite_mode** is set to "update" (update-insert).
697
+ refill_index_caches (bool | None): Whether to add new entries to
698
+ in-memory index caches if document insertions affect the edge index
699
+ or cache-enabled persistent indexes.
700
+ version_attribute (str | None): Support for simple external versioning to
701
+ document operations. Only applicable if **overwrite** is set to `True`
702
+ or **overwrite_mode** is set to "update" or "replace".
703
+
704
+ Returns:
705
+ bool | dict: Document metadata (e.g. document id, key, revision) or `True`
706
+ if **silent** is set to `True`.
707
+
708
+ Raises:
709
+ DocumentInsertError: If insertion fails.
710
+
711
+ References:
712
+ - `create-a-document <https://docs.arangodb.com/stable/develop/http-api/documents/#create-a-document>`__
713
+ """ # noqa: E501
714
+ if isinstance(document, dict):
715
+ # We assume that the document deserializer works with dictionaries.
716
+ document = cast(T, self._ensure_key_from_id(document))
717
+
718
+ params: Params = {}
719
+ if wait_for_sync is not None:
720
+ params["waitForSync"] = wait_for_sync
721
+ if return_new is not None:
722
+ params["returnNew"] = return_new
723
+ if return_old is not None:
724
+ params["returnOld"] = return_old
725
+ if silent is not None:
726
+ params["silent"] = silent
727
+ if overwrite is not None:
728
+ params["overwrite"] = overwrite
729
+ if overwrite_mode is not None:
730
+ params["overwriteMode"] = overwrite_mode
731
+ if keep_null is not None:
732
+ params["keepNull"] = keep_null
733
+ if merge_objects is not None:
734
+ params["mergeObjects"] = merge_objects
735
+ if refill_index_caches is not None:
736
+ params["refillIndexCaches"] = refill_index_caches
737
+ if version_attribute is not None:
738
+ params["versionAttribute"] = version_attribute
739
+
740
+ request = Request(
741
+ method=Method.POST,
742
+ endpoint=f"/_api/document/{self._name}",
743
+ params=params,
744
+ data=self._doc_serializer.dumps(document),
745
+ )
746
+
747
+ def response_handler(resp: Response) -> bool | Json:
748
+ if resp.is_success:
749
+ if silent is True:
750
+ return True
751
+ return self._executor.deserialize(resp.raw_body)
752
+ msg: Optional[str] = None
753
+ if resp.status_code == HTTP_BAD_PARAMETER:
754
+ msg = (
755
+ "Body does not contain a valid JSON representation of "
756
+ "one document."
757
+ )
758
+ elif resp.status_code == HTTP_NOT_FOUND:
759
+ msg = "Collection not found."
760
+ raise DocumentInsertError(resp, request, msg)
761
+
762
+ return await self._executor.execute(request, response_handler)
763
+
764
+ async def update(
765
+ self,
766
+ document: T,
767
+ ignore_revs: Optional[bool] = None,
768
+ wait_for_sync: Optional[bool] = None,
769
+ return_new: Optional[bool] = None,
770
+ return_old: Optional[bool] = None,
771
+ silent: Optional[bool] = None,
772
+ keep_null: Optional[bool] = None,
773
+ merge_objects: Optional[bool] = None,
774
+ refill_index_caches: Optional[bool] = None,
775
+ version_attribute: Optional[str] = None,
776
+ if_match: Optional[str] = None,
777
+ ) -> Result[bool | Json]:
778
+ """Insert a new document.
779
+
780
+ Args:
781
+ document (dict): Partial or full document with the updated values.
782
+ It must contain the "_key" or "_id" field.
783
+ ignore_revs (bool | None): If set to `True`, the `_rev` attribute in the
784
+ document is ignored. If this is set to `False`, then the `_rev`
785
+ attribute given in the body document is taken as a precondition.
786
+ The document is only updated if the current revision is the one
787
+ specified.
788
+ wait_for_sync (bool | None): Wait until document has been synced to disk.
789
+ return_new (bool | None): Additionally return the complete new document
790
+ under the attribute `new` in the result.
791
+ return_old (bool | None): Additionally return the complete old document
792
+ under the attribute `old` in the result.
793
+ silent (bool | None): If set to `True`, no document metadata is returned.
794
+ This can be used to save resources.
795
+ keep_null (bool | None): If the intention is to delete existing attributes
796
+ with the patch command, set this parameter to `False`.
797
+ merge_objects (bool | None): Controls whether objects (not arrays) are
798
+ merged if present in both the existing and the patch document.
799
+ If set to `False`, the value in the patch document overwrites the
800
+ existing document’s value. If set to `True`, objects are merged.
801
+ refill_index_caches (bool | None): Whether to add new entries to
802
+ in-memory index caches if document updates affect the edge index
803
+ or cache-enabled persistent indexes.
804
+ version_attribute (str | None): Support for simple external versioning to
805
+ document operations.
806
+ if_match (str | None): You can conditionally update a document based on a
807
+ target revision id by using the "if-match" HTTP header.
808
+
809
+ Returns:
810
+ bool | dict: Document metadata (e.g. document id, key, revision) or `True`
811
+ if **silent** is set to `True`.
812
+
813
+ Raises:
814
+ DocumentRevisionError: If precondition was violated.
815
+ DocumentUpdateError: If update fails.
816
+
817
+ References:
818
+ - `update-a-document <https://docs.arangodb.com/stable/develop/http-api/documents/#update-a-document>`__
819
+ """ # noqa: E501
820
+ params: Params = {}
821
+ if ignore_revs is not None:
822
+ params["ignoreRevs"] = ignore_revs
823
+ if wait_for_sync is not None:
824
+ params["waitForSync"] = wait_for_sync
825
+ if return_new is not None:
826
+ params["returnNew"] = return_new
827
+ if return_old is not None:
828
+ params["returnOld"] = return_old
829
+ if silent is not None:
830
+ params["silent"] = silent
831
+ if keep_null is not None:
832
+ params["keepNull"] = keep_null
833
+ if merge_objects is not None:
834
+ params["mergeObjects"] = merge_objects
835
+ if refill_index_caches is not None:
836
+ params["refillIndexCaches"] = refill_index_caches
837
+ if version_attribute is not None:
838
+ params["versionAttribute"] = version_attribute
839
+
840
+ headers: RequestHeaders = {}
841
+ if if_match is not None:
842
+ headers["If-Match"] = if_match
843
+
844
+ request = Request(
845
+ method=Method.PATCH,
846
+ endpoint=f"/_api/document/{self._extract_id(cast(Json, document))}",
847
+ params=params,
848
+ headers=headers,
849
+ data=self._doc_serializer.dumps(document),
850
+ )
851
+
852
+ def response_handler(resp: Response) -> bool | Json:
853
+ if resp.is_success:
854
+ if silent is True:
855
+ return True
856
+ return self._executor.deserialize(resp.raw_body)
857
+ msg: Optional[str] = None
858
+ if resp.status_code == HTTP_PRECONDITION_FAILED:
859
+ raise DocumentRevisionError(resp, request)
860
+ elif resp.status_code == HTTP_NOT_FOUND:
861
+ msg = "Document, collection or transaction not found."
862
+ raise DocumentUpdateError(resp, request, msg)
863
+
864
+ return await self._executor.execute(request, response_handler)
865
+
866
+ async def replace(
867
+ self,
868
+ document: T,
869
+ ignore_revs: Optional[bool] = None,
870
+ wait_for_sync: Optional[bool] = None,
871
+ return_new: Optional[bool] = None,
872
+ return_old: Optional[bool] = None,
873
+ silent: Optional[bool] = None,
874
+ refill_index_caches: Optional[bool] = None,
875
+ version_attribute: Optional[str] = None,
876
+ if_match: Optional[str] = None,
877
+ ) -> Result[bool | Json]:
878
+ """Replace a document.
879
+
880
+ Args:
881
+ document (dict): New document. It must contain the "_key" or "_id" field.
882
+ Edge document must also have "_from" and "_to" fields.
883
+ ignore_revs (bool | None): If set to `True`, the `_rev` attribute in the
884
+ document is ignored. If this is set to `False`, then the `_rev`
885
+ attribute given in the body document is taken as a precondition.
886
+ The document is only replaced if the current revision is the one
887
+ specified.
888
+ wait_for_sync (bool | None): Wait until document has been synced to disk.
889
+ return_new (bool | None): Additionally return the complete new document
890
+ under the attribute `new` in the result.
891
+ return_old (bool | None): Additionally return the complete old document
892
+ under the attribute `old` in the result.
893
+ silent (bool | None): If set to `True`, no document metadata is returned.
894
+ This can be used to save resources.
895
+ refill_index_caches (bool | None): Whether to add new entries to
896
+ in-memory index caches if document updates affect the edge index
897
+ or cache-enabled persistent indexes.
898
+ version_attribute (str | None): Support for simple external versioning to
899
+ document operations.
900
+ if_match (str | None): You can conditionally replace a document based on a
901
+ target revision id by using the "if-match" HTTP header.
902
+
903
+ Returns:
904
+ bool | dict: Document metadata (e.g. document id, key, revision) or `True`
905
+ if **silent** is set to `True`.
906
+
907
+ Raises:
908
+ DocumentRevisionError: If precondition was violated.
909
+ DocumentReplaceError: If replace fails.
910
+
911
+ References:
912
+ - `replace-a-document <https://docs.arangodb.com/stable/develop/http-api/documents/#replace-a-document>`__
913
+ """ # noqa: E501
914
+ params: Params = {}
915
+ if ignore_revs is not None:
916
+ params["ignoreRevs"] = ignore_revs
917
+ if wait_for_sync is not None:
918
+ params["waitForSync"] = wait_for_sync
919
+ if return_new is not None:
920
+ params["returnNew"] = return_new
921
+ if return_old is not None:
922
+ params["returnOld"] = return_old
923
+ if silent is not None:
924
+ params["silent"] = silent
925
+ if refill_index_caches is not None:
926
+ params["refillIndexCaches"] = refill_index_caches
927
+ if version_attribute is not None:
928
+ params["versionAttribute"] = version_attribute
929
+
930
+ headers: RequestHeaders = {}
931
+ if if_match is not None:
932
+ headers["If-Match"] = if_match
933
+
934
+ request = Request(
935
+ method=Method.PUT,
936
+ endpoint=f"/_api/document/{self._extract_id(cast(Json, document))}",
937
+ params=params,
938
+ headers=headers,
939
+ data=self._doc_serializer.dumps(document),
940
+ )
941
+
942
+ def response_handler(resp: Response) -> bool | Json:
943
+ if resp.is_success:
944
+ if silent is True:
945
+ return True
946
+ return self._executor.deserialize(resp.raw_body)
947
+ msg: Optional[str] = None
948
+ if resp.status_code == HTTP_PRECONDITION_FAILED:
949
+ raise DocumentRevisionError(resp, request)
950
+ elif resp.status_code == HTTP_NOT_FOUND:
951
+ msg = "Document, collection or transaction not found."
952
+ raise DocumentReplaceError(resp, request, msg)
953
+
954
+ return await self._executor.execute(request, response_handler)
955
+
956
+ async def delete(
957
+ self,
958
+ document: T,
959
+ ignore_revs: Optional[bool] = None,
960
+ ignore_missing: bool = False,
961
+ wait_for_sync: Optional[bool] = None,
962
+ return_old: Optional[bool] = None,
963
+ silent: Optional[bool] = None,
964
+ refill_index_caches: Optional[bool] = None,
965
+ if_match: Optional[str] = None,
966
+ ) -> Result[bool | Json]:
967
+ """Delete a document.
968
+
969
+ Args:
970
+ document (dict): Document ID, key or body. The body must contain the
971
+ "_key" or "_id" field.
972
+ ignore_revs (bool | None): If set to `True`, the `_rev` attribute in the
973
+ document is ignored. If this is set to `False`, then the `_rev`
974
+ attribute given in the body document is taken as a precondition.
975
+ The document is only replaced if the current revision is the one
976
+ specified.
977
+ ignore_missing (bool): Do not raise an exception on missing document.
978
+ This parameter has no effect in transactions where an exception is
979
+ always raised on failures.
980
+ wait_for_sync (bool | None): Wait until operation has been synced to disk.
981
+ return_old (bool | None): Additionally return the complete old document
982
+ under the attribute `old` in the result.
983
+ silent (bool | None): If set to `True`, no document metadata is returned.
984
+ This can be used to save resources.
985
+ refill_index_caches (bool | None): Whether to add new entries to
986
+ in-memory index caches if document updates affect the edge index
987
+ or cache-enabled persistent indexes.
988
+ if_match (bool | None): You can conditionally remove a document based
989
+ on a target revision id by using the "if-match" HTTP header.
990
+
991
+ Returns:
992
+ bool | dict: Document metadata (e.g. document id, key, revision) or `True`
993
+ if **silent** is set to `True` and the document was found.
994
+
995
+ Raises:
996
+ DocumentRevisionError: If precondition was violated.
997
+ DocumentDeleteError: If deletion fails.
998
+
999
+ References:
1000
+ - `remove-a-document <https://docs.arangodb.com/stable/develop/http-api/documents/#remove-a-document>`__
1001
+ """ # noqa: E501
1002
+ params: Params = {}
1003
+ if ignore_revs is not None:
1004
+ params["ignoreRevs"] = ignore_revs
1005
+ if wait_for_sync is not None:
1006
+ params["waitForSync"] = wait_for_sync
1007
+ if return_old is not None:
1008
+ params["returnOld"] = return_old
1009
+ if silent is not None:
1010
+ params["silent"] = silent
1011
+ if refill_index_caches is not None:
1012
+ params["refillIndexCaches"] = refill_index_caches
1013
+
1014
+ headers: RequestHeaders = {}
1015
+ if if_match is not None:
1016
+ headers["If-Match"] = if_match
1017
+
1018
+ request = Request(
1019
+ method=Method.DELETE,
1020
+ endpoint=f"/_api/document/{self._extract_id(cast(Json, document))}",
1021
+ params=params,
1022
+ headers=headers,
1023
+ )
1024
+
1025
+ def response_handler(resp: Response) -> bool | Json:
1026
+ if resp.is_success:
1027
+ if silent is True:
1028
+ return True
1029
+ return self._executor.deserialize(resp.raw_body)
1030
+ msg: Optional[str] = None
1031
+ if resp.status_code == HTTP_PRECONDITION_FAILED:
1032
+ raise DocumentRevisionError(resp, request)
1033
+ elif resp.status_code == HTTP_NOT_FOUND:
1034
+ if resp.error_code == DOCUMENT_NOT_FOUND and ignore_missing:
1035
+ return False
1036
+ msg = "Document, collection or transaction not found."
1037
+ raise DocumentDeleteError(resp, request, msg)
1038
+
1039
+ return await self._executor.execute(request, response_handler)
1040
+
1041
+ async def get_many(
1042
+ self,
1043
+ documents: Sequence[str | T],
1044
+ allow_dirty_read: Optional[bool] = None,
1045
+ ignore_revs: Optional[bool] = None,
1046
+ ) -> Result[V]:
1047
+ """Return multiple documents ignoring any missing ones.
1048
+
1049
+ Args:
1050
+ documents (list): List of document IDs, keys or bodies. A search document
1051
+ must contain at least a value for the `_key` field. A value for `_rev`
1052
+ may be specified to verify whether the document has the same revision
1053
+ value, unless `ignoreRevs` is set to false.
1054
+ allow_dirty_read (bool | None): Allow reads from followers in a cluster.
1055
+ ignore_revs (bool | None): If set to `True`, the `_rev` attribute in the
1056
+ document is ignored. If this is set to `False`, then the `_rev`
1057
+ attribute given in the body document is taken as a precondition.
1058
+ The document is only replaced if the current revision is the one
1059
+ specified.
1060
+
1061
+ Returns:
1062
+ list: List of documents. Missing ones are not included.
1063
+
1064
+ Raises:
1065
+ DocumentGetError: If retrieval fails.
1066
+
1067
+ References:
1068
+ - `get-multiple-documents <https://docs.arangodb.com/stable/develop/http-api/documents/#get-multiple-documents>`__
1069
+ """ # noqa: E501
1070
+ params: Params = {"onlyget": True}
1071
+ if ignore_revs is not None:
1072
+ params["ignoreRevs"] = ignore_revs
1073
+
1074
+ headers: RequestHeaders = {}
1075
+ if allow_dirty_read is not None:
1076
+ if allow_dirty_read is True:
1077
+ headers["x-arango-allow-dirty-read"] = "true"
1078
+ else:
1079
+ headers["x-arango-allow-dirty-read"] = "false"
1080
+
1081
+ request = Request(
1082
+ method=Method.PUT,
1083
+ endpoint=f"/_api/document/{self.name}",
1084
+ params=params,
1085
+ headers=headers,
1086
+ data=self._doc_serializer.dumps(documents),
1087
+ )
1088
+
1089
+ def response_handler(resp: Response) -> V:
1090
+ if not resp.is_success:
1091
+ raise DocumentGetError(resp, request)
1092
+ return self._doc_deserializer.loads_many(resp.raw_body)
1093
+
1094
+ return await self._executor.execute(request, response_handler)
1095
+
1096
+ async def find(
1097
+ self,
1098
+ filters: Optional[Json] = None,
1099
+ skip: Optional[int] = None,
1100
+ limit: Optional[int | str] = None,
1101
+ allow_dirty_read: Optional[bool] = False,
1102
+ sort: Optional[Jsons] = None,
1103
+ ) -> Result[Cursor]:
1104
+ """Return all documents that match the given filters.
1105
+
1106
+ Args:
1107
+ filters (dict | None): Query filters.
1108
+ skip (int | None): Number of documents to skip.
1109
+ limit (int | str | None): Maximum number of documents to return.
1110
+ allow_dirty_read (bool): Allow reads from followers in a cluster.
1111
+ sort (list | None): Document sort parameters.
1112
+
1113
+ Returns:
1114
+ Cursor: Document cursor.
1115
+
1116
+ Raises:
1117
+ DocumentGetError: If retrieval fails.
1118
+ SortValidationError: If sort parameters are invalid.
1119
+ """
1120
+ if not self._is_none_or_dict(filters):
1121
+ raise ValueError("filters parameter must be a dict")
1122
+ self._validate_sort_parameters(sort)
1123
+ if not self._is_none_or_int(skip):
1124
+ raise ValueError("skip parameter must be a non-negative int")
1125
+ if not (self._is_none_or_int(limit) or limit == "null"):
1126
+ raise ValueError("limit parameter must be a non-negative int")
1127
+
1128
+ skip = skip if skip is not None else 0
1129
+ limit = limit if limit is not None else "null"
1130
+ query = f"""
1131
+ FOR doc IN @@collection
1132
+ {self._build_filter_conditions(filters)}
1133
+ LIMIT {skip}, {limit}
1134
+ {self._build_sort_expression(sort)}
1135
+ RETURN doc
1136
+ """
1137
+ bind_vars = {"@collection": self.name}
1138
+ data: Json = {"query": query, "bindVars": bind_vars, "count": True}
1139
+ headers: RequestHeaders = {}
1140
+ if allow_dirty_read is not None:
1141
+ if allow_dirty_read is True:
1142
+ headers["x-arango-allow-dirty-read"] = "true"
1143
+ else:
1144
+ headers["x-arango-allow-dirty-read"] = "false"
1145
+
1146
+ request = Request(
1147
+ method=Method.POST,
1148
+ endpoint="/_api/cursor",
1149
+ data=self.serializer.dumps(data),
1150
+ headers=headers,
1151
+ )
1152
+
1153
+ def response_handler(resp: Response) -> Cursor:
1154
+ if not resp.is_success:
1155
+ raise DocumentGetError(resp, request)
1156
+ if self._executor.context == "async":
1157
+ # We cannot have a cursor giving back async jobs
1158
+ executor: NonAsyncExecutor = DefaultApiExecutor(
1159
+ self._executor.connection
1160
+ )
1161
+ else:
1162
+ executor = cast(NonAsyncExecutor, self._executor)
1163
+ return Cursor(executor, self.deserializer.loads(resp.raw_body))
1164
+
1165
+ return await self._executor.execute(request, response_handler)
1166
+
1167
+ async def update_match(
1168
+ self,
1169
+ filters: Json,
1170
+ body: T,
1171
+ limit: Optional[int | str] = None,
1172
+ keep_none: Optional[bool] = None,
1173
+ wait_for_sync: Optional[bool] = None,
1174
+ merge_objects: Optional[bool] = None,
1175
+ ) -> Result[int]:
1176
+ """Update matching documents.
1177
+
1178
+ Args:
1179
+ filters (dict | None): Query filters.
1180
+ body (dict): Full or partial document body with the updates.
1181
+ limit (int | str | None): Maximum number of documents to update.
1182
+ keep_none (bool | None): If set to `True`, fields with value `None` are
1183
+ retained in the document. Otherwise, they are removed completely.
1184
+ wait_for_sync (bool | None): Wait until operation has been synced to disk.
1185
+ merge_objects (bool | None): If set to `True`, sub-dictionaries are merged
1186
+ instead of the new one overwriting the old one.
1187
+
1188
+ Returns:
1189
+ int: Number of documents that got updated.
1190
+
1191
+ Raises:
1192
+ DocumentUpdateError: If update fails.
1193
+ """
1194
+ if not self._is_none_or_dict(filters):
1195
+ raise ValueError("filters parameter must be a dict")
1196
+ if not (self._is_none_or_int(limit) or limit == "null"):
1197
+ raise ValueError("limit parameter must be a non-negative int")
1198
+
1199
+ sync = f", waitForSync: {wait_for_sync}" if wait_for_sync is not None else ""
1200
+ query = f"""
1201
+ FOR doc IN @@collection
1202
+ {self._build_filter_conditions(filters)}
1203
+ {f"LIMIT {limit}" if limit is not None else ""}
1204
+ UPDATE doc WITH @body IN @@collection
1205
+ OPTIONS {{ keepNull: @keep_none, mergeObjects: @merge {sync} }}
1206
+ """ # noqa: E201 E202
1207
+ bind_vars = {
1208
+ "@collection": self.name,
1209
+ "body": body,
1210
+ "keep_none": keep_none,
1211
+ "merge": merge_objects,
1212
+ }
1213
+ data = {"query": query, "bindVars": bind_vars}
1214
+
1215
+ request = Request(
1216
+ method=Method.POST,
1217
+ endpoint="/_api/cursor",
1218
+ data=self.serializer.dumps(data),
1219
+ )
1220
+
1221
+ def response_handler(resp: Response) -> int:
1222
+ if resp.is_success:
1223
+ result = self.deserializer.loads(resp.raw_body)
1224
+ return cast(int, result["extra"]["stats"]["writesExecuted"])
1225
+ raise DocumentUpdateError(resp, request)
1226
+
1227
+ return await self._executor.execute(request, response_handler)
1228
+
1229
+ async def replace_match(
1230
+ self,
1231
+ filters: Json,
1232
+ body: T,
1233
+ limit: Optional[int | str] = None,
1234
+ wait_for_sync: Optional[bool] = None,
1235
+ ) -> Result[int]:
1236
+ """Replace matching documents.
1237
+
1238
+ Args:
1239
+ filters (dict | None): Query filters.
1240
+ body (dict): New document body.
1241
+ limit (int | str | None): Maximum number of documents to replace.
1242
+ wait_for_sync (bool | None): Wait until operation has been synced to disk.
1243
+
1244
+ Returns:
1245
+ int: Number of documents that got replaced.
1246
+
1247
+ Raises:
1248
+ DocumentReplaceError: If replace fails.
1249
+ """
1250
+ if not self._is_none_or_dict(filters):
1251
+ raise ValueError("filters parameter must be a dict")
1252
+ if not (self._is_none_or_int(limit) or limit == "null"):
1253
+ raise ValueError("limit parameter must be a non-negative int")
1254
+
1255
+ sync = f"waitForSync: {wait_for_sync}" if wait_for_sync is not None else ""
1256
+ query = f"""
1257
+ FOR doc IN @@collection
1258
+ {self._build_filter_conditions(filters)}
1259
+ {f"LIMIT {limit}" if limit is not None else ""}
1260
+ REPLACE doc WITH @body IN @@collection
1261
+ {f"OPTIONS {{ {sync} }}" if sync else ""}
1262
+ """ # noqa: E201 E202
1263
+ bind_vars = {
1264
+ "@collection": self.name,
1265
+ "body": body,
1266
+ }
1267
+ data = {"query": query, "bindVars": bind_vars}
1268
+
1269
+ request = Request(
1270
+ method=Method.POST,
1271
+ endpoint="/_api/cursor",
1272
+ data=self.serializer.dumps(data),
1273
+ )
1274
+
1275
+ def response_handler(resp: Response) -> int:
1276
+ if resp.is_success:
1277
+ result = self.deserializer.loads(resp.raw_body)
1278
+ return cast(int, result["extra"]["stats"]["writesExecuted"])
1279
+ raise DocumentReplaceError(resp, request)
1280
+
1281
+ return await self._executor.execute(request, response_handler)
1282
+
1283
+ async def delete_match(
1284
+ self,
1285
+ filters: Json,
1286
+ limit: Optional[int | str] = None,
1287
+ wait_for_sync: Optional[bool] = None,
1288
+ ) -> Result[int]:
1289
+ """Delete matching documents.
1290
+
1291
+ Args:
1292
+ filters (dict | None): Query filters.
1293
+ limit (int | str | None): Maximum number of documents to delete.
1294
+ wait_for_sync (bool | None): Wait until operation has been synced to disk.
1295
+
1296
+ Returns:
1297
+ int: Number of documents that got deleted.
1298
+
1299
+ Raises:
1300
+ DocumentDeleteError: If delete fails.
1301
+ """
1302
+ if not self._is_none_or_dict(filters):
1303
+ raise ValueError("filters parameter must be a dict")
1304
+ if not (self._is_none_or_int(limit) or limit == "null"):
1305
+ raise ValueError("limit parameter must be a non-negative int")
1306
+
1307
+ sync = f"waitForSync: {wait_for_sync}" if wait_for_sync is not None else ""
1308
+ query = f"""
1309
+ FOR doc IN @@collection
1310
+ {self._build_filter_conditions(filters)}
1311
+ {f"LIMIT {limit}" if limit is not None else ""}
1312
+ REMOVE doc IN @@collection
1313
+ {f"OPTIONS {{ {sync} }}" if sync else ""}
1314
+ """ # noqa: E201 E202
1315
+ bind_vars = {"@collection": self.name}
1316
+ data = {"query": query, "bindVars": bind_vars}
1317
+
1318
+ request = Request(
1319
+ method=Method.POST,
1320
+ endpoint="/_api/cursor",
1321
+ data=self.serializer.dumps(data),
1322
+ )
1323
+
1324
+ def response_handler(resp: Response) -> int:
1325
+ if resp.is_success:
1326
+ result = self.deserializer.loads(resp.raw_body)
1327
+ return cast(int, result["extra"]["stats"]["writesExecuted"])
1328
+ raise DocumentDeleteError(resp, request)
1329
+
1330
+ return await self._executor.execute(request, response_handler)
1331
+
1332
+ async def insert_many(
1333
+ self,
1334
+ documents: Sequence[T],
1335
+ wait_for_sync: Optional[bool] = None,
1336
+ return_new: Optional[bool] = None,
1337
+ return_old: Optional[bool] = None,
1338
+ silent: Optional[bool] = None,
1339
+ overwrite: Optional[bool] = None,
1340
+ overwrite_mode: Optional[str] = None,
1341
+ keep_null: Optional[bool] = None,
1342
+ merge_objects: Optional[bool] = None,
1343
+ refill_index_caches: Optional[bool] = None,
1344
+ version_attribute: Optional[str] = None,
1345
+ ) -> Result[Jsons]:
1346
+ """Insert multiple documents.
1347
+
1348
+ Note:
1349
+ If inserting a document fails, the exception is not raised but
1350
+ returned as an object in the "errors" list. It is up to you to
1351
+ inspect the list to determine which documents were inserted
1352
+ successfully (returns document metadata) and which were not
1353
+ (returns exception object).
1354
+
1355
+ Args:
1356
+ documents (list): Documents to insert. If an item contains the "_key" or
1357
+ "_id" field, the value is used as the key of the new document
1358
+ (otherwise it is auto-generated). Any "_rev" field is ignored.
1359
+ wait_for_sync (bool | None): Wait until documents have been synced to disk.
1360
+ return_new (bool | None): Additionally return the complete new document
1361
+ under the attribute `new` in the result.
1362
+ return_old (bool | None): Additionally return the complete old document
1363
+ under the attribute `old` in the result. Only available if the
1364
+ `overwrite` option is used.
1365
+ silent (bool | None): If set to `True`, an empty object is returned as
1366
+ response if all document operations succeed. No meta-data is returned
1367
+ for the created documents. If any of the operations raises an error,
1368
+ an array with the error object(s) is returned.
1369
+ overwrite (bool | None): If set to `True`, operation does not fail on
1370
+ duplicate key and existing document is overwritten (replace-insert).
1371
+ overwrite_mode (str | None): Overwrite mode. Supersedes **overwrite**
1372
+ option. May be one of "ignore", "replace", "update" or "conflict".
1373
+ keep_null (bool | None): If set to `True`, fields with value None are
1374
+ retained in the document. Otherwise, they are removed completely.
1375
+ Applies only when **overwrite_mode** is set to "update"
1376
+ (update-insert).
1377
+ merge_objects (bool | None): If set to `True`, sub-dictionaries are merged
1378
+ instead of the new one overwriting the old one. Applies only when
1379
+ **overwrite_mode** is set to "update" (update-insert).
1380
+ refill_index_caches (bool | None): Whether to add new entries to
1381
+ in-memory index caches if document operations affect the edge index
1382
+ or cache-enabled persistent indexes.
1383
+ version_attribute (str | None): Support for simple external versioning to
1384
+ document operations. Only applicable if **overwrite** is set to `True`
1385
+ or **overwrite_mode** is set to "update" or "replace".
1386
+
1387
+ Returns:
1388
+ list: Documents metadata (e.g. document id, key, revision) and
1389
+ errors or just errors if **silent** is set to `True`.
1390
+
1391
+ Raises:
1392
+ DocumentInsertError: If insertion fails.
1393
+
1394
+ References:
1395
+ - `create-multiple-documents <https://docs.arangodb.com/stable/develop/http-api/documents/#create-multiple-documents>`__
1396
+ """ # noqa: E501
1397
+ params: Params = {}
1398
+ if wait_for_sync is not None:
1399
+ params["waitForSync"] = wait_for_sync
1400
+ if return_new is not None:
1401
+ params["returnNew"] = return_new
1402
+ if return_old is not None:
1403
+ params["returnOld"] = return_old
1404
+ if silent is not None:
1405
+ params["silent"] = silent
1406
+ if overwrite is not None:
1407
+ params["overwrite"] = overwrite
1408
+ if overwrite_mode is not None:
1409
+ params["overwriteMode"] = overwrite_mode
1410
+ if keep_null is not None:
1411
+ params["keepNull"] = keep_null
1412
+ if merge_objects is not None:
1413
+ params["mergeObjects"] = merge_objects
1414
+ if refill_index_caches is not None:
1415
+ params["refillIndexCaches"] = refill_index_caches
1416
+ if version_attribute is not None:
1417
+ params["versionAttribute"] = version_attribute
1418
+
1419
+ request = Request(
1420
+ method=Method.POST,
1421
+ endpoint=f"/_api/document/{self.name}",
1422
+ data=self._doc_serializer.dumps(documents),
1423
+ params=params,
1424
+ )
1425
+
1426
+ def response_handler(
1427
+ resp: Response,
1428
+ ) -> Jsons:
1429
+ if not resp.is_success:
1430
+ raise DocumentInsertError(resp, request)
1431
+ return self.deserializer.loads_many(resp.raw_body)
1432
+
1433
+ return await self._executor.execute(request, response_handler)
1434
+
1435
+ async def replace_many(
1436
+ self,
1437
+ documents: Sequence[T],
1438
+ wait_for_sync: Optional[bool] = None,
1439
+ ignore_revs: Optional[bool] = None,
1440
+ return_new: Optional[bool] = None,
1441
+ return_old: Optional[bool] = None,
1442
+ silent: Optional[bool] = None,
1443
+ refill_index_caches: Optional[bool] = None,
1444
+ version_attribute: Optional[str] = None,
1445
+ ) -> Result[Jsons]:
1446
+ """Insert multiple documents.
1447
+
1448
+ Note:
1449
+ If replacing a document fails, the exception is not raised but
1450
+ returned as an object in the "errors" list. It is up to you to
1451
+ inspect the list to determine which documents were replaced
1452
+ successfully (returns document metadata) and which were not
1453
+ (returns exception object).
1454
+
1455
+ Args:
1456
+ documents (list): New documents to replace the old ones. An item must
1457
+ contain the "_key" or "_id" field.
1458
+ wait_for_sync (bool | None): Wait until documents have been synced to disk.
1459
+ ignore_revs (bool | None): If this is set to `False`, then any `_rev`
1460
+ attribute given in a body document is taken as a precondition. The
1461
+ document is only replaced if the current revision is the one
1462
+ specified.
1463
+ return_new (bool | None): Additionally return the complete new document
1464
+ under the attribute `new` in the result.
1465
+ return_old (bool | None): Additionally return the complete old document
1466
+ under the attribute `old` in the result.
1467
+ silent (bool | None): If set to `True`, an empty object is returned as
1468
+ response if all document operations succeed. No meta-data is returned
1469
+ for the created documents. If any of the operations raises an error,
1470
+ an array with the error object(s) is returned.
1471
+ refill_index_caches (bool | None): Whether to add new entries to
1472
+ in-memory index caches if document operations affect the edge index
1473
+ or cache-enabled persistent indexes.
1474
+ version_attribute (str | None): Support for simple external versioning to
1475
+ document operations.
1476
+
1477
+ Returns:
1478
+ list: Documents metadata (e.g. document id, key, revision) and
1479
+ errors or just errors if **silent** is set to `True`.
1480
+
1481
+ Raises:
1482
+ DocumentReplaceError: If replacing fails.
1483
+
1484
+ References:
1485
+ - `replace-multiple-documents <https://docs.arangodb.com/stable/develop/http-api/documents/#replace-multiple-documents>`__
1486
+ """ # noqa: E501
1487
+ params: Params = {}
1488
+ if wait_for_sync is not None:
1489
+ params["waitForSync"] = wait_for_sync
1490
+ if ignore_revs is not None:
1491
+ params["ignoreRevs"] = ignore_revs
1492
+ if return_new is not None:
1493
+ params["returnNew"] = return_new
1494
+ if return_old is not None:
1495
+ params["returnOld"] = return_old
1496
+ if silent is not None:
1497
+ params["silent"] = silent
1498
+ if refill_index_caches is not None:
1499
+ params["refillIndexCaches"] = refill_index_caches
1500
+ if version_attribute is not None:
1501
+ params["versionAttribute"] = version_attribute
1502
+
1503
+ request = Request(
1504
+ method=Method.PUT,
1505
+ endpoint=f"/_api/document/{self.name}",
1506
+ data=self._doc_serializer.dumps(documents),
1507
+ params=params,
1508
+ )
1509
+
1510
+ def response_handler(
1511
+ resp: Response,
1512
+ ) -> Jsons:
1513
+ if not resp.is_success:
1514
+ raise DocumentReplaceError(resp, request)
1515
+ return self.deserializer.loads_many(resp.raw_body)
1516
+
1517
+ return await self._executor.execute(request, response_handler)
1518
+
1519
+ async def update_many(
1520
+ self,
1521
+ documents: Sequence[T],
1522
+ wait_for_sync: Optional[bool] = None,
1523
+ ignore_revs: Optional[bool] = None,
1524
+ return_new: Optional[bool] = None,
1525
+ return_old: Optional[bool] = None,
1526
+ silent: Optional[bool] = None,
1527
+ keep_null: Optional[bool] = None,
1528
+ merge_objects: Optional[bool] = None,
1529
+ refill_index_caches: Optional[bool] = None,
1530
+ version_attribute: Optional[str] = None,
1531
+ ) -> Result[Jsons]:
1532
+ """Insert multiple documents.
1533
+
1534
+ Note:
1535
+ If updating a document fails, the exception is not raised but
1536
+ returned as an object in the "errors" list. It is up to you to
1537
+ inspect the list to determine which documents were updated
1538
+ successfully (returned as document metadata) and which were not
1539
+ (returned as exception object).
1540
+
1541
+ Args:
1542
+ documents (list): Documents to update. An item must contain the "_key" or
1543
+ "_id" field.
1544
+ wait_for_sync (bool | None): Wait until documents have been synced to disk.
1545
+ ignore_revs (bool | None): If this is set to `False`, then any `_rev`
1546
+ attribute given in a body document is taken as a precondition. The
1547
+ document is only updated if the current revision is the one
1548
+ specified.
1549
+ return_new (bool | None): Additionally return the complete new document
1550
+ under the attribute `new` in the result.
1551
+ return_old (bool | None): Additionally return the complete old document
1552
+ under the attribute `old` in the result.
1553
+ silent (bool | None): If set to `True`, an empty object is returned as
1554
+ response if all document operations succeed. No meta-data is returned
1555
+ for the created documents. If any of the operations raises an error,
1556
+ an array with the error object(s) is returned.
1557
+ keep_null (bool | None): If set to `True`, fields with value None are
1558
+ retained in the document. Otherwise, they are removed completely.
1559
+ Applies only when **overwrite_mode** is set to "update"
1560
+ (update-insert).
1561
+ merge_objects (bool | None): If set to `True`, sub-dictionaries are merged
1562
+ instead of the new one overwriting the old one. Applies only when
1563
+ **overwrite_mode** is set to "update" (update-insert).
1564
+ refill_index_caches (bool | None): Whether to add new entries to
1565
+ in-memory index caches if document operations affect the edge index
1566
+ or cache-enabled persistent indexes.
1567
+ version_attribute (str | None): Support for simple external versioning to
1568
+ document operations.
1569
+
1570
+ Returns:
1571
+ list: Documents metadata (e.g. document id, key, revision) and
1572
+ errors or just errors if **silent** is set to `True`.
1573
+
1574
+ Raises:
1575
+ DocumentUpdateError: If update fails.
1576
+
1577
+ References:
1578
+ - `update-multiple-documents <https://docs.arangodb.com/stable/develop/http-api/documents/#update-multiple-documents>`__
1579
+ """ # noqa: E501
1580
+ params: Params = {}
1581
+ if wait_for_sync is not None:
1582
+ params["waitForSync"] = wait_for_sync
1583
+ if ignore_revs is not None:
1584
+ params["ignoreRevs"] = ignore_revs
1585
+ if return_new is not None:
1586
+ params["returnNew"] = return_new
1587
+ if return_old is not None:
1588
+ params["returnOld"] = return_old
1589
+ if silent is not None:
1590
+ params["silent"] = silent
1591
+ if keep_null is not None:
1592
+ params["keepNull"] = keep_null
1593
+ if merge_objects is not None:
1594
+ params["mergeObjects"] = merge_objects
1595
+ if refill_index_caches is not None:
1596
+ params["refillIndexCaches"] = refill_index_caches
1597
+ if version_attribute is not None:
1598
+ params["versionAttribute"] = version_attribute
1599
+
1600
+ request = Request(
1601
+ method=Method.PATCH,
1602
+ endpoint=f"/_api/document/{self.name}",
1603
+ data=self._doc_serializer.dumps(documents),
1604
+ params=params,
1605
+ )
1606
+
1607
+ def response_handler(
1608
+ resp: Response,
1609
+ ) -> Jsons:
1610
+ if not resp.is_success:
1611
+ raise DocumentUpdateError(resp, request)
1612
+ return self.deserializer.loads_many(resp.raw_body)
1613
+
1614
+ return await self._executor.execute(request, response_handler)
1615
+
1616
+ async def delete_many(
1617
+ self,
1618
+ documents: Sequence[T],
1619
+ wait_for_sync: Optional[bool] = None,
1620
+ ignore_revs: Optional[bool] = None,
1621
+ return_old: Optional[bool] = None,
1622
+ silent: Optional[bool] = None,
1623
+ refill_index_caches: Optional[bool] = None,
1624
+ ) -> Result[Jsons]:
1625
+ """Delete multiple documents.
1626
+
1627
+ Note:
1628
+ If deleting a document fails, the exception is not raised but
1629
+ returned as an object in the "errors" list. It is up to you to
1630
+ inspect the list to determine which documents were deleted
1631
+ successfully (returned as document metadata) and which were not
1632
+ (returned as exception object).
1633
+
1634
+ Args:
1635
+ documents (list): Documents to delete. An item must contain the "_key" or
1636
+ "_id" field.
1637
+ wait_for_sync (bool | None): Wait until documents have been synced to disk.
1638
+ ignore_revs (bool | None): If this is set to `False`, then any `_rev`
1639
+ attribute given in a body document is taken as a precondition. The
1640
+ document is only updated if the current revision is the one
1641
+ specified.
1642
+ return_old (bool | None): Additionally return the complete old document
1643
+ under the attribute `old` in the result.
1644
+ silent (bool | None): If set to `True`, an empty object is returned as
1645
+ response if all document operations succeed. No meta-data is returned
1646
+ for the created documents. If any of the operations raises an error,
1647
+ an array with the error object(s) is returned.
1648
+ refill_index_caches (bool | None): Whether to add new entries to
1649
+ in-memory index caches if document operations affect the edge index
1650
+ or cache-enabled persistent indexes.
1651
+
1652
+ Returns:
1653
+ list: Documents metadata (e.g. document id, key, revision) and
1654
+ errors or just errors if **silent** is set to `True`.
1655
+
1656
+ Raises:
1657
+ DocumentRemoveError: If removal fails.
1658
+
1659
+ References:
1660
+ - `remove-multiple-documents <https://docs.arangodb.com/stable/develop/http-api/documents/#remove-multiple-documents>`__
1661
+ """ # noqa: E501
1662
+ params: Params = {}
1663
+ if wait_for_sync is not None:
1664
+ params["waitForSync"] = wait_for_sync
1665
+ if ignore_revs is not None:
1666
+ params["ignoreRevs"] = ignore_revs
1667
+ if return_old is not None:
1668
+ params["returnOld"] = return_old
1669
+ if silent is not None:
1670
+ params["silent"] = silent
1671
+ if refill_index_caches is not None:
1672
+ params["refillIndexCaches"] = refill_index_caches
1673
+
1674
+ request = Request(
1675
+ method=Method.DELETE,
1676
+ endpoint=f"/_api/document/{self.name}",
1677
+ data=self._doc_serializer.dumps(documents),
1678
+ params=params,
1679
+ )
1680
+
1681
+ def response_handler(
1682
+ resp: Response,
1683
+ ) -> Jsons:
1684
+ if not resp.is_success:
1685
+ raise DocumentDeleteError(resp, request)
1686
+ return self.deserializer.loads_many(resp.raw_body)
1687
+
1688
+ return await self._executor.execute(request, response_handler)