Skip to content

Commit 325c4e0

Browse files
authored
Hot Backup API (#60)
* Hot Backup API * Hot Backup docs * Hot Backup only tested in cluster * Hot Backup only tested for enterprise * Minimize backup tests
1 parent e014bf8 commit 325c4e0

File tree

7 files changed

+468
-0
lines changed

7 files changed

+468
-0
lines changed

arangoasync/backup.py

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
__all__ = ["Backup"]
2+
3+
from numbers import Number
4+
from typing import Optional, cast
5+
6+
from arangoasync.exceptions import (
7+
BackupCreateError,
8+
BackupDeleteError,
9+
BackupDownloadError,
10+
BackupGetError,
11+
BackupRestoreError,
12+
BackupUploadError,
13+
)
14+
from arangoasync.executor import ApiExecutor
15+
from arangoasync.request import Method, Request
16+
from arangoasync.response import Response
17+
from arangoasync.result import Result
18+
from arangoasync.serialization import Deserializer, Serializer
19+
from arangoasync.typings import Json, Jsons
20+
21+
22+
class Backup:
23+
"""Backup API wrapper."""
24+
25+
def __init__(self, executor: ApiExecutor) -> None:
26+
self._executor = executor
27+
28+
@property
29+
def serializer(self) -> Serializer[Json]:
30+
"""Return the serializer."""
31+
return self._executor.serializer
32+
33+
@property
34+
def deserializer(self) -> Deserializer[Json, Jsons]:
35+
"""Return the deserializer."""
36+
return self._executor.deserializer
37+
38+
async def get(self, backup_id: Optional[str] = None) -> Result[Json]:
39+
"""Return backup details.
40+
41+
Args:
42+
backup_id (str | None): If set, the returned list is restricted to the
43+
backup with the given id.
44+
45+
Returns:
46+
dict: Backup details.
47+
48+
Raises:
49+
BackupGetError: If the operation fails.
50+
51+
References:
52+
- `list-backups <https://docs.arangodb.com/stable/develop/http-api/hot-backups/#list-all-backups>`__
53+
""" # noqa: E501
54+
data: Json = {}
55+
if backup_id is not None:
56+
data["id"] = backup_id
57+
58+
request = Request(
59+
method=Method.POST,
60+
endpoint="/_admin/backup/list",
61+
data=self.serializer.dumps(data) if data else None,
62+
prefix_needed=False,
63+
)
64+
65+
def response_handler(resp: Response) -> Json:
66+
if not resp.is_success:
67+
raise BackupGetError(resp, request)
68+
result: Json = self.deserializer.loads(resp.raw_body)
69+
return cast(Json, result["result"])
70+
71+
return await self._executor.execute(request, response_handler)
72+
73+
async def create(
74+
self,
75+
label: Optional[str] = None,
76+
allow_inconsistent: Optional[bool] = None,
77+
force: Optional[bool] = None,
78+
timeout: Optional[Number] = None,
79+
) -> Result[Json]:
80+
"""Create a backup when the global write lock can be obtained.
81+
82+
Args:
83+
label (str | None): Label for this backup. If not specified, a UUID is used.
84+
allow_inconsistent (bool | None): Allow inconsistent backup when the global
85+
transaction lock cannot be acquired before timeout.
86+
force (bool | None): Forcefully abort all running transactions to ensure a
87+
consistent backup when the global transaction lock cannot be
88+
acquired before timeout. Default (and highly recommended) value
89+
is `False`.
90+
timeout (float | None): The time in seconds that the operation tries to
91+
get a consistent snapshot.
92+
93+
Returns:
94+
dict: Backup information.
95+
96+
Raises:
97+
BackupCreateError: If the backup creation fails.
98+
99+
References:
100+
- `create-backup <https://docs.arangodb.com/stable/develop/http-api/hot-backups/#create-a-backup>`__
101+
""" # noqa: E501
102+
data: Json = {}
103+
if label is not None:
104+
data["label"] = label
105+
if allow_inconsistent is not None:
106+
data["allowInconsistent"] = allow_inconsistent
107+
if force is not None:
108+
data["force"] = force
109+
if timeout is not None:
110+
data["timeout"] = timeout
111+
112+
request = Request(
113+
method=Method.POST,
114+
endpoint="/_admin/backup/create",
115+
data=self.serializer.dumps(data),
116+
prefix_needed=False,
117+
)
118+
119+
def response_handler(resp: Response) -> Json:
120+
if not resp.is_success:
121+
raise BackupCreateError(resp, request)
122+
result: Json = self.deserializer.loads(resp.raw_body)
123+
return cast(Json, result["result"])
124+
125+
return await self._executor.execute(request, response_handler)
126+
127+
async def restore(self, backup_id: str) -> Result[Json]:
128+
"""Restore a local backup.
129+
130+
Args:
131+
backup_id (str): Backup ID.
132+
133+
Returns:
134+
dict: Result of the restore operation.
135+
136+
Raises:
137+
BackupRestoreError: If the restore operation fails.
138+
139+
References:
140+
- `restore-backup <https://docs.arangodb.com/stable/develop/http-api/hot-backups/#restore-a-backup>`__
141+
""" # noqa: E501
142+
data: Json = {"id": backup_id}
143+
request = Request(
144+
method=Method.POST,
145+
endpoint="/_admin/backup/restore",
146+
data=self.serializer.dumps(data),
147+
prefix_needed=False,
148+
)
149+
150+
def response_handler(resp: Response) -> Json:
151+
if not resp.is_success:
152+
raise BackupRestoreError(resp, request)
153+
result: Json = self.deserializer.loads(resp.raw_body)
154+
return cast(Json, result["result"])
155+
156+
return await self._executor.execute(request, response_handler)
157+
158+
async def delete(self, backup_id: str) -> None:
159+
"""Delete a backup.
160+
161+
Args:
162+
backup_id (str): Backup ID.
163+
164+
Raises:
165+
BackupDeleteError: If the delete operation fails.
166+
167+
References:
168+
- `delete-backup <https://docs.arangodb.com/stable/develop/http-api/hot-backups/#delete-a-backup>`__
169+
""" # noqa: E501
170+
data: Json = {"id": backup_id}
171+
request = Request(
172+
method=Method.POST,
173+
endpoint="/_admin/backup/delete",
174+
data=self.serializer.dumps(data),
175+
prefix_needed=False,
176+
)
177+
178+
def response_handler(resp: Response) -> None:
179+
if not resp.is_success:
180+
raise BackupDeleteError(resp, request)
181+
182+
await self._executor.execute(request, response_handler)
183+
184+
async def upload(
185+
self,
186+
backup_id: Optional[str] = None,
187+
repository: Optional[str] = None,
188+
abort: Optional[bool] = None,
189+
config: Optional[Json] = None,
190+
upload_id: Optional[str] = None,
191+
) -> Result[Json]:
192+
"""Manage backup uploads.
193+
194+
Args:
195+
backup_id (str | None): Backup ID used for scheduling an upload. Mutually
196+
exclusive with parameter **upload_id**.
197+
repository (str | None): Remote repository URL(e.g. "local://tmp/backups").
198+
abort (str | None): If set to `True`, running upload is aborted. Used with
199+
parameter **upload_id**.
200+
config (dict | None): Remote repository configuration. Required for scheduling
201+
an upload and mutually exclusive with parameter **upload_id**.
202+
upload_id (str | None): Upload ID. Mutually exclusive with parameters
203+
**backup_id**, **repository**, and **config**.
204+
205+
Returns:
206+
dict: Upload details.
207+
208+
Raises:
209+
BackupUploadError: If upload operation fails.
210+
211+
References:
212+
- `upload-a-backup-to-a-remote-repository <https://docs.arangodb.com/stable/develop/http-api/hot-backups/#upload-a-backup-to-a-remote-repository>`__
213+
""" # noqa: E501
214+
data: Json = {}
215+
if upload_id is not None:
216+
data["uploadId"] = upload_id
217+
if backup_id is not None:
218+
data["id"] = backup_id
219+
if repository is not None:
220+
data["remoteRepository"] = repository
221+
if abort is not None:
222+
data["abort"] = abort
223+
if config is not None:
224+
data["config"] = config
225+
226+
request = Request(
227+
method=Method.POST,
228+
endpoint="/_admin/backup/upload",
229+
data=self.serializer.dumps(data),
230+
prefix_needed=False,
231+
)
232+
233+
def response_handler(resp: Response) -> Json:
234+
if not resp.is_success:
235+
raise BackupUploadError(resp, request)
236+
result: Json = self.deserializer.loads(resp.raw_body)
237+
return cast(Json, result["result"])
238+
239+
return await self._executor.execute(request, response_handler)
240+
241+
async def download(
242+
self,
243+
backup_id: Optional[str] = None,
244+
repository: Optional[str] = None,
245+
abort: Optional[bool] = None,
246+
config: Optional[Json] = None,
247+
download_id: Optional[str] = None,
248+
) -> Result[Json]:
249+
"""Manage backup downloads.
250+
251+
Args:
252+
backup_id (str | None): Backup ID used for scheduling a download. Mutually
253+
exclusive with parameter **download_id**.
254+
repository (str | None): Remote repository URL (e.g. "local://tmp/backups").
255+
abort (bool | None): If set to `True`, running download is aborted.
256+
config (dict | None): Remote repository configuration. Required for scheduling
257+
a download and mutually exclusive with parameter **download_id**.
258+
download_id (str | None): Download ID. Mutually exclusive with parameters
259+
**backup_id**, **repository**, and **config**.
260+
261+
Returns:
262+
dict: Download details.
263+
264+
Raises:
265+
BackupDownloadError: If the download operation fails.
266+
267+
References:
268+
- `download-a-backup-from-a-remote-repository <https://docs.arangodb.com/stable/develop/http-api/hot-backups/#download-a-backup-from-a-remote-repository>`__
269+
""" # noqa: E501
270+
data: Json = {}
271+
if download_id is not None:
272+
data["downloadId"] = download_id
273+
if backup_id is not None:
274+
data["id"] = backup_id
275+
if repository is not None:
276+
data["remoteRepository"] = repository
277+
if abort is not None:
278+
data["abort"] = abort
279+
if config is not None:
280+
data["config"] = config
281+
282+
request = Request(
283+
method=Method.POST,
284+
endpoint="/_admin/backup/download",
285+
data=self.serializer.dumps(data),
286+
prefix_needed=False,
287+
)
288+
289+
def response_handler(resp: Response) -> Json:
290+
if not resp.is_success:
291+
raise BackupDownloadError(resp, request)
292+
result: Json = self.deserializer.loads(resp.raw_body)
293+
return cast(Json, result["result"])
294+
295+
return await self._executor.execute(request, response_handler)

arangoasync/database.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from warnings import warn
1111

1212
from arangoasync.aql import AQL
13+
from arangoasync.backup import Backup
1314
from arangoasync.collection import Collection, StandardCollection
1415
from arangoasync.connection import Connection
1516
from arangoasync.errno import HTTP_FORBIDDEN, HTTP_NOT_FOUND
@@ -172,6 +173,15 @@ def aql(self) -> AQL:
172173
"""
173174
return AQL(self._executor)
174175

176+
@property
177+
def backup(self) -> Backup:
178+
"""Return Backup API wrapper.
179+
180+
Returns:
181+
arangoasync.backup.Backup: Backup API wrapper.
182+
"""
183+
return Backup(self._executor)
184+
175185
async def properties(self) -> Result[DatabaseProperties]:
176186
"""Return database properties.
177187

arangoasync/exceptions.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,30 @@ class AuthHeaderError(ArangoClientError):
179179
"""The authentication header could not be determined."""
180180

181181

182+
class BackupCreateError(ArangoServerError):
183+
"""Failed to create a backup."""
184+
185+
186+
class BackupDeleteError(ArangoServerError):
187+
"""Failed to delete a backup."""
188+
189+
190+
class BackupDownloadError(ArangoServerError):
191+
"""Failed to download a backup from remote repository."""
192+
193+
194+
class BackupGetError(ArangoServerError):
195+
"""Failed to retrieve backup details."""
196+
197+
198+
class BackupRestoreError(ArangoServerError):
199+
"""Failed to restore from backup."""
200+
201+
202+
class BackupUploadError(ArangoServerError):
203+
"""Failed to upload a backup to remote repository."""
204+
205+
182206
class CollectionCreateError(ArangoServerError):
183207
"""Failed to create collection."""
184208

0 commit comments

Comments
 (0)