flowpydisk 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.
- flowpydisk/__init__.py +50 -0
- flowpydisk/_client.py +266 -0
- flowpydisk/_errors.py +118 -0
- flowpydisk/_generated/__init__.py +0 -0
- flowpydisk/_generated/operations.py +5003 -0
- flowpydisk/files.py +272 -0
- flowpydisk/inbound_webhooks.py +34 -0
- flowpydisk/index.py +44 -0
- flowpydisk/webhooks.py +42 -0
- flowpydisk-0.0.1.dist-info/METADATA +156 -0
- flowpydisk-0.0.1.dist-info/RECORD +14 -0
- flowpydisk-0.0.1.dist-info/WHEEL +5 -0
- flowpydisk-0.0.1.dist-info/licenses/LICENSE +201 -0
- flowpydisk-0.0.1.dist-info/top_level.txt +1 -0
flowpydisk/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Python client for the FlowpyDisk API.
|
|
2
|
+
|
|
3
|
+
from flowpydisk import FlowpyDisk
|
|
4
|
+
|
|
5
|
+
client = FlowpyDisk(api_key="fd_…")
|
|
6
|
+
for f in client.paginate("storage/files"):
|
|
7
|
+
print(f["name"])
|
|
8
|
+
|
|
9
|
+
The shape and the reasoning behind every choice here are in the
|
|
10
|
+
FlowpyDisk SDK design plan.
|
|
11
|
+
|
|
12
|
+
**What is built:** the transport (C2) - auth, the error taxonomy, retries,
|
|
13
|
+
pagination; the generated operations (C3), one namespace per tag in the spec
|
|
14
|
+
(`client.storage.list_files()`, `client.tables.rows_tables(...)`); and the
|
|
15
|
+
hand-written layer over them (C4) - `client.files.upload(path)`, the three-step
|
|
16
|
+
upload as one call, and `client.index`. Everything a caller imports is
|
|
17
|
+
re-exported from this file, so `_generated` stays private and a regeneration
|
|
18
|
+
cannot change what a caller imports.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from ._client import DEFAULT_BASE_URL, FlowpyDisk
|
|
22
|
+
from ._errors import (
|
|
23
|
+
AuthError,
|
|
24
|
+
FlowpyDiskError,
|
|
25
|
+
InvalidRequest,
|
|
26
|
+
LimitExceeded,
|
|
27
|
+
NotFound,
|
|
28
|
+
Throttled,
|
|
29
|
+
)
|
|
30
|
+
from .files import Files
|
|
31
|
+
from .inbound_webhooks import sign_inbound_webhook
|
|
32
|
+
from .webhooks import SIGNATURE_HEADER, verify_webhook
|
|
33
|
+
|
|
34
|
+
__version__ = "0.0.1"
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"DEFAULT_BASE_URL",
|
|
38
|
+
"SIGNATURE_HEADER",
|
|
39
|
+
"AuthError",
|
|
40
|
+
"Files",
|
|
41
|
+
"FlowpyDisk",
|
|
42
|
+
"FlowpyDiskError",
|
|
43
|
+
"InvalidRequest",
|
|
44
|
+
"LimitExceeded",
|
|
45
|
+
"NotFound",
|
|
46
|
+
"Throttled",
|
|
47
|
+
"__version__",
|
|
48
|
+
"sign_inbound_webhook",
|
|
49
|
+
"verify_webhook",
|
|
50
|
+
]
|
flowpydisk/_client.py
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""The transport: auth, errors, retries, pagination.
|
|
2
|
+
|
|
3
|
+
SDK design plan, sections 3 and 4. Everything a generated
|
|
4
|
+
operation needs and cannot have. The generated layer (C3) will call
|
|
5
|
+
``client.request(...)``; nothing below it knows about any particular endpoint,
|
|
6
|
+
which is why this file can be finished before the generator is chosen.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Iterator
|
|
13
|
+
from typing import Any
|
|
14
|
+
from urllib.parse import urljoin
|
|
15
|
+
|
|
16
|
+
import httpx
|
|
17
|
+
|
|
18
|
+
from ._errors import BY_STATUS, FlowpyDiskError, Throttled
|
|
19
|
+
from ._generated.operations import ALIASES, NAMESPACES
|
|
20
|
+
from .files import Files
|
|
21
|
+
from .index import Index
|
|
22
|
+
|
|
23
|
+
DEFAULT_BASE_URL = "https://app.flowpydisk.com"
|
|
24
|
+
|
|
25
|
+
#: Verbs that may be retried. A retried POST is a duplicate row: this product
|
|
26
|
+
#: has no idempotency keys, so "the connection dropped" and "it was created and
|
|
27
|
+
#: the response was lost" are indistinguishable from here. DELETE is in because
|
|
28
|
+
#: deleting twice is the same state as deleting once; POST never is.
|
|
29
|
+
RETRYABLE_METHODS = frozenset({"GET", "HEAD", "PUT", "DELETE"})
|
|
30
|
+
|
|
31
|
+
#: 5xx and 429 only. A 4xx will be refused identically however many times it is
|
|
32
|
+
#: sent, and retrying it just makes the caller wait to see their own mistake.
|
|
33
|
+
RETRYABLE_STATUS = frozenset({429, 500, 502, 503, 504})
|
|
34
|
+
|
|
35
|
+
DEFAULT_RETRIES = 2
|
|
36
|
+
DEFAULT_TIMEOUT = 30.0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FlowpyDisk:
|
|
40
|
+
"""A client for the one space its API key was made in.
|
|
41
|
+
|
|
42
|
+
client = FlowpyDisk(api_key="fd_…")
|
|
43
|
+
|
|
44
|
+
A key acts in its own space and nowhere else: a key made in your personal
|
|
45
|
+
space acts there, a key made in a workspace acts in that workspace. To act
|
|
46
|
+
in a workspace, mint a key in that workspace (Settings > API Keys).
|
|
47
|
+
|
|
48
|
+
There is no ``workspace`` argument (removed 2026-11-10, M3762). It sent a
|
|
49
|
+
``Workspace-Id`` header, and the server ignores that header for every API
|
|
50
|
+
key, so a caller who passed it was told they acted in a workspace while
|
|
51
|
+
acting in the key's own space.
|
|
52
|
+
|
|
53
|
+
**There is no session-cookie mode**, and the absence is a decision rather
|
|
54
|
+
than an omission - see SDK_PLAN §3. A program authenticates with an API key
|
|
55
|
+
minted in Settings.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
api_key: str,
|
|
61
|
+
*,
|
|
62
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
63
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
64
|
+
retries: int = DEFAULT_RETRIES,
|
|
65
|
+
transport: httpx.BaseTransport | None = None,
|
|
66
|
+
) -> None:
|
|
67
|
+
if not api_key:
|
|
68
|
+
raise ValueError("api_key is required. Mint one in Settings > API Keys.")
|
|
69
|
+
self.base_url = base_url.rstrip("/") + "/"
|
|
70
|
+
self.retries = retries
|
|
71
|
+
headers = {
|
|
72
|
+
"Authorization": f"Bearer {api_key}",
|
|
73
|
+
"Accept": "application/json",
|
|
74
|
+
"User-Agent": "flowpydisk-python/0.0.1",
|
|
75
|
+
}
|
|
76
|
+
self._http = httpx.Client(headers=headers, timeout=timeout, transport=transport)
|
|
77
|
+
# One attribute per tag in the spec: client.storage, client.tables, …
|
|
78
|
+
# Built from the generated map rather than written out, so a new tag
|
|
79
|
+
# arrives with the regeneration and no edit here - which is the whole
|
|
80
|
+
# point of the layer being generated.
|
|
81
|
+
for attr, cls in NAMESPACES.items():
|
|
82
|
+
setattr(self, attr, cls(self))
|
|
83
|
+
# A renamed tag keeps its older name as the same object, so
|
|
84
|
+
# `client.ai_units is client.credits` (M4030). No warning: the older
|
|
85
|
+
# name is promised, and the README says which one is current.
|
|
86
|
+
for older, now in ALIASES.items():
|
|
87
|
+
setattr(self, older, getattr(self, now))
|
|
88
|
+
# The hand-written layer, over the top. `files` deliberately SHADOWS
|
|
89
|
+
# nothing: the generated storage namespace keeps every endpoint, and
|
|
90
|
+
# this adds the one call that is several endpoints (SDK_PLAN §4).
|
|
91
|
+
self.files = Files(self)
|
|
92
|
+
# "Index this file" for held files, one selector at a time (P7).
|
|
93
|
+
self.index = Index(self)
|
|
94
|
+
|
|
95
|
+
# -- lifecycle ---------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
def close(self) -> None:
|
|
98
|
+
self._http.close()
|
|
99
|
+
|
|
100
|
+
def __enter__(self) -> FlowpyDisk:
|
|
101
|
+
return self
|
|
102
|
+
|
|
103
|
+
def __exit__(self, *exc: object) -> None:
|
|
104
|
+
self.close()
|
|
105
|
+
|
|
106
|
+
# -- the one call everything else is built on --------------------------
|
|
107
|
+
|
|
108
|
+
def request(self, method: str, path: str, **kw: Any) -> Any:
|
|
109
|
+
"""Send one request, raise on refusal, return the decoded body.
|
|
110
|
+
|
|
111
|
+
``path`` is relative to the API root (``storage/files``), so a
|
|
112
|
+
generated operation never has to know the deployment's hostname.
|
|
113
|
+
"""
|
|
114
|
+
url = urljoin(self.base_url, f"api/v1/{path.lstrip('/')}")
|
|
115
|
+
method = method.upper()
|
|
116
|
+
attempts = self.retries + 1 if method in RETRYABLE_METHODS else 1
|
|
117
|
+
|
|
118
|
+
last: httpx.Response | None = None
|
|
119
|
+
for attempt in range(attempts):
|
|
120
|
+
response = self._http.request(method, url, **kw)
|
|
121
|
+
last = response
|
|
122
|
+
if response.status_code not in RETRYABLE_STATUS or attempt == attempts - 1:
|
|
123
|
+
break
|
|
124
|
+
time.sleep(_backoff(response, attempt))
|
|
125
|
+
|
|
126
|
+
assert last is not None # noqa: S101 - the loop runs at least once
|
|
127
|
+
if last.status_code >= 400:
|
|
128
|
+
self._raise(last)
|
|
129
|
+
if last.status_code == 204 or not last.content:
|
|
130
|
+
return None
|
|
131
|
+
# A picture the API streams (the marketplace preview's pages, reduced
|
|
132
|
+
# images and posters, M1008) is its bytes, not JSON.
|
|
133
|
+
if last.headers.get("content-type", "").startswith("image/"):
|
|
134
|
+
return last.content
|
|
135
|
+
return last.json()
|
|
136
|
+
|
|
137
|
+
def get(self, path: str, **kw: Any) -> Any:
|
|
138
|
+
return self.request("GET", path, **kw)
|
|
139
|
+
|
|
140
|
+
def post(self, path: str, **kw: Any) -> Any:
|
|
141
|
+
return self.request("POST", path, **kw)
|
|
142
|
+
|
|
143
|
+
def patch(self, path: str, **kw: Any) -> Any:
|
|
144
|
+
return self.request("PATCH", path, **kw)
|
|
145
|
+
|
|
146
|
+
def delete(self, path: str, **kw: Any) -> Any:
|
|
147
|
+
return self.request("DELETE", path, **kw)
|
|
148
|
+
|
|
149
|
+
def blob_put(self, path: str, **kw: Any) -> httpx.Response:
|
|
150
|
+
"""PUT to a same-origin blob path, with this client's headers.
|
|
151
|
+
|
|
152
|
+
For the RELATIVE presigned URLs a dev backend hands back
|
|
153
|
+
(`/api/v1/storage/_dev/blob/…`). They are paths on our own origin and
|
|
154
|
+
need the Bearer header; an absolute R2 link must not get it, and
|
|
155
|
+
`files._blob_put` is where that fork lives. Raw response, not decoded:
|
|
156
|
+
the caller wants the ETag header, and a blob PUT has no JSON body.
|
|
157
|
+
"""
|
|
158
|
+
return self._http.put(urljoin(self.base_url, path.lstrip("/")), **kw)
|
|
159
|
+
|
|
160
|
+
# -- pagination --------------------------------------------------------
|
|
161
|
+
|
|
162
|
+
def paginate(self, path: str, *, limit: int = 200, **kw: Any) -> Iterator[dict]:
|
|
163
|
+
"""Every row of a paged listing, one at a time.
|
|
164
|
+
|
|
165
|
+
Safe to offer only since the ordering tiebreak landed on 2026-09-14,
|
|
166
|
+
and it is worth knowing why. These listings ordered by ``-created_at``
|
|
167
|
+
with no tiebreak, so two rows sharing a timestamp had no defined order
|
|
168
|
+
between pages: walking the list could return one of them twice and the
|
|
169
|
+
other never, and the "never" half was invisible to every client.
|
|
170
|
+
``-id`` breaks the tie on all six paged listings now, so a walk is a
|
|
171
|
+
walk.
|
|
172
|
+
|
|
173
|
+
The entry is M17 of the mobile app requirements, not of the findings
|
|
174
|
+
register, which has its own unrelated M17.
|
|
175
|
+
"""
|
|
176
|
+
offset = 0
|
|
177
|
+
params = dict(kw.pop("params", {}) or {})
|
|
178
|
+
while True:
|
|
179
|
+
page = self.request("GET", path, params={**params, "limit": limit, "offset": offset}, **kw)
|
|
180
|
+
rows = page if isinstance(page, list) else page.get("results", [])
|
|
181
|
+
yield from rows
|
|
182
|
+
if len(rows) < limit:
|
|
183
|
+
return
|
|
184
|
+
offset += limit
|
|
185
|
+
|
|
186
|
+
# -- errors ------------------------------------------------------------
|
|
187
|
+
|
|
188
|
+
def _raise(self, response: httpx.Response) -> None:
|
|
189
|
+
code: str | None = None
|
|
190
|
+
detail: Any = None
|
|
191
|
+
try:
|
|
192
|
+
body = response.json()
|
|
193
|
+
except ValueError:
|
|
194
|
+
# A proxy's HTML 502, a gateway timeout page. There is no envelope
|
|
195
|
+
# to read, and inventing a code for it would make `.code` a value
|
|
196
|
+
# the backend never sent.
|
|
197
|
+
body = None
|
|
198
|
+
if isinstance(body, dict):
|
|
199
|
+
code = body.get("code")
|
|
200
|
+
detail = body.get("detail", body.get("error", body))
|
|
201
|
+
|
|
202
|
+
message = _readable(detail) or response.reason_phrase or f"HTTP {response.status_code}"
|
|
203
|
+
cls = BY_STATUS.get(response.status_code, FlowpyDiskError)
|
|
204
|
+
|
|
205
|
+
kw: dict[str, Any] = {
|
|
206
|
+
"status": response.status_code,
|
|
207
|
+
"code": code,
|
|
208
|
+
"detail": detail,
|
|
209
|
+
"response": response,
|
|
210
|
+
}
|
|
211
|
+
if cls is Throttled:
|
|
212
|
+
raise Throttled(message, retry_after=_retry_after(response), **kw)
|
|
213
|
+
raise cls(message, **kw)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _readable(detail: Any) -> str:
|
|
217
|
+
"""A sentence, from whichever shape the backend used.
|
|
218
|
+
|
|
219
|
+
Three shapes reach here and only one of them reads well on its own:
|
|
220
|
+
|
|
221
|
+
{"detail": "File exceeds the 20 GB per-file limit."} -> the string
|
|
222
|
+
{"name": ["Ensure this field has no more than 255…"]} -> DRF's field errors
|
|
223
|
+
{} -> nothing at all
|
|
224
|
+
|
|
225
|
+
The middle one is a serializer refusal, and it is common - the quickstart
|
|
226
|
+
hit it on its first real run against a live server. Left alone it reaches
|
|
227
|
+
the caller as `{'name': ['Ensure this field has…']}`, a dict repr in an
|
|
228
|
+
exception message, which is the kind of thing that makes an SDK feel like
|
|
229
|
+
a thin wrapper over `requests`. Flattened it is "name: Ensure this field
|
|
230
|
+
has no more than 255 characters." and needs no explanation.
|
|
231
|
+
"""
|
|
232
|
+
if not detail:
|
|
233
|
+
return ""
|
|
234
|
+
if isinstance(detail, str):
|
|
235
|
+
return detail
|
|
236
|
+
if isinstance(detail, list):
|
|
237
|
+
return "; ".join(_readable(d) for d in detail if d)
|
|
238
|
+
if isinstance(detail, dict):
|
|
239
|
+
return "; ".join(f"{field}: {_readable(problem)}" for field, problem in detail.items() if problem)
|
|
240
|
+
return str(detail)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _retry_after(response: httpx.Response) -> float | None:
|
|
244
|
+
raw = response.headers.get("Retry-After")
|
|
245
|
+
if not raw:
|
|
246
|
+
return None
|
|
247
|
+
try:
|
|
248
|
+
return float(raw)
|
|
249
|
+
except ValueError:
|
|
250
|
+
# The header also allows an HTTP-date. Rare here, and a wrong number is
|
|
251
|
+
# worse than none: a caller who sleeps on a misparsed date sleeps for
|
|
252
|
+
# the wrong length and blames the SDK.
|
|
253
|
+
return None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _backoff(response: httpx.Response, attempt: int) -> float:
|
|
257
|
+
"""The server's number if it gave one, otherwise 0.5s, 1s, 2s.
|
|
258
|
+
|
|
259
|
+
`Retry-After` wins over the schedule, always. The server knows when its
|
|
260
|
+
window reopens and the client is guessing; ignoring it is how a client
|
|
261
|
+
retries into the same 429 three times and calls the API flaky.
|
|
262
|
+
"""
|
|
263
|
+
told = _retry_after(response)
|
|
264
|
+
if told is not None:
|
|
265
|
+
return told
|
|
266
|
+
return 0.5 * (2**attempt)
|
flowpydisk/_errors.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Five classes, and every error code the backend has.
|
|
2
|
+
|
|
3
|
+
SDK design plan, section 5. The worklist's C5 row asks for
|
|
4
|
+
"quota, size cap, scoped-key not-found, throttles, each with its own exception
|
|
5
|
+
and message". Measured against the tree on 2026-09-15, the backend returns
|
|
6
|
+
**67 distinct ``code`` values**, and sixty-seven exception classes is a
|
|
7
|
+
taxonomy nobody can hold in their head and an ``except`` clause nobody can
|
|
8
|
+
write.
|
|
9
|
+
|
|
10
|
+
So the split is by **what you can do about it**, and the exact code survives
|
|
11
|
+
on ``.code``:
|
|
12
|
+
|
|
13
|
+
except Throttled as e: # back off, e.retry_after says how long
|
|
14
|
+
except LimitExceeded as e: # ask for more room, or send less
|
|
15
|
+
if e.code == "file_too_large": … # ...or handle exactly this one
|
|
16
|
+
|
|
17
|
+
Nothing is swallowed. ``.status``, ``.code``, ``.detail`` and the response
|
|
18
|
+
itself are all on the exception, so a caller who needs something this module
|
|
19
|
+
did not anticipate still has it.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FlowpyDiskError(Exception):
|
|
28
|
+
"""Anything the API refused, plus what it said about it.
|
|
29
|
+
|
|
30
|
+
``code`` is the backend's own string (``file_too_large``, ``no_workspace``,
|
|
31
|
+
``kind_not_publishable``, …) and is the value to branch on. It is ``None``
|
|
32
|
+
only when a response carried no envelope at all - a proxy's 502, say - and
|
|
33
|
+
that is worth distinguishing from an empty string, which would read as "the
|
|
34
|
+
backend sent a code and it was blank".
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
message: str,
|
|
40
|
+
*,
|
|
41
|
+
status: int | None = None,
|
|
42
|
+
code: str | None = None,
|
|
43
|
+
detail: Any = None,
|
|
44
|
+
response: Any = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
super().__init__(message)
|
|
47
|
+
self.status = status
|
|
48
|
+
self.code = code
|
|
49
|
+
self.detail = detail
|
|
50
|
+
self.response = response
|
|
51
|
+
|
|
52
|
+
def __str__(self) -> str:
|
|
53
|
+
base = super().__str__()
|
|
54
|
+
# The code in the repr, always. A traceback that says only "Not found"
|
|
55
|
+
# sends somebody to the wrong problem; one that says
|
|
56
|
+
# "Not found (code=folder_not_found, status=404)" does not.
|
|
57
|
+
bits = [f"status={self.status}" if self.status else "", f"code={self.code}" if self.code else ""]
|
|
58
|
+
suffix = ", ".join(b for b in bits if b)
|
|
59
|
+
return f"{base} ({suffix})" if suffix else base
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class AuthError(FlowpyDiskError):
|
|
63
|
+
"""401 and 403. The key is wrong, expired, or not allowed to do this."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class NotFound(FlowpyDiskError):
|
|
67
|
+
"""404. The object does not exist, or is outside the space this key acts
|
|
68
|
+
in: the server answers not-found rather than forbidden, on purpose,
|
|
69
|
+
because forbidden would confirm the thing exists.
|
|
70
|
+
|
|
71
|
+
The client once asked the server whether a folder-scoped key was behind
|
|
72
|
+
a 404 and named the folder. Folder keys are retired, so it no longer asks
|
|
73
|
+
(M2893).
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class InvalidRequest(FlowpyDiskError):
|
|
78
|
+
"""400 and 422. The call itself was wrong - a bad filter, an unknown kind."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class LimitExceeded(FlowpyDiskError):
|
|
82
|
+
"""402 and 413. Quota, the per-file cap, a plan limit.
|
|
83
|
+
|
|
84
|
+
Distinct from InvalidRequest because the remedy is different in kind: the
|
|
85
|
+
call was well formed and the account cannot currently accommodate it.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Throttled(FlowpyDiskError):
|
|
90
|
+
"""429. ``retry_after`` is seconds, when the response said so."""
|
|
91
|
+
|
|
92
|
+
def __init__(self, message: str, *, retry_after: float | None = None, **kw: Any) -> None:
|
|
93
|
+
super().__init__(message, **kw)
|
|
94
|
+
self.retry_after = retry_after
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
#: Status -> class. Anything unmapped raises the base class rather than being
|
|
98
|
+
#: forced into the nearest one: a 418 or a 502 is not an InvalidRequest, and
|
|
99
|
+
#: saying so wrongly is worse than saying nothing.
|
|
100
|
+
BY_STATUS: dict[int, type[FlowpyDiskError]] = {
|
|
101
|
+
400: InvalidRequest,
|
|
102
|
+
401: AuthError,
|
|
103
|
+
402: LimitExceeded,
|
|
104
|
+
403: AuthError,
|
|
105
|
+
404: NotFound,
|
|
106
|
+
413: LimitExceeded,
|
|
107
|
+
422: InvalidRequest,
|
|
108
|
+
429: Throttled,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
__all__ = [
|
|
112
|
+
"AuthError",
|
|
113
|
+
"FlowpyDiskError",
|
|
114
|
+
"InvalidRequest",
|
|
115
|
+
"LimitExceeded",
|
|
116
|
+
"NotFound",
|
|
117
|
+
"Throttled",
|
|
118
|
+
]
|
|
File without changes
|