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,1533 @@
1
+ __all__ = [
2
+ "Database",
3
+ "StandardDatabase",
4
+ "TransactionDatabase",
5
+ "AsyncDatabase",
6
+ ]
7
+
8
+
9
+ from typing import Any, List, Optional, Sequence, TypeVar, cast
10
+ from warnings import warn
11
+
12
+ from arangoasync.aql import AQL
13
+ from arangoasync.collection import StandardCollection
14
+ from arangoasync.connection import Connection
15
+ from arangoasync.errno import HTTP_FORBIDDEN, HTTP_NOT_FOUND
16
+ from arangoasync.exceptions import (
17
+ AsyncJobClearError,
18
+ AsyncJobListError,
19
+ CollectionCreateError,
20
+ CollectionDeleteError,
21
+ CollectionListError,
22
+ DatabaseCreateError,
23
+ DatabaseDeleteError,
24
+ DatabaseListError,
25
+ DatabasePropertiesError,
26
+ JWTSecretListError,
27
+ JWTSecretReloadError,
28
+ PermissionGetError,
29
+ PermissionListError,
30
+ PermissionResetError,
31
+ PermissionUpdateError,
32
+ ServerStatusError,
33
+ ServerVersionError,
34
+ TransactionAbortError,
35
+ TransactionCommitError,
36
+ TransactionExecuteError,
37
+ TransactionInitError,
38
+ TransactionListError,
39
+ TransactionStatusError,
40
+ UserCreateError,
41
+ UserDeleteError,
42
+ UserGetError,
43
+ UserListError,
44
+ UserReplaceError,
45
+ UserUpdateError,
46
+ )
47
+ from arangoasync.executor import (
48
+ ApiExecutor,
49
+ AsyncApiExecutor,
50
+ DefaultApiExecutor,
51
+ TransactionApiExecutor,
52
+ )
53
+ from arangoasync.request import Method, Request
54
+ from arangoasync.response import Response
55
+ from arangoasync.result import Result
56
+ from arangoasync.serialization import Deserializer, Serializer
57
+ from arangoasync.typings import (
58
+ CollectionInfo,
59
+ CollectionType,
60
+ DatabaseProperties,
61
+ Json,
62
+ Jsons,
63
+ KeyOptions,
64
+ Params,
65
+ ServerStatusInformation,
66
+ UserInfo,
67
+ )
68
+
69
+ T = TypeVar("T")
70
+ U = TypeVar("U")
71
+ V = TypeVar("V")
72
+
73
+
74
+ class Database:
75
+ """Database API.
76
+
77
+ Args:
78
+ executor: API executor.
79
+ Responsible for executing requests and handling responses.
80
+ """
81
+
82
+ def __init__(self, executor: ApiExecutor) -> None:
83
+ self._executor = executor
84
+
85
+ @property
86
+ def connection(self) -> Connection:
87
+ """Return the HTTP connection."""
88
+ return self._executor.connection
89
+
90
+ @property
91
+ def name(self) -> str:
92
+ """Return the name of the current database."""
93
+ return self._executor.db_name
94
+
95
+ @property
96
+ def serializer(self) -> Serializer[Json]:
97
+ """Return the serializer."""
98
+ return self._executor.serializer
99
+
100
+ @property
101
+ def deserializer(self) -> Deserializer[Json, Jsons]:
102
+ """Return the deserializer."""
103
+ return self._executor.deserializer
104
+
105
+ @property
106
+ def context(self) -> str:
107
+ """Return the API execution context.
108
+
109
+ Returns:
110
+ str: API execution context. Possible values are "default", "transaction".
111
+ """
112
+ return self._executor.context
113
+
114
+ @property
115
+ def aql(self) -> AQL:
116
+ """Return the AQL API wrapper.
117
+
118
+ Returns:
119
+ arangoasync.aql.AQL: AQL API wrapper.
120
+ """
121
+ return AQL(self._executor)
122
+
123
+ async def properties(self) -> Result[DatabaseProperties]:
124
+ """Return database properties.
125
+
126
+ Returns:
127
+ DatabaseProperties: Properties of the current database.
128
+
129
+ Raises:
130
+ DatabasePropertiesError: If retrieval fails.
131
+
132
+ References:
133
+ - `get-information-about-the-current-database <https://docs.arangodb.com/stable/develop/http-api/databases/#get-information-about-the-current-database>`__
134
+ """ # noqa: E501
135
+ request = Request(method=Method.GET, endpoint="/_api/database/current")
136
+
137
+ def response_handler(resp: Response) -> DatabaseProperties:
138
+ if not resp.is_success:
139
+ raise DatabasePropertiesError(resp, request)
140
+ return DatabaseProperties(
141
+ self.deserializer.loads(resp.raw_body), strip_result=True
142
+ )
143
+
144
+ return await self._executor.execute(request, response_handler)
145
+
146
+ async def status(self) -> Result[ServerStatusInformation]:
147
+ """Query the server status.
148
+
149
+ Returns:
150
+ ServerStatusInformation: Server status.
151
+
152
+ Raises:
153
+ ServerSatusError: If retrieval fails.
154
+
155
+ References:
156
+ - `get-server-status-information <https://docs.arangodb.com/stable/develop/http-api/administration/#get-server-status-information>`__
157
+ """ # noqa: E501
158
+ request = Request(method=Method.GET, endpoint="/_admin/status")
159
+
160
+ def response_handler(resp: Response) -> ServerStatusInformation:
161
+ if not resp.is_success:
162
+ raise ServerStatusError(resp, request)
163
+ return ServerStatusInformation(self.deserializer.loads(resp.raw_body))
164
+
165
+ return await self._executor.execute(request, response_handler)
166
+
167
+ async def databases(self) -> Result[List[str]]:
168
+ """Return the names of all databases.
169
+
170
+ Note:
171
+ This method can only be executed in the **_system** database.
172
+
173
+ Returns:
174
+ list: Database names.
175
+
176
+ Raises:
177
+ DatabaseListError: If retrieval fails.
178
+
179
+ References:
180
+ - `list-all-databases <https://docs.arangodb.com/stable/develop/http-api/databases/#list-all-databases>`__
181
+ """ # noqa: E501
182
+ request = Request(method=Method.GET, endpoint="/_api/database")
183
+
184
+ def response_handler(resp: Response) -> List[str]:
185
+ if resp.is_success:
186
+ body = self.deserializer.loads(resp.raw_body)
187
+ return cast(List[str], body["result"])
188
+ msg: Optional[str] = None
189
+ if resp.status_code == HTTP_FORBIDDEN:
190
+ msg = "This request can only be executed in the _system database."
191
+ raise DatabaseListError(resp, request, msg)
192
+
193
+ return await self._executor.execute(request, response_handler)
194
+
195
+ async def databases_accessible_to_user(self) -> Result[List[str]]:
196
+ """Return the names of all databases accessible to the current user.
197
+
198
+ Note:
199
+ This method can only be executed in the **_system** database.
200
+
201
+ Returns:
202
+ list: Database names.
203
+
204
+ Raises:
205
+ DatabaseListError: If retrieval fails.
206
+
207
+ References:
208
+ - `list-the-accessible-databases <https://docs.arangodb.com/stable/develop/http-api/databases/#list-the-accessible-databases>`__
209
+ """ # noqa: E501
210
+ request = Request(method=Method.GET, endpoint="/_api/database/user")
211
+
212
+ def response_handler(resp: Response) -> List[str]:
213
+ if resp.is_success:
214
+ body = self.deserializer.loads(resp.raw_body)
215
+ return cast(List[str], body["result"])
216
+ raise DatabaseListError(resp, request)
217
+
218
+ return await self._executor.execute(request, response_handler)
219
+
220
+ async def has_database(self, name: str) -> Result[bool]:
221
+ """Check if a database exists.
222
+
223
+ Note:
224
+ This method can only be executed from within the **_system** database.
225
+
226
+ Args:
227
+ name (str): Database name.
228
+
229
+ Returns:
230
+ bool: `True` if the database exists, `False` otherwise.
231
+
232
+ Raises:
233
+ DatabaseListError: If retrieval fails.
234
+ """
235
+ request = Request(method=Method.GET, endpoint="/_api/database")
236
+
237
+ def response_handler(resp: Response) -> bool:
238
+ if resp.is_success:
239
+ body = self.deserializer.loads(resp.raw_body)
240
+ return name in body["result"]
241
+ msg: Optional[str] = None
242
+ if resp.status_code == HTTP_FORBIDDEN:
243
+ msg = "This request can only be executed in the _system database."
244
+ raise DatabaseListError(resp, request, msg)
245
+
246
+ return await self._executor.execute(request, response_handler)
247
+
248
+ async def create_database(
249
+ self,
250
+ name: str,
251
+ users: Optional[Sequence[Json | UserInfo]] = None,
252
+ replication_factor: Optional[int | str] = None,
253
+ write_concern: Optional[int] = None,
254
+ sharding: Optional[bool] = None,
255
+ ) -> Result[bool]:
256
+ """Create a new database.
257
+
258
+ Note:
259
+ This method can only be executed from within the **_system** database.
260
+
261
+ Args:
262
+ name (str): Database name.
263
+ users (list | None): Optional list of users with access to the new
264
+ database, where each user is of :class:`User
265
+ <arangoasync.wrapper.UserInfo>` type, or a dictionary with fields
266
+ "username", "password" and "active". If not set, the default user
267
+ **root** will be used to ensure that the new database will be
268
+ accessible after it is created.
269
+ replication_factor (int | str | None): Default replication factor for new
270
+ collections created in this database. Special values include
271
+ “satellite”, which will replicate the collection to every DB-Server
272
+ (Enterprise Edition only), and 1, which disables replication. Used
273
+ for clusters only.
274
+ write_concern (int | None): Default write concern for collections created
275
+ in this database. Determines how many copies of each shard are required
276
+ to be in sync on different DB-Servers. If there are less than these many
277
+ copies in the cluster a shard will refuse to write. Writes to shards with
278
+ enough up-to-date copies will succeed at the same time, however. Value of
279
+ this parameter can not be larger than the value of **replication_factor**.
280
+ Used for clusters only.
281
+ sharding (str | None): Sharding method used for new collections in this
282
+ database. Allowed values are: "", "flexible" and "single". The first
283
+ two are equivalent. Used for clusters only.
284
+
285
+ Returns:
286
+ bool: True if the database was created successfully.
287
+
288
+ Raises:
289
+ DatabaseCreateError: If creation fails.
290
+
291
+ References:
292
+ - `create-a-database <https://docs.arangodb.com/stable/develop/http-api/databases/#create-a-database>`__
293
+ """ # noqa: E501
294
+ data: Json = {"name": name}
295
+
296
+ options: Json = {}
297
+ if replication_factor is not None:
298
+ options["replicationFactor"] = replication_factor
299
+ if write_concern is not None:
300
+ options["writeConcern"] = write_concern
301
+ if sharding is not None:
302
+ options["sharding"] = sharding
303
+ if options:
304
+ data["options"] = options
305
+
306
+ if users is not None:
307
+ data["users"] = [
308
+ {
309
+ "username": user["user"] if "user" in user else user["username"],
310
+ "passwd": user["password"],
311
+ "active": user.get("active", True),
312
+ "extra": user.get("extra", {}),
313
+ }
314
+ for user in users
315
+ ]
316
+
317
+ request = Request(
318
+ method=Method.POST,
319
+ endpoint="/_api/database",
320
+ data=self.serializer.dumps(data),
321
+ )
322
+
323
+ def response_handler(resp: Response) -> bool:
324
+ if resp.is_success:
325
+ return True
326
+ msg: Optional[str] = None
327
+ if resp.status_code == HTTP_FORBIDDEN:
328
+ msg = "This request can only be executed in the _system database."
329
+ raise DatabaseCreateError(resp, request, msg)
330
+
331
+ return await self._executor.execute(request, response_handler)
332
+
333
+ async def delete_database(
334
+ self, name: str, ignore_missing: bool = False
335
+ ) -> Result[bool]:
336
+ """Delete a database.
337
+
338
+ Note:
339
+ This method can only be executed from within the **_system** database.
340
+
341
+ Args:
342
+ name (str): Database name.
343
+ ignore_missing (bool): Do not raise an exception on missing database.
344
+
345
+ Returns:
346
+ bool: True if the database was deleted successfully, `False` if the
347
+ database was not found but **ignore_missing** was set to `True`.
348
+
349
+ Raises:
350
+ DatabaseDeleteError: If deletion fails.
351
+
352
+ References:
353
+ - `drop-a-database <https://docs.arangodb.com/stable/develop/http-api/databases/#drop-a-database>`__
354
+ """ # noqa: E501
355
+ request = Request(method=Method.DELETE, endpoint=f"/_api/database/{name}")
356
+
357
+ def response_handler(resp: Response) -> bool:
358
+ if resp.is_success:
359
+ return True
360
+ if resp.status_code == HTTP_NOT_FOUND and ignore_missing:
361
+ return False
362
+ msg: Optional[str] = None
363
+ if resp.status_code == HTTP_FORBIDDEN:
364
+ msg = "This request can only be executed in the _system database."
365
+ raise DatabaseDeleteError(resp, request, msg)
366
+
367
+ return await self._executor.execute(request, response_handler)
368
+
369
+ def collection(
370
+ self,
371
+ name: str,
372
+ doc_serializer: Optional[Serializer[T]] = None,
373
+ doc_deserializer: Optional[Deserializer[U, V]] = None,
374
+ ) -> StandardCollection[T, U, V]:
375
+ """Return the collection API wrapper.
376
+
377
+ Args:
378
+ name (str): Collection name.
379
+ doc_serializer (Serializer): Custom document serializer.
380
+ This will be used only for document operations.
381
+ doc_deserializer (Deserializer): Custom document deserializer.
382
+ This will be used only for document operations.
383
+
384
+ Returns:
385
+ StandardCollection: Collection API wrapper.
386
+ """
387
+ if doc_serializer is None:
388
+ serializer = cast(Serializer[T], self.serializer)
389
+ else:
390
+ serializer = doc_serializer
391
+ if doc_deserializer is None:
392
+ deserializer = cast(Deserializer[U, V], self.deserializer)
393
+ else:
394
+ deserializer = doc_deserializer
395
+
396
+ return StandardCollection[T, U, V](
397
+ self._executor, name, serializer, deserializer
398
+ )
399
+
400
+ async def collections(
401
+ self,
402
+ exclude_system: Optional[bool] = None,
403
+ ) -> Result[List[CollectionInfo]]:
404
+ """Returns basic information for all collections in the current database,
405
+ optionally excluding system collections.
406
+
407
+ Returns:
408
+ list: Collection names.
409
+
410
+ Raises:
411
+ CollectionListError: If retrieval fails.
412
+
413
+ References:
414
+ - `list-all-collections <https://docs.arangodb.com/stable/develop/http-api/collections/#list-all-collections>`__
415
+ """ # noqa: E501
416
+ params: Params = {}
417
+ if exclude_system is not None:
418
+ params["excludeSystem"] = exclude_system
419
+
420
+ request = Request(
421
+ method=Method.GET,
422
+ endpoint="/_api/collection",
423
+ params=params,
424
+ )
425
+
426
+ def response_handler(resp: Response) -> List[CollectionInfo]:
427
+ if not resp.is_success:
428
+ raise CollectionListError(resp, request)
429
+ body = self.deserializer.loads(resp.raw_body)
430
+ return [CollectionInfo(c) for c in body["result"]]
431
+
432
+ return await self._executor.execute(request, response_handler)
433
+
434
+ async def has_collection(self, name: str) -> Result[bool]:
435
+ """Check if a collection exists in the database.
436
+
437
+ Args:
438
+ name (str): Collection name.
439
+
440
+ Returns:
441
+ bool: True if the collection exists, False otherwise.
442
+
443
+ Raises:
444
+ CollectionListError: If retrieval fails.
445
+ """
446
+ request = Request(method=Method.GET, endpoint=f"/_api/collection/{name}")
447
+
448
+ def response_handler(resp: Response) -> bool:
449
+ if resp.is_success:
450
+ return True
451
+ if resp.status_code == HTTP_NOT_FOUND:
452
+ return False
453
+ raise CollectionListError(resp, request)
454
+
455
+ return await self._executor.execute(request, response_handler)
456
+
457
+ async def create_collection(
458
+ self,
459
+ name: str,
460
+ doc_serializer: Optional[Serializer[T]] = None,
461
+ doc_deserializer: Optional[Deserializer[U, V]] = None,
462
+ col_type: Optional[CollectionType] = None,
463
+ write_concern: Optional[int] = None,
464
+ wait_for_sync: Optional[bool] = None,
465
+ number_of_shards: Optional[int] = None,
466
+ replication_factor: Optional[int] = None,
467
+ cache_enabled: Optional[bool] = None,
468
+ computed_values: Optional[Jsons] = None,
469
+ distribute_shards_like: Optional[str] = None,
470
+ is_system: Optional[bool] = False,
471
+ key_options: Optional[KeyOptions | Json] = None,
472
+ schema: Optional[Json] = None,
473
+ shard_keys: Optional[Sequence[str]] = None,
474
+ sharding_strategy: Optional[str] = None,
475
+ smart_graph_attribute: Optional[str] = None,
476
+ smart_join_attribute: Optional[str] = None,
477
+ wait_for_sync_replication: Optional[bool] = None,
478
+ enforce_replication_factor: Optional[bool] = None,
479
+ ) -> Result[StandardCollection[T, U, V]]:
480
+ """Create a new collection.
481
+
482
+ Args:
483
+ name (str): Collection name.
484
+ doc_serializer (Serializer): Custom document serializer.
485
+ This will be used only for document operations.
486
+ doc_deserializer (Deserializer): Custom document deserializer.
487
+ This will be used only for document operations.
488
+ col_type (CollectionType | int | str | None): Collection type.
489
+ write_concern (int | None): Determines how many copies of each shard are
490
+ required to be in sync on the different DB-Servers.
491
+ wait_for_sync (bool | None): If `True`, the data is synchronised to disk
492
+ before returning from a document create, update, replace or removal
493
+ operation.
494
+ number_of_shards (int | None): In a cluster, this value determines the
495
+ number of shards to create for the collection.
496
+ replication_factor (int | None): In a cluster, this attribute determines
497
+ how many copies of each shard are kept on different DB-Servers.
498
+ cache_enabled (bool | None): Whether the in-memory hash cache for
499
+ documents should be enabled for this collection.
500
+ computed_values (Jsons | None): An optional list of objects, each
501
+ representing a computed value.
502
+ distribute_shards_like (str | None): The name of another collection.
503
+ If this property is set in a cluster, the collection copies the
504
+ replicationFactor, numberOfShards and shardingStrategy properties
505
+ from the specified collection (referred to as the prototype
506
+ collection) and distributes the shards of this collection in the same
507
+ way as the shards of the other collection.
508
+ is_system (bool | None): If `True`, create a system collection.
509
+ In this case, the collection name should start with an underscore.
510
+ key_options (KeyOptions | dict | None): Additional options for key
511
+ generation. You may use a :class:`KeyOptions
512
+ <arangoasync.wrapper.KeyOptions>` object for easier configuration,
513
+ or pass a dictionary directly.
514
+ schema (dict | None): Optional object that specifies the collection
515
+ level schema for documents.
516
+ shard_keys (list | None): In a cluster, this attribute determines which
517
+ document attributes are used to determine the target shard for
518
+ documents.
519
+ sharding_strategy (str | None): Name of the sharding strategy.
520
+ smart_graph_attribute: (str | None): The attribute that is used for
521
+ sharding: vertices with the same value of this attribute are placed
522
+ in the same shard.
523
+ smart_join_attribute: (str | None): Determines an attribute of the
524
+ collection that must contain the shard key value of the referred-to
525
+ SmartJoin collection.
526
+ wait_for_sync_replication (bool | None): If `True`, the server only
527
+ reports success back to the client when all replicas have created
528
+ the collection. Set it to `False` if you want faster server
529
+ responses and don’t care about full replication.
530
+ enforce_replication_factor (bool | None): If `True`, the server checks
531
+ if there are enough replicas available at creation time and bail out
532
+ otherwise. Set it to `False` to disable this extra check.
533
+
534
+ Returns:
535
+ StandardCollection: Collection API wrapper.
536
+
537
+ Raises:
538
+ ValueError: If parameters are invalid.
539
+ CollectionCreateError: If the operation fails.
540
+
541
+ References:
542
+ - `create-a-collection <https://docs.arangodb.com/stable/develop/http-api/collections/#create-a-collection>`__
543
+ """ # noqa: E501
544
+ data: Json = {"name": name}
545
+ if col_type is not None:
546
+ if isinstance(col_type, int):
547
+ col_type = CollectionType.from_int(col_type)
548
+ elif isinstance(col_type, str):
549
+ col_type = CollectionType.from_str(col_type)
550
+ elif not isinstance(col_type, CollectionType):
551
+ raise ValueError("Invalid collection type")
552
+ data["type"] = col_type.value
553
+ if write_concern is not None:
554
+ data["writeConcern"] = write_concern
555
+ if wait_for_sync is not None:
556
+ data["waitForSync"] = wait_for_sync
557
+ if number_of_shards is not None:
558
+ data["numberOfShards"] = number_of_shards
559
+ if replication_factor is not None:
560
+ data["replicationFactor"] = replication_factor
561
+ if cache_enabled is not None:
562
+ data["cacheEnabled"] = cache_enabled
563
+ if computed_values is not None:
564
+ data["computedValues"] = computed_values
565
+ if distribute_shards_like is not None:
566
+ data["distributeShardsLike"] = distribute_shards_like
567
+ if is_system is not None:
568
+ data["isSystem"] = is_system
569
+ if key_options is not None:
570
+ if isinstance(key_options, dict):
571
+ key_options = KeyOptions(data=key_options)
572
+ key_options.validate()
573
+ data["keyOptions"] = key_options.to_dict()
574
+ if schema is not None:
575
+ data["schema"] = schema
576
+ if shard_keys is not None:
577
+ data["shardKeys"] = shard_keys
578
+ if sharding_strategy is not None:
579
+ data["shardingStrategy"] = sharding_strategy
580
+ if smart_graph_attribute is not None:
581
+ data["smartGraphAttribute"] = smart_graph_attribute
582
+ if smart_join_attribute is not None:
583
+ data["smartJoinAttribute"] = smart_join_attribute
584
+
585
+ params: Params = {}
586
+ if wait_for_sync_replication is not None:
587
+ params["waitForSyncReplication"] = wait_for_sync_replication
588
+ if enforce_replication_factor is not None:
589
+ params["enforceReplicationFactor"] = enforce_replication_factor
590
+
591
+ request = Request(
592
+ method=Method.POST,
593
+ endpoint="/_api/collection",
594
+ data=self.serializer.dumps(data),
595
+ params=params,
596
+ )
597
+
598
+ def response_handler(resp: Response) -> StandardCollection[T, U, V]:
599
+ nonlocal doc_serializer, doc_deserializer
600
+ if not resp.is_success:
601
+ raise CollectionCreateError(resp, request)
602
+ if doc_serializer is None:
603
+ serializer = cast(Serializer[T], self.serializer)
604
+ else:
605
+ serializer = doc_serializer
606
+ if doc_deserializer is None:
607
+ deserializer = cast(Deserializer[U, V], self.deserializer)
608
+ else:
609
+ deserializer = doc_deserializer
610
+ return StandardCollection[T, U, V](
611
+ self._executor, name, serializer, deserializer
612
+ )
613
+
614
+ return await self._executor.execute(request, response_handler)
615
+
616
+ async def delete_collection(
617
+ self,
618
+ name: str,
619
+ ignore_missing: bool = False,
620
+ is_system: Optional[bool] = None,
621
+ ) -> Result[bool]:
622
+ """Delete a collection.
623
+
624
+ Args:
625
+ name (str): Collection name.
626
+ ignore_missing (bool): Do not raise an exception on missing collection.
627
+ is_system (bool | None): Whether to drop a system collection. This parameter
628
+ must be set to `True` in order to drop a system collection.
629
+
630
+ Returns:
631
+ bool: True if the collection was deleted successfully, `False` if the
632
+ collection was not found but **ignore_missing** was set to `True`.
633
+
634
+ Raises:
635
+ CollectionDeleteError: If the operation fails.
636
+
637
+ References:
638
+ - `drop-a-collection <https://docs.arangodb.com/stable/develop/http-api/collections/#drop-a-collection>`__
639
+ """ # noqa: E501
640
+ params: Params = {}
641
+ if is_system is not None:
642
+ params["isSystem"] = is_system
643
+
644
+ request = Request(
645
+ method=Method.DELETE,
646
+ endpoint=f"/_api/collection/{name}",
647
+ params=params,
648
+ )
649
+
650
+ def response_handler(resp: Response) -> bool:
651
+ nonlocal ignore_missing
652
+ if resp.is_success:
653
+ return True
654
+ if resp.status_code == HTTP_NOT_FOUND and ignore_missing:
655
+ return False
656
+ raise CollectionDeleteError(resp, request)
657
+
658
+ return await self._executor.execute(request, response_handler)
659
+
660
+ async def has_user(self, username: str) -> Result[bool]:
661
+ """Check if a user exists.
662
+
663
+ Args:
664
+ username (str): Username.
665
+
666
+ Returns:
667
+ bool: True if the user exists, False otherwise.
668
+
669
+ Raises:
670
+ UserListError: If the operation fails.
671
+ """
672
+ request = Request(method=Method.GET, endpoint=f"/_api/user/{username}")
673
+
674
+ def response_handler(resp: Response) -> bool:
675
+ if resp.is_success:
676
+ return True
677
+ if resp.status_code == HTTP_NOT_FOUND:
678
+ return False
679
+ raise UserListError(resp, request)
680
+
681
+ return await self._executor.execute(request, response_handler)
682
+
683
+ async def user(self, username: str) -> Result[UserInfo]:
684
+ """Fetches data about a user.
685
+
686
+ Args:
687
+ username (str): Username.
688
+
689
+ Returns:
690
+ UserInfo: User details.
691
+
692
+ Raises:
693
+ UserGetError: If the operation fails.
694
+
695
+ References:
696
+ - `get-a-user` <https://docs.arangodb.com/stable/develop/http-api/users/#get-a-user>`__
697
+ """ # noqa: E501
698
+ request = Request(method=Method.GET, endpoint=f"/_api/user/{username}")
699
+
700
+ def response_handler(resp: Response) -> UserInfo:
701
+ if not resp.is_success:
702
+ raise UserGetError(resp, request)
703
+ body = self.deserializer.loads(resp.raw_body)
704
+ return UserInfo(
705
+ user=body["user"],
706
+ active=cast(bool, body.get("active")),
707
+ extra=body.get("extra"),
708
+ )
709
+
710
+ return await self._executor.execute(request, response_handler)
711
+
712
+ async def users(self) -> Result[Sequence[UserInfo]]:
713
+ """Fetches data about all users.
714
+
715
+ Without the necessary permissions, you might only get data about the
716
+ current user.
717
+
718
+ Returns:
719
+ list: User information.
720
+
721
+ Raises:
722
+ UserListError: If the operation fails.
723
+
724
+ References:
725
+ - `list-available-users <https://docs.arangodb.com/stable/develop/http-api/users/#list-available-users>`__
726
+ """ # noqa: E501
727
+ request = Request(method=Method.GET, endpoint="/_api/user")
728
+
729
+ def response_handler(resp: Response) -> Sequence[UserInfo]:
730
+ if not resp.is_success:
731
+ raise UserListError(resp, request)
732
+ body = self.deserializer.loads(resp.raw_body)
733
+ return [
734
+ UserInfo(user=u["user"], active=u.get("active"), extra=u.get("extra"))
735
+ for u in body["result"]
736
+ ]
737
+
738
+ return await self._executor.execute(request, response_handler)
739
+
740
+ async def create_user(self, user: UserInfo | Json) -> Result[UserInfo]:
741
+ """Create a new user.
742
+
743
+ Args:
744
+ user (UserInfo | dict): User information.
745
+
746
+ Returns:
747
+ UserInfo: New user details.
748
+
749
+ Raises:
750
+ ValueError: If the username is missing.
751
+ UserCreateError: If the operation fails.
752
+
753
+ Example:
754
+ .. code-block:: python
755
+
756
+ await db.create_user(UserInfo(user="john", password="secret"))
757
+ await db.create_user({user="john", password="secret"})
758
+
759
+ References:
760
+ - `create-a-user <https://docs.arangodb.com/stable/develop/http-api/users/#create-a-user>`__
761
+ """ # noqa: E501
762
+ if isinstance(user, dict):
763
+ user = UserInfo(**user)
764
+ if not user.user:
765
+ raise ValueError("Username is required.")
766
+
767
+ data: Json = user.format(UserInfo.user_management_formatter)
768
+ request = Request(
769
+ method=Method.POST,
770
+ endpoint="/_api/user",
771
+ data=self.serializer.dumps(data),
772
+ )
773
+
774
+ def response_handler(resp: Response) -> UserInfo:
775
+ if not resp.is_success:
776
+ raise UserCreateError(resp, request)
777
+ body = self.deserializer.loads(resp.raw_body)
778
+ return UserInfo(
779
+ user=body["user"],
780
+ active=cast(bool, body.get("active")),
781
+ extra=body.get("extra"),
782
+ )
783
+
784
+ return await self._executor.execute(request, response_handler)
785
+
786
+ async def replace_user(self, user: UserInfo | Json) -> Result[UserInfo]:
787
+ """Replace the data of an existing user.
788
+
789
+ Args:
790
+ user (UserInfo | dict): New user information.
791
+
792
+ Returns:
793
+ UserInfo: New user details.
794
+
795
+ Raises:
796
+ ValueError: If the username is missing.
797
+ UserReplaceError: If the operation fails.
798
+
799
+ References:
800
+ - `replace-a-user <https://docs.arangodb.com/stable/develop/http-api/users/#replace-a-user>`__
801
+ """ # noqa: E501
802
+ if isinstance(user, dict):
803
+ user = UserInfo(**user)
804
+ if not user.user:
805
+ raise ValueError("Username is required.")
806
+
807
+ data: Json = user.format(UserInfo.user_management_formatter)
808
+ request = Request(
809
+ method=Method.PUT,
810
+ endpoint=f"/_api/user/{user.user}",
811
+ data=self.serializer.dumps(data),
812
+ )
813
+
814
+ def response_handler(resp: Response) -> UserInfo:
815
+ if not resp.is_success:
816
+ raise UserReplaceError(resp, request)
817
+ body = self.deserializer.loads(resp.raw_body)
818
+ return UserInfo(
819
+ user=body["user"],
820
+ active=cast(bool, body.get("active")),
821
+ extra=body.get("extra"),
822
+ )
823
+
824
+ return await self._executor.execute(request, response_handler)
825
+
826
+ async def update_user(self, user: UserInfo | Json) -> Result[UserInfo]:
827
+ """Partially modifies the data of an existing user.
828
+
829
+ Args:
830
+ user (UserInfo | dict): User information.
831
+
832
+ Returns:
833
+ UserInfo: Updated user details.
834
+
835
+ Raises:
836
+ ValueError: If the username is missing.
837
+ UserUpdateError: If the operation fails.
838
+
839
+ References:
840
+ - `update-a-user <https://docs.arangodb.com/stable/develop/http-api/users/#update-a-user>`__
841
+ """ # noqa: E501
842
+ if isinstance(user, dict):
843
+ user = UserInfo(**user)
844
+ if not user.user:
845
+ raise ValueError("Username is required.")
846
+
847
+ data: Json = user.format(UserInfo.user_management_formatter)
848
+ request = Request(
849
+ method=Method.PATCH,
850
+ endpoint=f"/_api/user/{user.user}",
851
+ data=self.serializer.dumps(data),
852
+ )
853
+
854
+ def response_handler(resp: Response) -> UserInfo:
855
+ if not resp.is_success:
856
+ raise UserUpdateError(resp, request)
857
+ body = self.deserializer.loads(resp.raw_body)
858
+ return UserInfo(
859
+ user=body["user"],
860
+ active=cast(bool, body.get("active")),
861
+ extra=body.get("extra"),
862
+ )
863
+
864
+ return await self._executor.execute(request, response_handler)
865
+
866
+ async def delete_user(
867
+ self,
868
+ username: str,
869
+ ignore_missing: bool = False,
870
+ ) -> Result[bool]:
871
+ """Delete a user.
872
+
873
+ Args:
874
+ username (str): Username.
875
+ ignore_missing (bool): Do not raise an exception on missing user.
876
+
877
+ Returns:
878
+ bool: True if the user was deleted successfully, `False` if the user was
879
+ not found but **ignore_missing** was set to `True`.
880
+
881
+ Raises:
882
+ UserDeleteError: If the operation fails.
883
+
884
+ References:
885
+ - `remove-a-user <https://docs.arangodb.com/stable/develop/http-api/users/#remove-a-user>`__
886
+ """ # noqa: E501
887
+ request = Request(method=Method.DELETE, endpoint=f"/_api/user/{username}")
888
+
889
+ def response_handler(resp: Response) -> bool:
890
+ if resp.is_success:
891
+ return True
892
+ if resp.status_code == HTTP_NOT_FOUND and ignore_missing:
893
+ return False
894
+ raise UserDeleteError(resp, request)
895
+
896
+ return await self._executor.execute(request, response_handler)
897
+
898
+ async def permissions(self, username: str, full: bool = True) -> Result[Json]:
899
+ """Return user permissions for all databases and collections.
900
+
901
+ Args:
902
+ username (str): Username.
903
+ full (bool): If `True`, the result will contain the permissions for the
904
+ databases as well as the permissions for the collections.
905
+
906
+ Returns:
907
+ dict: User permissions for all databases and (optionally) collections.
908
+
909
+ Raises:
910
+ PermissionListError: If the operation fails.
911
+
912
+ References:
913
+ - `list-a-users-accessible-databases <https://docs.arangodb.com/stable/develop/http-api/users/#list-a-users-accessible-databases>`__
914
+ """ # noqa: 501
915
+ request = Request(
916
+ method=Method.GET,
917
+ endpoint=f"/_api/user/{username}/database",
918
+ params={"full": full},
919
+ )
920
+
921
+ def response_handler(resp: Response) -> Json:
922
+ if resp.is_success:
923
+ result: Json = self.deserializer.loads(resp.raw_body)["result"]
924
+ return result
925
+ raise PermissionListError(resp, request)
926
+
927
+ return await self._executor.execute(request, response_handler)
928
+
929
+ async def permission(
930
+ self,
931
+ username: str,
932
+ database: str,
933
+ collection: Optional[str] = None,
934
+ ) -> Result[str]:
935
+ """Return user permission for a specific database or collection.
936
+
937
+ Args:
938
+ username (str): Username.
939
+ database (str): Database name.
940
+ collection (str | None): Collection name.
941
+
942
+ Returns:
943
+ str: User access level.
944
+
945
+ Raises:
946
+ PermissionGetError: If the operation fails.
947
+
948
+ References:
949
+ - `get-a-users-database-access-level <https://docs.arangodb.com/stable/develop/http-api/users/#get-a-users-database-access-level>`__
950
+ - `get-a-users-collection-access-level <https://docs.arangodb.com/stable/develop/http-api/users/#get-a-users-collection-access-level>`__
951
+ """ # noqa: 501
952
+ endpoint = f"/_api/user/{username}/database/{database}"
953
+ if collection is not None:
954
+ endpoint += f"/{collection}"
955
+ request = Request(method=Method.GET, endpoint=endpoint)
956
+
957
+ def response_handler(resp: Response) -> str:
958
+ if resp.is_success:
959
+ return cast(str, self.deserializer.loads(resp.raw_body)["result"])
960
+ raise PermissionGetError(resp, request)
961
+
962
+ return await self._executor.execute(request, response_handler)
963
+
964
+ async def update_permission(
965
+ self,
966
+ username: str,
967
+ permission: str,
968
+ database: str,
969
+ collection: Optional[str] = None,
970
+ ignore_failure: bool = False,
971
+ ) -> Result[bool]:
972
+ """Update user permissions for a specific database or collection.
973
+
974
+ Args:
975
+ username (str): Username.
976
+ permission (str): Allowed values are "rw" (administrate),
977
+ "ro" (access) and "none" (no access).
978
+ database (str): Database to set the access level for.
979
+ collection (str | None): Collection to set the access level for.
980
+ ignore_failure (bool): Do not raise an exception on failure.
981
+
982
+ Returns:
983
+ bool: `True` if the operation was successful.
984
+
985
+ Raises:
986
+ PermissionUpdateError: If the operation fails and `ignore_failure`
987
+ is `False`.
988
+
989
+ References:
990
+ - `set-a-users-database-access-level <https://docs.arangodb.com/stable/develop/http-api/users/#set-a-users-database-access-level>`__
991
+ - `set-a-users-collection-access-level <https://docs.arangodb.com/stable/develop/http-api/users/#set-a-users-collection-access-level>`__
992
+ """ # noqa: E501
993
+ endpoint = f"/_api/user/{username}/database/{database}"
994
+ if collection is not None:
995
+ endpoint += f"/{collection}"
996
+
997
+ request = Request(
998
+ method=Method.PUT,
999
+ endpoint=endpoint,
1000
+ data=self.serializer.dumps({"grant": permission}),
1001
+ )
1002
+
1003
+ def response_handler(resp: Response) -> bool:
1004
+ nonlocal ignore_failure
1005
+ if resp.is_success:
1006
+ return True
1007
+ if ignore_failure:
1008
+ return False
1009
+ raise PermissionUpdateError(resp, request)
1010
+
1011
+ return await self._executor.execute(request, response_handler)
1012
+
1013
+ async def reset_permission(
1014
+ self,
1015
+ username: str,
1016
+ database: str,
1017
+ collection: Optional[str] = None,
1018
+ ignore_failure: bool = False,
1019
+ ) -> Result[bool]:
1020
+ """Reset user permission for a specific database or collection.
1021
+
1022
+ Args:
1023
+ username (str): Username.
1024
+ database (str): Database to reset the access level for.
1025
+ collection (str | None): Collection to reset the access level for.
1026
+ ignore_failure (bool): Do not raise an exception on failure.
1027
+
1028
+ Returns:
1029
+ bool: `True` if the operation was successful.
1030
+
1031
+ Raises:
1032
+ PermissionResetError: If the operation fails and `ignore_failure`
1033
+ is `False`.
1034
+
1035
+ References:
1036
+ - `clear-a-users-database-access-level <https://docs.arangodb.com/stable/develop/http-api/users/#clear-a-users-database-access-level>`__
1037
+ - `clear-a-users-collection-access-level <https://docs.arangodb.com/stable/develop/http-api/users/#clear-a-users-collection-access-level>`__
1038
+ """ # noqa: E501
1039
+ endpoint = f"/_api/user/{username}/database/{database}"
1040
+ if collection is not None:
1041
+ endpoint += f"/{collection}"
1042
+
1043
+ request = Request(
1044
+ method=Method.DELETE,
1045
+ endpoint=endpoint,
1046
+ )
1047
+
1048
+ def response_handler(resp: Response) -> bool:
1049
+ nonlocal ignore_failure
1050
+ if resp.is_success:
1051
+ return True
1052
+ if ignore_failure:
1053
+ return False
1054
+ raise PermissionResetError(resp, request)
1055
+
1056
+ return await self._executor.execute(request, response_handler)
1057
+
1058
+ async def jwt_secrets(self) -> Result[Json]:
1059
+ """Return information on currently loaded JWT secrets.
1060
+
1061
+ Returns:
1062
+ dict: JWT secrets.
1063
+
1064
+ Raises:
1065
+ JWTSecretListError: If the operation fails.
1066
+
1067
+ References:
1068
+ - `get-information-about-the-loaded-jwt-secrets <https://docs.arangodb.com/stable/develop/http-api/authentication/#get-information-about-the-loaded-jwt-secrets>`__
1069
+ """ # noqa: 501
1070
+ request = Request(method=Method.GET, endpoint="/_admin/server/jwt")
1071
+
1072
+ def response_handler(resp: Response) -> Json:
1073
+ if not resp.is_success:
1074
+ raise JWTSecretListError(resp, request)
1075
+ result: Json = self.deserializer.loads(resp.raw_body)
1076
+ return result
1077
+
1078
+ return await self._executor.execute(request, response_handler)
1079
+
1080
+ async def reload_jwt_secrets(self) -> Result[Json]:
1081
+ """Hot_reload JWT secrets from disk.
1082
+
1083
+ Returns:
1084
+ dict: Information on reloaded JWT secrets.
1085
+
1086
+ Raises:
1087
+ JWTSecretReloadError: If the operation fails.
1088
+
1089
+ References:
1090
+ - `hot-reload-the-jwt-secrets-from-disk <https://docs.arangodb.com/stable/develop/http-api/authentication/#hot-reload-the-jwt-secrets-from-disk>`__
1091
+ """ # noqa: 501
1092
+ request = Request(method=Method.POST, endpoint="/_admin/server/jwt")
1093
+
1094
+ def response_handler(resp: Response) -> Json:
1095
+ if not resp.is_success:
1096
+ raise JWTSecretReloadError(resp, request)
1097
+ result: Json = self.deserializer.loads(resp.raw_body)
1098
+ return result
1099
+
1100
+ return await self._executor.execute(request, response_handler)
1101
+
1102
+ async def list_transactions(self) -> Result[Jsons]:
1103
+ """List all currently running stream transactions.
1104
+
1105
+ Returns:
1106
+ list: List of transactions, with each transaction containing
1107
+ an "id" and a "state" field.
1108
+
1109
+ Raises:
1110
+ TransactionListError: If the operation fails on the server side.
1111
+ """
1112
+ request = Request(method=Method.GET, endpoint="/_api/transaction")
1113
+
1114
+ def response_handler(resp: Response) -> Jsons:
1115
+ if not resp.is_success:
1116
+ raise TransactionListError(resp, request)
1117
+ result: Json = self.deserializer.loads(resp.raw_body)
1118
+ return cast(Jsons, result["transactions"])
1119
+
1120
+ return await self._executor.execute(request, response_handler)
1121
+
1122
+ async def execute_transaction(
1123
+ self,
1124
+ command: str,
1125
+ params: Optional[Json] = None,
1126
+ read: Optional[str | Sequence[str]] = None,
1127
+ write: Optional[str | Sequence[str]] = None,
1128
+ exclusive: Optional[str | Sequence[str]] = None,
1129
+ allow_implicit: Optional[bool] = None,
1130
+ wait_for_sync: Optional[bool] = None,
1131
+ lock_timeout: Optional[int] = None,
1132
+ max_transaction_size: Optional[int] = None,
1133
+ ) -> Result[Any]:
1134
+ """Execute a JavaScript Transaction.
1135
+
1136
+ Warning:
1137
+ JavaScript Transactions are deprecated from ArangoDB v3.12.0 onward and
1138
+ will be removed in a future version.
1139
+
1140
+ Args:
1141
+ command (str): The actual transaction operations to be executed, in the
1142
+ form of stringified JavaScript code.
1143
+ params (dict): Optional parameters passed into the JavaScript command.
1144
+ read (str | list | None): Name(s) of collections read during transaction.
1145
+ write (str | list | None): Name(s) of collections written to during
1146
+ transaction with shared access.
1147
+ exclusive (str | list | None): Name(s) of collections written to during
1148
+ transaction with exclusive access.
1149
+ allow_implicit (bool | None): Allow reading from undeclared collections.
1150
+ wait_for_sync (bool | None): If `True`, will force the transaction to write
1151
+ all data to disk before returning.
1152
+ lock_timeout (int | None): Timeout for waiting on collection locks. Setting
1153
+ it to 0 will prevent ArangoDB from timing out while waiting for a lock.
1154
+ max_transaction_size (int | None): Transaction size limit in bytes.
1155
+
1156
+ Returns:
1157
+ Any: Result of the transaction.
1158
+
1159
+ Raises:
1160
+ TransactionExecuteError: If the operation fails on the server side.
1161
+
1162
+ References:
1163
+ - `execute-a-javascript-transaction <https://docs.arangodb.com/stable/develop/http-api/transactions/javascript-transactions/#execute-a-javascript-transaction>`__
1164
+ """ # noqa: 501
1165
+ m = "JavaScript Transactions are deprecated from ArangoDB v3.12.0 onward and will be removed in a future version." # noqa: E501
1166
+ warn(m, DeprecationWarning, stacklevel=2)
1167
+
1168
+ collections = dict()
1169
+ if read is not None:
1170
+ collections["read"] = read
1171
+ if write is not None:
1172
+ collections["write"] = write
1173
+ if exclusive is not None:
1174
+ collections["exclusive"] = exclusive
1175
+
1176
+ data: Json = dict(collections=collections, action=command)
1177
+ if params is not None:
1178
+ data["params"] = params
1179
+ if wait_for_sync is not None:
1180
+ data["waitForSync"] = wait_for_sync
1181
+ if allow_implicit is not None:
1182
+ data["allowImplicit"] = allow_implicit
1183
+ if lock_timeout is not None:
1184
+ data["lockTimeout"] = lock_timeout
1185
+ if max_transaction_size is not None:
1186
+ data["maxTransactionSize"] = max_transaction_size
1187
+
1188
+ request = Request(
1189
+ method=Method.POST,
1190
+ endpoint="/_api/transaction",
1191
+ data=self.serializer.dumps(data),
1192
+ )
1193
+
1194
+ def response_handler(resp: Response) -> Any:
1195
+ if not resp.is_success:
1196
+ raise TransactionExecuteError(resp, request)
1197
+ return self.deserializer.loads(resp.raw_body)["result"]
1198
+
1199
+ return await self._executor.execute(request, response_handler)
1200
+
1201
+ async def version(self, details: bool = False) -> Result[Json]:
1202
+ """Return the server version information.
1203
+
1204
+ Args:
1205
+ details (bool): If `True`, return detailed version information.
1206
+
1207
+ Returns:
1208
+ dict: Server version information.
1209
+
1210
+ Raises:
1211
+ ServerVersionError: If the operation fails on the server side.
1212
+
1213
+ References:
1214
+ - `get-the-server-version <https://docs.arangodb.com/stable/develop/http-api/administration/#get-the-server-version>`__
1215
+ """ # noqa: E501
1216
+ request = Request(
1217
+ method=Method.GET, endpoint="/_api/version", params={"details": details}
1218
+ )
1219
+
1220
+ def response_handler(resp: Response) -> Json:
1221
+ if not resp.is_success:
1222
+ raise ServerVersionError(resp, request)
1223
+ return self.deserializer.loads(resp.raw_body)
1224
+
1225
+ return await self._executor.execute(request, response_handler)
1226
+
1227
+
1228
+ class StandardDatabase(Database):
1229
+ """Standard database API wrapper.
1230
+
1231
+ Args:
1232
+ connection (Connection): Connection object to be used by the API executor.
1233
+ """
1234
+
1235
+ def __init__(self, connection: Connection) -> None:
1236
+ super().__init__(DefaultApiExecutor(connection))
1237
+
1238
+ def __repr__(self) -> str:
1239
+ return f"<StandardDatabase {self.name}>"
1240
+
1241
+ async def begin_transaction(
1242
+ self,
1243
+ read: Optional[str | Sequence[str]] = None,
1244
+ write: Optional[str | Sequence[str]] = None,
1245
+ exclusive: Optional[str | Sequence[str]] = None,
1246
+ wait_for_sync: Optional[bool] = None,
1247
+ allow_implicit: Optional[bool] = None,
1248
+ lock_timeout: Optional[int] = None,
1249
+ max_transaction_size: Optional[int] = None,
1250
+ allow_dirty_read: Optional[bool] = None,
1251
+ skip_fast_lock_round: Optional[bool] = None,
1252
+ ) -> "TransactionDatabase":
1253
+ """Begin a Stream Transaction.
1254
+
1255
+ Args:
1256
+ read (str | list | None): Name(s) of collections read during transaction.
1257
+ Read-only collections are added lazily but should be declared if
1258
+ possible to avoid deadlocks.
1259
+ write (str | list | None): Name(s) of collections written to during
1260
+ transaction with shared access.
1261
+ exclusive (str | list | None): Name(s) of collections written to during
1262
+ transaction with exclusive access.
1263
+ wait_for_sync (bool | None): If `True`, will force the transaction to write
1264
+ all data to disk before returning
1265
+ allow_implicit (bool | None): Allow reading from undeclared collections.
1266
+ lock_timeout (int | None): Timeout for waiting on collection locks. Setting
1267
+ it to 0 will prevent ArangoDB from timing out while waiting for a lock.
1268
+ max_transaction_size (int | None): Transaction size limit in bytes.
1269
+ allow_dirty_read (bool | None): If `True`, allows the Coordinator to ask any
1270
+ shard replica for the data, not only the shard leader. This may result
1271
+ in “dirty reads”. This setting decides about dirty reads for the entire
1272
+ transaction. Individual read operations, that are performed as part of
1273
+ the transaction, cannot override it.
1274
+ skip_fast_lock_round (bool | None): Whether to disable fast locking for
1275
+ write operations.
1276
+
1277
+ Returns:
1278
+ TransactionDatabase: Database API wrapper specifically tailored for
1279
+ transactions.
1280
+
1281
+ Raises:
1282
+ TransactionInitError: If the operation fails on the server side.
1283
+
1284
+ References:
1285
+ - `begin-a-stream-transaction <https://docs.arangodb.com/stable/develop/http-api/transactions/stream-transactions/#begin-a-stream-transaction>`__
1286
+ """ # noqa: E501
1287
+ collections = dict()
1288
+ if read is not None:
1289
+ collections["read"] = read
1290
+ if write is not None:
1291
+ collections["write"] = write
1292
+ if exclusive is not None:
1293
+ collections["exclusive"] = exclusive
1294
+
1295
+ data: Json = dict(collections=collections)
1296
+ if wait_for_sync is not None:
1297
+ data["waitForSync"] = wait_for_sync
1298
+ if allow_implicit is not None:
1299
+ data["allowImplicit"] = allow_implicit
1300
+ if lock_timeout is not None:
1301
+ data["lockTimeout"] = lock_timeout
1302
+ if max_transaction_size is not None:
1303
+ data["maxTransactionSize"] = max_transaction_size
1304
+ if skip_fast_lock_round is not None:
1305
+ data["skipFastLockRound"] = skip_fast_lock_round
1306
+
1307
+ headers = dict()
1308
+ if allow_dirty_read is not None:
1309
+ headers["x-arango-allow-dirty-read"] = str(allow_dirty_read).lower()
1310
+
1311
+ request = Request(
1312
+ method=Method.POST,
1313
+ endpoint="/_api/transaction/begin",
1314
+ data=self.serializer.dumps(data),
1315
+ headers=headers,
1316
+ )
1317
+
1318
+ def response_handler(resp: Response) -> str:
1319
+ if not resp.is_success:
1320
+ raise TransactionInitError(resp, request)
1321
+ result: Json = self.deserializer.loads(resp.raw_body)["result"]
1322
+ return cast(str, result["id"])
1323
+
1324
+ transaction_id = await self._executor.execute(request, response_handler)
1325
+ return TransactionDatabase(self.connection, cast(str, transaction_id))
1326
+
1327
+ def fetch_transaction(self, transaction_id: str) -> "TransactionDatabase":
1328
+ """Fetch an existing transaction.
1329
+
1330
+ Args:
1331
+ transaction_id (str): Transaction ID.
1332
+
1333
+ Returns:
1334
+ TransactionDatabase: Database API wrapper specifically tailored for
1335
+ transactions.
1336
+ """
1337
+ return TransactionDatabase(self.connection, transaction_id)
1338
+
1339
+ def begin_async_execution(self, return_result: bool = True) -> "AsyncDatabase":
1340
+ """Begin async execution.
1341
+
1342
+ Args:
1343
+ return_result (bool): If set to `True`, API executions return instances of
1344
+ `arangoasync.job.AsyncJob`, which you can be used to retrieve
1345
+ results from server once available. Otherwise, API executions
1346
+ return `None` and no results are stored on server.
1347
+
1348
+ Returns:
1349
+ AsyncDatabase: Database API wrapper tailored for async execution.
1350
+ """
1351
+ return AsyncDatabase(self.connection, return_result)
1352
+
1353
+ async def async_jobs(
1354
+ self, status: str, count: Optional[int] = None
1355
+ ) -> Result[List[str]]:
1356
+ """Return IDs of async jobs with given status.
1357
+
1358
+ Args:
1359
+ status (str): Job status (e.g. "pending", "done").
1360
+ count (int | None): Max number of job IDs to return.
1361
+
1362
+ Returns:
1363
+ list: List of job IDs.
1364
+
1365
+ Raises:
1366
+ AsyncJobListError: If retrieval fails.
1367
+
1368
+ References:
1369
+ - `list-async-jobs-by-status-or-get-the-status-of-specific-job <https://docs.arangodb.com/stable/develop/http-api/jobs/#list-async-jobs-by-status-or-get-the-status-of-specific-job>`__
1370
+ """ # noqa: E501
1371
+ params: Params = {}
1372
+ if count is not None:
1373
+ params["count"] = count
1374
+
1375
+ request = Request(
1376
+ method=Method.GET, endpoint=f"/_api/job/{status}", params=params
1377
+ )
1378
+
1379
+ def response_handler(resp: Response) -> List[str]:
1380
+ if resp.is_success:
1381
+ return cast(List[str], self.deserializer.loads(resp.raw_body))
1382
+ raise AsyncJobListError(resp, request)
1383
+
1384
+ return await self._executor.execute(request, response_handler)
1385
+
1386
+ async def clear_async_jobs(self, threshold: Optional[float] = None) -> None:
1387
+ """Clear async job results from the server.
1388
+
1389
+ Async jobs that are still queued or running are not stopped.
1390
+ Clients can use this method to perform an eventual garbage
1391
+ collection of job results.
1392
+
1393
+ Args:
1394
+ threshold (float | None): If specified, only the job results created
1395
+ prior to the threshold (a Unix timestamp) are deleted. Otherwise,
1396
+ all job results are deleted.
1397
+
1398
+ Raises:
1399
+ AsyncJobClearError: If the operation fails.
1400
+
1401
+ References:
1402
+ - `delete-async-job-results <https://docs.arangodb.com/stable/develop/http-api/jobs/#delete-async-job-results>`__
1403
+ """ # noqa: E501
1404
+ if threshold is None:
1405
+ request = Request(method=Method.DELETE, endpoint="/_api/job/all")
1406
+ else:
1407
+ request = Request(
1408
+ method=Method.DELETE,
1409
+ endpoint="/_api/job/expired",
1410
+ params={"stamp": threshold},
1411
+ )
1412
+
1413
+ def response_handler(resp: Response) -> None:
1414
+ if not resp.is_success:
1415
+ raise AsyncJobClearError(resp, request)
1416
+
1417
+ await self._executor.execute(request, response_handler)
1418
+
1419
+
1420
+ class TransactionDatabase(Database):
1421
+ """Database API tailored specifically for
1422
+ `Stream Transactions <https://docs.arangodb.com/stable/develop/http-api/transactions/stream-transactions/>`__.
1423
+
1424
+ It allows you start a transaction, run multiple operations (eg. AQL queries) over a short period of time,
1425
+ and then commit or abort the transaction.
1426
+
1427
+ See :func:`arangoasync.database.StandardDatabase.begin_transaction`.
1428
+
1429
+ Args:
1430
+ connection (Connection): Connection object to be used by the API executor.
1431
+ transaction_id (str): Transaction ID.
1432
+ """ # noqa: E501
1433
+
1434
+ def __init__(self, connection: Connection, transaction_id: str) -> None:
1435
+ super().__init__(TransactionApiExecutor(connection, transaction_id))
1436
+ self._standard_executor = DefaultApiExecutor(connection)
1437
+ self._transaction_id = transaction_id
1438
+
1439
+ def __repr__(self) -> str:
1440
+ return f"<TransactionDatabase {self.name}>"
1441
+
1442
+ @property
1443
+ def transaction_id(self) -> str:
1444
+ """Transaction ID."""
1445
+ return self._transaction_id
1446
+
1447
+ async def transaction_status(self) -> str:
1448
+ """Get the status of the transaction.
1449
+
1450
+ Returns:
1451
+ str: Transaction status: one of "running", "committed" or "aborted".
1452
+
1453
+ Raises:
1454
+ TransactionStatusError: If the transaction is not found.
1455
+
1456
+ References:
1457
+ - `get-the-status-of-a-stream-transaction <https://docs.arangodb.com/stable/develop/http-api/transactions/stream-transactions/#get-the-status-of-a-stream-transaction>`__
1458
+ """ # noqa: E501
1459
+ request = Request(
1460
+ method=Method.GET,
1461
+ endpoint=f"/_api/transaction/{self.transaction_id}",
1462
+ )
1463
+
1464
+ def response_handler(resp: Response) -> str:
1465
+ if not resp.is_success:
1466
+ raise TransactionStatusError(resp, request)
1467
+ result: Json = self.deserializer.loads(resp.raw_body)["result"]
1468
+ return cast(str, result["status"])
1469
+
1470
+ return await self._standard_executor.execute(request, response_handler)
1471
+
1472
+ async def commit_transaction(self) -> None:
1473
+ """Commit the transaction.
1474
+
1475
+ Raises:
1476
+ TransactionCommitError: If the operation fails on the server side.
1477
+
1478
+ References:
1479
+ - `commit-a-stream-transaction <https://docs.arangodb.com/stable/develop/http-api/transactions/stream-transactions/#commit-a-stream-transaction>`__
1480
+ """ # noqa: E501
1481
+ request = Request(
1482
+ method=Method.PUT,
1483
+ endpoint=f"/_api/transaction/{self.transaction_id}",
1484
+ )
1485
+
1486
+ def response_handler(resp: Response) -> None:
1487
+ if not resp.is_success:
1488
+ raise TransactionCommitError(resp, request)
1489
+
1490
+ await self._standard_executor.execute(request, response_handler)
1491
+
1492
+ async def abort_transaction(self) -> None:
1493
+ """Abort the transaction.
1494
+
1495
+ Raises:
1496
+ TransactionAbortError: If the operation fails on the server side.
1497
+
1498
+ References:
1499
+ - `abort-a-stream-transaction <https://docs.arangodb.com/stable/develop/http-api/transactions/stream-transactions/#abort-a-stream-transaction>`__
1500
+ """ # noqa: E501
1501
+ request = Request(
1502
+ method=Method.DELETE,
1503
+ endpoint=f"/_api/transaction/{self.transaction_id}",
1504
+ )
1505
+
1506
+ def response_handler(resp: Response) -> None:
1507
+ if not resp.is_success:
1508
+ raise TransactionAbortError(resp, request)
1509
+
1510
+ await self._standard_executor.execute(request, response_handler)
1511
+
1512
+
1513
+ class AsyncDatabase(Database):
1514
+ """Database API wrapper tailored specifically for async execution.
1515
+
1516
+ See :func:`arangoasync.database.StandardDatabase.begin_async_execution`.
1517
+
1518
+ Args:
1519
+ connection (Connection): HTTP connection.
1520
+ return_result (bool): If set to `True`, API executions return instances of
1521
+ :class:`arangoasync.job.AsyncJob`, which you can use to retrieve results
1522
+ from server once available. If set to `False`, API executions return `None`
1523
+ and no results are stored on server.
1524
+
1525
+ References:
1526
+ - `jobs <https://docs.arangodb.com/stable/develop/http-api/jobs/>`__
1527
+ """ # noqa: E501
1528
+
1529
+ def __init__(self, connection: Connection, return_result: bool) -> None:
1530
+ super().__init__(executor=AsyncApiExecutor(connection, return_result))
1531
+
1532
+ def __repr__(self) -> str:
1533
+ return f"<AsyncDatabase {self.name}>"