backd 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.
- backd/__init__.py +284 -0
- backd/_aio.py +29 -0
- backd/_async/__init__.py +1 -0
- backd/_async/account.py +153 -0
- backd/_async/auth.py +791 -0
- backd/_async/client.py +489 -0
- backd/_async/config.py +365 -0
- backd/_async/consents.py +48 -0
- backd/_async/data.py +500 -0
- backd/_async/files.py +448 -0
- backd/_async/functions.py +159 -0
- backd/_async/orgs.py +418 -0
- backd/_async/push.py +101 -0
- backd/_async/realtime.py +268 -0
- backd/_core.py +99 -0
- backd/_live.py +777 -0
- backd/_retry.py +23 -0
- backd/_sio.py +23 -0
- backd/_sync/__init__.py +1 -0
- backd/_sync/account.py +154 -0
- backd/_sync/auth.py +792 -0
- backd/_sync/client.py +490 -0
- backd/_sync/config.py +366 -0
- backd/_sync/consents.py +49 -0
- backd/_sync/data.py +501 -0
- backd/_sync/files.py +449 -0
- backd/_sync/functions.py +160 -0
- backd/_sync/orgs.py +419 -0
- backd/_sync/push.py +102 -0
- backd/_sync/realtime.py +316 -0
- backd/cache.py +137 -0
- backd/config_storage.py +90 -0
- backd/errors.py +388 -0
- backd/files.py +37 -0
- backd/models.py +716 -0
- backd/oauth.py +163 -0
- backd/py.typed +0 -0
- backd/query.py +303 -0
- backd/size.py +87 -0
- backd/storage.py +97 -0
- backd/upload.py +136 -0
- backd-0.0.1.dist-info/METADATA +58 -0
- backd-0.0.1.dist-info/RECORD +45 -0
- backd-0.0.1.dist-info/WHEEL +4 -0
- backd-0.0.1.dist-info/licenses/LICENSE +21 -0
backd/__init__.py
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Python client for backd: auth, sessions and data for services, scripts and notebooks.
|
|
2
|
+
|
|
3
|
+
``Client`` is the sync client and ``AsyncClient`` the async one; both have the same calls.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from ._async.client import AsyncClient
|
|
7
|
+
from ._async.data import AsyncCollection, AsyncDatabase
|
|
8
|
+
from ._async.functions import AsyncJob
|
|
9
|
+
from ._async.realtime import AsyncLiveView
|
|
10
|
+
from ._live import LiveState, LiveStatus, LiveSubscriptionState, ReferenceSpec, SubscribeOptions, SubscriptionSpec
|
|
11
|
+
from ._retry import Retry
|
|
12
|
+
from ._sync.client import Client
|
|
13
|
+
from ._sync.data import Collection, Database
|
|
14
|
+
from ._sync.functions import Job
|
|
15
|
+
from ._sync.realtime import LiveView
|
|
16
|
+
from .cache import CacheOptions, CacheStats
|
|
17
|
+
from .config_storage import ConfigStorage, FileConfigStorage, MemoryConfigStorage
|
|
18
|
+
from .errors import (
|
|
19
|
+
AggregateNotEnabledError,
|
|
20
|
+
AggregateTooExpensiveError,
|
|
21
|
+
AuthenticationError,
|
|
22
|
+
BackdError,
|
|
23
|
+
ConflictError,
|
|
24
|
+
ConsentRequiredError,
|
|
25
|
+
ErrorDetail,
|
|
26
|
+
ExpandTooLargeError,
|
|
27
|
+
FieldEncryptedError,
|
|
28
|
+
ForbiddenError,
|
|
29
|
+
IdTakenError,
|
|
30
|
+
IndexBuildingError,
|
|
31
|
+
InvalidAggregateError,
|
|
32
|
+
InvalidExpandError,
|
|
33
|
+
InvalidIdError,
|
|
34
|
+
InvalidResumeTokenError,
|
|
35
|
+
JobTimeoutError,
|
|
36
|
+
LastOrgManagerError,
|
|
37
|
+
NetworkError,
|
|
38
|
+
NotAllowedError,
|
|
39
|
+
NotFoundError,
|
|
40
|
+
NotOrgMemberError,
|
|
41
|
+
NotTeamMemberError,
|
|
42
|
+
OrgRequiresMFAError,
|
|
43
|
+
OrgRoleRequiresMFAError,
|
|
44
|
+
PasswordExpiredError,
|
|
45
|
+
RealtimeNotEnabledError,
|
|
46
|
+
RetryableError,
|
|
47
|
+
SearchNotEnabledError,
|
|
48
|
+
SearchTooExpensiveError,
|
|
49
|
+
SecondStepRequiredError,
|
|
50
|
+
SessionEndedError,
|
|
51
|
+
SsoRequiredError,
|
|
52
|
+
SsoSessionError,
|
|
53
|
+
TeamError,
|
|
54
|
+
TooManySubscriptionsError,
|
|
55
|
+
ValidationError,
|
|
56
|
+
VerificationRequiredError,
|
|
57
|
+
VersionMismatchError,
|
|
58
|
+
)
|
|
59
|
+
from .files import public_url, version_status
|
|
60
|
+
from .models import (
|
|
61
|
+
AggregateGroup,
|
|
62
|
+
AggregateResult,
|
|
63
|
+
BatchOperation,
|
|
64
|
+
ConsentChallenge,
|
|
65
|
+
ConsentInfo,
|
|
66
|
+
ConsentState,
|
|
67
|
+
ConsentText,
|
|
68
|
+
DataExport,
|
|
69
|
+
Device,
|
|
70
|
+
DeviceInput,
|
|
71
|
+
Doc,
|
|
72
|
+
EnrollmentChallenge,
|
|
73
|
+
FileDetails,
|
|
74
|
+
FileField,
|
|
75
|
+
FileLink,
|
|
76
|
+
Identity,
|
|
77
|
+
JobData,
|
|
78
|
+
JobResult,
|
|
79
|
+
JobStep,
|
|
80
|
+
Me,
|
|
81
|
+
Meta,
|
|
82
|
+
MfaChallenge,
|
|
83
|
+
Org,
|
|
84
|
+
OrgConnection,
|
|
85
|
+
OrgInvitation,
|
|
86
|
+
OrgMember,
|
|
87
|
+
OrgTeam,
|
|
88
|
+
OrgTeamMember,
|
|
89
|
+
OrgTeamRoleGrant,
|
|
90
|
+
OrgTemplate,
|
|
91
|
+
OrgTemplateText,
|
|
92
|
+
Page,
|
|
93
|
+
PreparedUpload,
|
|
94
|
+
Profile,
|
|
95
|
+
ProviderSession,
|
|
96
|
+
RoleOrigin,
|
|
97
|
+
Session,
|
|
98
|
+
SessionInfo,
|
|
99
|
+
SignInResult,
|
|
100
|
+
User,
|
|
101
|
+
VersionState,
|
|
102
|
+
)
|
|
103
|
+
from .oauth import MemoryOAuthStorage, OAuthReturn, OAuthStorage
|
|
104
|
+
from .query import (
|
|
105
|
+
Where,
|
|
106
|
+
and_,
|
|
107
|
+
between,
|
|
108
|
+
bucket_key,
|
|
109
|
+
contains,
|
|
110
|
+
ends_with,
|
|
111
|
+
eq,
|
|
112
|
+
expand,
|
|
113
|
+
fill_buckets,
|
|
114
|
+
gt,
|
|
115
|
+
gte,
|
|
116
|
+
icontains,
|
|
117
|
+
iends_with,
|
|
118
|
+
ilike,
|
|
119
|
+
in_,
|
|
120
|
+
is_null,
|
|
121
|
+
istarts_with,
|
|
122
|
+
like,
|
|
123
|
+
lt,
|
|
124
|
+
lte,
|
|
125
|
+
ne,
|
|
126
|
+
nin,
|
|
127
|
+
not_null,
|
|
128
|
+
or_,
|
|
129
|
+
search,
|
|
130
|
+
starts_with,
|
|
131
|
+
)
|
|
132
|
+
from .size import SizeFamily, format_bytes, parse_size
|
|
133
|
+
from .storage import FileStorage, MemoryStorage, SlotStorage, TokenStorage
|
|
134
|
+
from .upload import Progress, Upload, UploadSource, part_size_for
|
|
135
|
+
|
|
136
|
+
__version__ = "0.0.1"
|
|
137
|
+
|
|
138
|
+
__all__ = [
|
|
139
|
+
"AggregateGroup",
|
|
140
|
+
"AggregateNotEnabledError",
|
|
141
|
+
"AggregateResult",
|
|
142
|
+
"AggregateTooExpensiveError",
|
|
143
|
+
"AsyncClient",
|
|
144
|
+
"AsyncCollection",
|
|
145
|
+
"AsyncDatabase",
|
|
146
|
+
"AsyncJob",
|
|
147
|
+
"AsyncLiveView",
|
|
148
|
+
"AuthenticationError",
|
|
149
|
+
"BackdError",
|
|
150
|
+
"BatchOperation",
|
|
151
|
+
"CacheOptions",
|
|
152
|
+
"CacheStats",
|
|
153
|
+
"Client",
|
|
154
|
+
"Collection",
|
|
155
|
+
"ConfigStorage",
|
|
156
|
+
"ConflictError",
|
|
157
|
+
"ConsentChallenge",
|
|
158
|
+
"ConsentInfo",
|
|
159
|
+
"ConsentRequiredError",
|
|
160
|
+
"ConsentState",
|
|
161
|
+
"ConsentText",
|
|
162
|
+
"DataExport",
|
|
163
|
+
"Database",
|
|
164
|
+
"Device",
|
|
165
|
+
"DeviceInput",
|
|
166
|
+
"Doc",
|
|
167
|
+
"EnrollmentChallenge",
|
|
168
|
+
"ErrorDetail",
|
|
169
|
+
"ExpandTooLargeError",
|
|
170
|
+
"FieldEncryptedError",
|
|
171
|
+
"FileConfigStorage",
|
|
172
|
+
"FileDetails",
|
|
173
|
+
"FileField",
|
|
174
|
+
"FileLink",
|
|
175
|
+
"FileStorage",
|
|
176
|
+
"ForbiddenError",
|
|
177
|
+
"IdTakenError",
|
|
178
|
+
"Identity",
|
|
179
|
+
"IndexBuildingError",
|
|
180
|
+
"InvalidAggregateError",
|
|
181
|
+
"InvalidExpandError",
|
|
182
|
+
"InvalidIdError",
|
|
183
|
+
"InvalidResumeTokenError",
|
|
184
|
+
"Job",
|
|
185
|
+
"JobData",
|
|
186
|
+
"JobResult",
|
|
187
|
+
"JobStep",
|
|
188
|
+
"JobTimeoutError",
|
|
189
|
+
"LastOrgManagerError",
|
|
190
|
+
"LiveState",
|
|
191
|
+
"LiveStatus",
|
|
192
|
+
"LiveSubscriptionState",
|
|
193
|
+
"LiveView",
|
|
194
|
+
"Me",
|
|
195
|
+
"MemoryConfigStorage",
|
|
196
|
+
"MemoryOAuthStorage",
|
|
197
|
+
"MemoryStorage",
|
|
198
|
+
"Meta",
|
|
199
|
+
"MfaChallenge",
|
|
200
|
+
"NetworkError",
|
|
201
|
+
"NotAllowedError",
|
|
202
|
+
"NotFoundError",
|
|
203
|
+
"NotOrgMemberError",
|
|
204
|
+
"NotTeamMemberError",
|
|
205
|
+
"OAuthReturn",
|
|
206
|
+
"OAuthStorage",
|
|
207
|
+
"Org",
|
|
208
|
+
"OrgConnection",
|
|
209
|
+
"OrgInvitation",
|
|
210
|
+
"OrgMember",
|
|
211
|
+
"OrgRequiresMFAError",
|
|
212
|
+
"OrgRoleRequiresMFAError",
|
|
213
|
+
"OrgTeam",
|
|
214
|
+
"OrgTeamMember",
|
|
215
|
+
"OrgTeamRoleGrant",
|
|
216
|
+
"OrgTemplate",
|
|
217
|
+
"OrgTemplateText",
|
|
218
|
+
"Page",
|
|
219
|
+
"PasswordExpiredError",
|
|
220
|
+
"PreparedUpload",
|
|
221
|
+
"Profile",
|
|
222
|
+
"Progress",
|
|
223
|
+
"ProviderSession",
|
|
224
|
+
"RealtimeNotEnabledError",
|
|
225
|
+
"ReferenceSpec",
|
|
226
|
+
"Retry",
|
|
227
|
+
"RetryableError",
|
|
228
|
+
"RoleOrigin",
|
|
229
|
+
"SearchNotEnabledError",
|
|
230
|
+
"SearchTooExpensiveError",
|
|
231
|
+
"SecondStepRequiredError",
|
|
232
|
+
"Session",
|
|
233
|
+
"SessionEndedError",
|
|
234
|
+
"SessionInfo",
|
|
235
|
+
"SignInResult",
|
|
236
|
+
"SizeFamily",
|
|
237
|
+
"SlotStorage",
|
|
238
|
+
"SsoRequiredError",
|
|
239
|
+
"SsoSessionError",
|
|
240
|
+
"SubscribeOptions",
|
|
241
|
+
"SubscriptionSpec",
|
|
242
|
+
"TeamError",
|
|
243
|
+
"TokenStorage",
|
|
244
|
+
"TooManySubscriptionsError",
|
|
245
|
+
"Upload",
|
|
246
|
+
"UploadSource",
|
|
247
|
+
"User",
|
|
248
|
+
"ValidationError",
|
|
249
|
+
"VerificationRequiredError",
|
|
250
|
+
"VersionMismatchError",
|
|
251
|
+
"VersionState",
|
|
252
|
+
"Where",
|
|
253
|
+
"__version__",
|
|
254
|
+
"and_",
|
|
255
|
+
"between",
|
|
256
|
+
"bucket_key",
|
|
257
|
+
"contains",
|
|
258
|
+
"ends_with",
|
|
259
|
+
"eq",
|
|
260
|
+
"expand",
|
|
261
|
+
"fill_buckets",
|
|
262
|
+
"format_bytes",
|
|
263
|
+
"gt",
|
|
264
|
+
"gte",
|
|
265
|
+
"icontains",
|
|
266
|
+
"iends_with",
|
|
267
|
+
"ilike",
|
|
268
|
+
"in_",
|
|
269
|
+
"is_null",
|
|
270
|
+
"istarts_with",
|
|
271
|
+
"like",
|
|
272
|
+
"lt",
|
|
273
|
+
"lte",
|
|
274
|
+
"ne",
|
|
275
|
+
"nin",
|
|
276
|
+
"not_null",
|
|
277
|
+
"or_",
|
|
278
|
+
"parse_size",
|
|
279
|
+
"part_size_for",
|
|
280
|
+
"public_url",
|
|
281
|
+
"search",
|
|
282
|
+
"starts_with",
|
|
283
|
+
"version_status",
|
|
284
|
+
]
|
backd/_aio.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""The few places where the async client needs a different function from the sync one.
|
|
2
|
+
``scripts/unasync.py`` swaps this module for ``_sio`` when it writes ``_sync``."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
from collections.abc import Awaitable, Callable, Coroutine
|
|
8
|
+
from typing import Any, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def sleep(seconds: float) -> None:
|
|
14
|
+
await asyncio.sleep(seconds)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_tasks: set[asyncio.Task[None]] = set()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def spawn(fn: Callable[..., Awaitable[None]], *args: Any) -> None:
|
|
21
|
+
"""Runs ``fn(*args)`` in the background: a task here, a thread in the sync client."""
|
|
22
|
+
task: asyncio.Task[None] = asyncio.get_running_loop().create_task(cast("Coroutine[Any, Any, None]", fn(*args)))
|
|
23
|
+
_tasks.add(task) # a task nothing refers to may be collected before it ends
|
|
24
|
+
task.add_done_callback(_tasks.discard)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def run_blocking(fn: Callable[..., T], *args: Any) -> T:
|
|
28
|
+
"""Runs a blocking function (hashing a big file) without stopping the event loop; the sync client just calls it."""
|
|
29
|
+
return await asyncio.to_thread(fn, *args)
|
backd/_async/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""The async client. These files are the source of ``_sync``; run ``python scripts/unasync.py``."""
|
backd/_async/account.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""The signed-in user's own account data: ``client.account``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import AsyncIterator, Callable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import TYPE_CHECKING, Any, BinaryIO, cast
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from .._aio import sleep
|
|
14
|
+
from ..errors import BackdError
|
|
15
|
+
from ..models import DataExport
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from .client import AsyncClient
|
|
19
|
+
|
|
20
|
+
__all__ = ["AsyncAccount", "AsyncExportDownload"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AsyncExportDownload:
|
|
24
|
+
"""A ready export, to save. ``url`` is a link signed for a few minutes, when the realm delivers by link
|
|
25
|
+
(``expires_at`` says when it stops working); a realm whose storage clients cannot reach sends the zip through the
|
|
26
|
+
API and ``url`` is ``None``. Either way :meth:`read` and :meth:`save` give the zip."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
client: AsyncClient,
|
|
31
|
+
response: httpx.Response | None,
|
|
32
|
+
url: str | None = None,
|
|
33
|
+
expires_at: str | None = None,
|
|
34
|
+
) -> None:
|
|
35
|
+
self._client = client
|
|
36
|
+
self._response = response
|
|
37
|
+
self.url = url
|
|
38
|
+
self.expires_at = expires_at
|
|
39
|
+
|
|
40
|
+
async def _stream(self) -> AsyncIterator[bytes]:
|
|
41
|
+
if self._response is None:
|
|
42
|
+
assert self.url is not None
|
|
43
|
+
response = await self._client.open_external("GET", self.url)
|
|
44
|
+
if not response.is_success:
|
|
45
|
+
await response.aclose()
|
|
46
|
+
raise BackdError(
|
|
47
|
+
f"the export link answered {response.status_code}",
|
|
48
|
+
status=response.status_code,
|
|
49
|
+
code="download_failed",
|
|
50
|
+
)
|
|
51
|
+
else:
|
|
52
|
+
response = self._response
|
|
53
|
+
try:
|
|
54
|
+
async for chunk in response.aiter_bytes():
|
|
55
|
+
yield chunk
|
|
56
|
+
finally:
|
|
57
|
+
await response.aclose()
|
|
58
|
+
|
|
59
|
+
async def read(self) -> bytes:
|
|
60
|
+
"""The zip's bytes."""
|
|
61
|
+
return b"".join([chunk async for chunk in self._stream()])
|
|
62
|
+
|
|
63
|
+
async def save(self, to: str | os.PathLike[str] | BinaryIO) -> int:
|
|
64
|
+
"""Writes the zip to a path or an open binary file as it arrives; returns the bytes written."""
|
|
65
|
+
n = 0
|
|
66
|
+
if hasattr(to, "write"):
|
|
67
|
+
async for chunk in self._stream():
|
|
68
|
+
cast(BinaryIO, to).write(chunk)
|
|
69
|
+
n += len(chunk)
|
|
70
|
+
return n
|
|
71
|
+
with Path(to).open("wb") as f:
|
|
72
|
+
async for chunk in self._stream():
|
|
73
|
+
f.write(chunk)
|
|
74
|
+
n += len(chunk)
|
|
75
|
+
return n
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class AsyncAccount:
|
|
79
|
+
"""The signed-in user's own account data: ``client.account``. A person can download a copy of everything the
|
|
80
|
+
realm holds about them (documents, files and account data, with a ``README.html``): ask for it, follow it, and
|
|
81
|
+
save it when it is ready.
|
|
82
|
+
|
|
83
|
+
client.auth.reauth({"method": "password", "password": password}) # a fresh proof, as for any sensitive action
|
|
84
|
+
job = client.account.request_export() # {"id": …, "status": "queued", …}
|
|
85
|
+
ready = client.account.wait_for_export(job["id"]) # polls until it is ready
|
|
86
|
+
client.account.download_export(ready["id"]).save("export.zip")
|
|
87
|
+
|
|
88
|
+
The realm must set ``compliance.export: true`` (:class:`NotFoundError` otherwise). A user may ask once every 24
|
|
89
|
+
hours: a second request is a :class:`RetryableError` (``429``) whose ``retry_after`` says, in seconds, how long to
|
|
90
|
+
wait. When the package is ready the user is also emailed a link to the app's download page.
|
|
91
|
+
"""
|
|
92
|
+
|
|
93
|
+
def __init__(self, client: AsyncClient) -> None:
|
|
94
|
+
self._client = client
|
|
95
|
+
|
|
96
|
+
async def request_export(self) -> DataExport:
|
|
97
|
+
"""Asks for an export of the user's personal data. Needs a fresh proof: call ``client.auth.reauth()`` first (a
|
|
98
|
+
``reauth_required`` error says so). It is built in the background."""
|
|
99
|
+
res = await self._client.request("POST", ["_auth", "me", "export"], reauth=True)
|
|
100
|
+
return cast(DataExport, res.data)
|
|
101
|
+
|
|
102
|
+
async def exports(self) -> list[DataExport]:
|
|
103
|
+
"""The exports the user asked for, newest first."""
|
|
104
|
+
res = await self._client.request("GET", ["_auth", "me", "exports"], cache=False)
|
|
105
|
+
return cast("list[DataExport]", cast(dict[str, Any], res.data)["items"])
|
|
106
|
+
|
|
107
|
+
async def export(self, export_id: str) -> DataExport:
|
|
108
|
+
"""One export, for a page that waits for it: ``step`` says what it is doing while it runs."""
|
|
109
|
+
res = await self._client.request("GET", ["_auth", "me", "exports", export_id], cache=False)
|
|
110
|
+
return cast(DataExport, res.data)
|
|
111
|
+
|
|
112
|
+
async def wait_for_export(
|
|
113
|
+
self,
|
|
114
|
+
export_id: str,
|
|
115
|
+
*,
|
|
116
|
+
poll_interval: float = 2.0,
|
|
117
|
+
timeout: float = 1800.0,
|
|
118
|
+
on_progress: Callable[[DataExport], None] | None = None,
|
|
119
|
+
) -> DataExport:
|
|
120
|
+
"""Polls an export until it is ``ready``. A ``failed`` or ``expired`` one raises a :class:`BackdError` (code
|
|
121
|
+
``export_failed`` or ``export_expired``: the user may ask again), and so does waiting longer than ``timeout``
|
|
122
|
+
(``export_timeout``). Polls every 2 s for up to 30 minutes by default."""
|
|
123
|
+
end = time.monotonic() + timeout
|
|
124
|
+
while True:
|
|
125
|
+
e = await self.export(export_id)
|
|
126
|
+
if on_progress is not None:
|
|
127
|
+
on_progress(e)
|
|
128
|
+
if e["status"] == "ready":
|
|
129
|
+
return e
|
|
130
|
+
if e["status"] in ("failed", "expired"):
|
|
131
|
+
hint = ": ask for a new one" if e["status"] == "failed" else ""
|
|
132
|
+
raise BackdError(f"the export {e['status']}{hint}", code=f"export_{e['status']}")
|
|
133
|
+
if time.monotonic() + poll_interval > end:
|
|
134
|
+
raise BackdError(f"the export is still {e['status']} after {timeout:.0f}s", code="export_timeout")
|
|
135
|
+
await sleep(poll_interval)
|
|
136
|
+
|
|
137
|
+
async def download_export(self, export_id: str) -> AsyncExportDownload:
|
|
138
|
+
"""Gives access to a ready export. :class:`ConflictError` until it is ready, and ``410 gone`` once it has
|
|
139
|
+
expired. A realm that delivers by link answers with ``url`` (open it, or :meth:`~AsyncExportDownload.read`
|
|
140
|
+
reads it); one whose storage clients cannot reach sends the zip through the API."""
|
|
141
|
+
res = await self._client.request(
|
|
142
|
+
"GET",
|
|
143
|
+
["_auth", "me", "exports", export_id, "download"],
|
|
144
|
+
raw=True,
|
|
145
|
+
headers={"Accept": "application/json"},
|
|
146
|
+
)
|
|
147
|
+
assert res.response is not None
|
|
148
|
+
if not res.response.headers.get("Content-Type", "").startswith("application/json"):
|
|
149
|
+
return AsyncExportDownload(self._client, res.response)
|
|
150
|
+
await res.response.aread()
|
|
151
|
+
await res.response.aclose()
|
|
152
|
+
link = res.response.json()
|
|
153
|
+
return AsyncExportDownload(self._client, None, link.get("url"), link.get("expires_at"))
|