UGOS 1.0.0__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.
- ugos/__about__.py +6 -0
- ugos/__init__.py +92 -0
- ugos/_auth.py +293 -0
- ugos/_client_base.py +52 -0
- ugos/_client_calls.py +200 -0
- ugos/_client_helpers.py +217 -0
- ugos/_client_lifecycle.py +144 -0
- ugos/_client_resources.py +192 -0
- ugos/_client_transfer.py +501 -0
- ugos/_codec.py +528 -0
- ugos/_config.py +183 -0
- ugos/_data/__init__.py +1 -0
- ugos/_data/action_contracts.json +325 -0
- ugos/_data/catalog.json +1256 -0
- ugos/_dynamic.py +221 -0
- ugos/_encoding.py +159 -0
- ugos/_pagination.py +158 -0
- ugos/_redaction.py +158 -0
- ugos/_transport.py +446 -0
- ugos/catalog.py +184 -0
- ugos/client.py +113 -0
- ugos/exceptions.py +137 -0
- ugos/models/__init__.py +37 -0
- ugos/models/common.py +87 -0
- ugos/models/entities.py +124 -0
- ugos/models/users.py +43 -0
- ugos/py.typed +0 -0
- ugos/resources/__init__.py +55 -0
- ugos/resources/administrators.py +71 -0
- ugos/resources/application.py +107 -0
- ugos/resources/base.py +38 -0
- ugos/resources/certificates.py +62 -0
- ugos/resources/contentrules.py +98 -0
- ugos/resources/contenttypes.py +9 -0
- ugos/resources/fwrules.py +103 -0
- ugos/resources/generic.py +283 -0
- ugos/resources/generic_controllers.py +169 -0
- ugos/resources/groups.py +94 -0
- ugos/resources/icap.py +132 -0
- ugos/resources/idps.py +125 -0
- ugos/resources/interfaces.py +40 -0
- ugos/resources/l7.py +110 -0
- ugos/resources/licensing.py +39 -0
- ugos/resources/mailsecurity.py +101 -0
- ugos/resources/networks.py +82 -0
- ugos/resources/netzones.py +78 -0
- ugos/resources/nlist.py +72 -0
- ugos/resources/overridedomains.py +47 -0
- ugos/resources/overrides.py +109 -0
- ugos/resources/overrides_content.py +254 -0
- ugos/resources/overrides_content_a.py +187 -0
- ugos/resources/overrides_lists.py +294 -0
- ugos/resources/overrides_misc.py +342 -0
- ugos/resources/overrides_network.py +291 -0
- ugos/resources/pcap.py +64 -0
- ugos/resources/registry.py +55 -0
- ugos/resources/settings.py +72 -0
- ugos/resources/sysinfo.py +39 -0
- ugos/resources/twofa.py +62 -0
- ugos/resources/urlcategory.py +69 -0
- ugos/resources/urlfiltering.py +23 -0
- ugos/resources/urllists.py +9 -0
- ugos/resources/users.py +74 -0
- ugos-1.0.0.dist-info/METADATA +139 -0
- ugos-1.0.0.dist-info/RECORD +67 -0
- ugos-1.0.0.dist-info/WHEEL +4 -0
- ugos-1.0.0.dist-info/licenses/LICENSE +16 -0
ugos/_client_transfer.py
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
"""Download, upload and pagination implementation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import contextlib
|
|
6
|
+
import os
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Collection, Iterator, Mapping
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import httpx
|
|
13
|
+
|
|
14
|
+
from ._client_base import _ClientBase
|
|
15
|
+
from ._client_helpers import (
|
|
16
|
+
_capture_file_positions,
|
|
17
|
+
_DownloadSpec,
|
|
18
|
+
_error_body_limit,
|
|
19
|
+
_filename_from_response,
|
|
20
|
+
_limited_chunks,
|
|
21
|
+
_merge_params,
|
|
22
|
+
_multipart_fields,
|
|
23
|
+
_resolve_destination,
|
|
24
|
+
_restore_file_positions,
|
|
25
|
+
_RetryState,
|
|
26
|
+
_sanitize_filename,
|
|
27
|
+
)
|
|
28
|
+
from ._codec import _looks_like_login_html, parse_response
|
|
29
|
+
from ._encoding import encode_pairs, encode_params
|
|
30
|
+
from ._pagination import paginate
|
|
31
|
+
from ._redaction import redact_headers, redact_text
|
|
32
|
+
from ._transport import FORM_HEADERS
|
|
33
|
+
from ._transport import UNSET as _UNSET
|
|
34
|
+
from .exceptions import (
|
|
35
|
+
ConfigurationError,
|
|
36
|
+
CSRFError,
|
|
37
|
+
DownloadError,
|
|
38
|
+
HTTPStatusError,
|
|
39
|
+
SessionExpiredError,
|
|
40
|
+
)
|
|
41
|
+
from .models.common import DownloadedFile
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class _TransferMixin(_ClientBase):
|
|
45
|
+
def _build_download_request(
|
|
46
|
+
self,
|
|
47
|
+
spec: _DownloadSpec,
|
|
48
|
+
) -> tuple[bytes | str | None, list[tuple[str, str]] | None]:
|
|
49
|
+
request_params = dict(spec.params)
|
|
50
|
+
if spec.method == "GET":
|
|
51
|
+
return None, encode_pairs(request_params, json_fields=spec.json_fields)
|
|
52
|
+
with self._lock:
|
|
53
|
+
request_params["csrfmiddlewaretoken"] = self._auth.ensure_csrf()
|
|
54
|
+
body = encode_params(request_params, json_fields=spec.json_fields)
|
|
55
|
+
return body, None
|
|
56
|
+
|
|
57
|
+
def _raise_download_status(self, response: httpx.Response, spec: _DownloadSpec) -> None:
|
|
58
|
+
status = response.status_code
|
|
59
|
+
if 300 <= status < 400:
|
|
60
|
+
location = response.headers.get("location", "")
|
|
61
|
+
if "utm/login" in location or location.rstrip("/").endswith("/login"):
|
|
62
|
+
raise SessionExpiredError(
|
|
63
|
+
"session expired or login required",
|
|
64
|
+
status_code=status,
|
|
65
|
+
controller=spec.controller,
|
|
66
|
+
action=spec.action,
|
|
67
|
+
)
|
|
68
|
+
raise HTTPStatusError(
|
|
69
|
+
f"unexpected redirect: HTTP {status}",
|
|
70
|
+
status_code=status,
|
|
71
|
+
controller=spec.controller,
|
|
72
|
+
action=spec.action,
|
|
73
|
+
)
|
|
74
|
+
if status == 401:
|
|
75
|
+
raise SessionExpiredError(
|
|
76
|
+
"unauthorized",
|
|
77
|
+
status_code=status,
|
|
78
|
+
controller=spec.controller,
|
|
79
|
+
action=spec.action,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def _raise_download_html(
|
|
83
|
+
self,
|
|
84
|
+
response: httpx.Response,
|
|
85
|
+
first: bytes,
|
|
86
|
+
raw_chunks: Iterator[bytes],
|
|
87
|
+
spec: _DownloadSpec,
|
|
88
|
+
) -> None:
|
|
89
|
+
payload_bytes = b"".join(
|
|
90
|
+
_limited_chunks(
|
|
91
|
+
first,
|
|
92
|
+
raw_chunks,
|
|
93
|
+
limit=_error_body_limit(spec.limit),
|
|
94
|
+
controller=spec.controller,
|
|
95
|
+
action=spec.action,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
text = payload_bytes.decode(response.encoding or "utf-8", errors="replace")
|
|
99
|
+
if _looks_like_login_html(text):
|
|
100
|
+
raise SessionExpiredError(
|
|
101
|
+
"session expired or a login page was returned",
|
|
102
|
+
status_code=response.status_code,
|
|
103
|
+
controller=spec.controller,
|
|
104
|
+
action=spec.action,
|
|
105
|
+
)
|
|
106
|
+
raise DownloadError(
|
|
107
|
+
"expected a file, got HTML",
|
|
108
|
+
status_code=response.status_code,
|
|
109
|
+
controller=spec.controller,
|
|
110
|
+
action=spec.action,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def _is_json_like(self, content_type: str, head: bytes) -> bool:
|
|
114
|
+
if "json" in content_type.lower():
|
|
115
|
+
return True
|
|
116
|
+
return head.lstrip().startswith((b"{", b"[")) and any(
|
|
117
|
+
marker in head for marker in (b'"success"', b'"errorCode"', b'"error_code"', b'"error"')
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
def _raise_download_payload(
|
|
121
|
+
self,
|
|
122
|
+
response: httpx.Response,
|
|
123
|
+
content_type: str,
|
|
124
|
+
first: bytes,
|
|
125
|
+
raw_chunks: Iterator[bytes],
|
|
126
|
+
spec: _DownloadSpec,
|
|
127
|
+
) -> None:
|
|
128
|
+
payload_bytes = b"".join(
|
|
129
|
+
_limited_chunks(
|
|
130
|
+
first,
|
|
131
|
+
raw_chunks,
|
|
132
|
+
limit=_error_body_limit(spec.limit),
|
|
133
|
+
controller=spec.controller,
|
|
134
|
+
action=spec.action,
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
with self._lock:
|
|
138
|
+
self._auth.note_response(response)
|
|
139
|
+
synthetic = httpx.Response(
|
|
140
|
+
response.status_code,
|
|
141
|
+
headers={"content-type": content_type},
|
|
142
|
+
content=payload_bytes,
|
|
143
|
+
)
|
|
144
|
+
data = parse_response(
|
|
145
|
+
synthetic,
|
|
146
|
+
expect="auto",
|
|
147
|
+
controller=spec.controller,
|
|
148
|
+
action=spec.action,
|
|
149
|
+
)
|
|
150
|
+
raise DownloadError(
|
|
151
|
+
f"expected a file, got JSON: {redact_text(str(data))[:500]}",
|
|
152
|
+
controller=spec.controller,
|
|
153
|
+
action=spec.action,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
def _download_to_memory(
|
|
157
|
+
self,
|
|
158
|
+
first: bytes,
|
|
159
|
+
raw_chunks: Iterator[bytes],
|
|
160
|
+
spec: _DownloadSpec,
|
|
161
|
+
content_type: str,
|
|
162
|
+
filename: str,
|
|
163
|
+
headers: Mapping[str, str],
|
|
164
|
+
) -> DownloadedFile:
|
|
165
|
+
content = b"".join(
|
|
166
|
+
_limited_chunks(
|
|
167
|
+
first,
|
|
168
|
+
raw_chunks,
|
|
169
|
+
limit=spec.limit,
|
|
170
|
+
controller=spec.controller,
|
|
171
|
+
action=spec.action,
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
return DownloadedFile(
|
|
175
|
+
content=content,
|
|
176
|
+
filename=filename,
|
|
177
|
+
content_type=content_type or None,
|
|
178
|
+
headers=headers,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def _write_download(
|
|
182
|
+
self,
|
|
183
|
+
target: Path,
|
|
184
|
+
first: bytes,
|
|
185
|
+
raw_chunks: Iterator[bytes],
|
|
186
|
+
spec: _DownloadSpec,
|
|
187
|
+
) -> None:
|
|
188
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
189
|
+
fd, temp_name = tempfile.mkstemp(
|
|
190
|
+
dir=target.parent,
|
|
191
|
+
prefix=f".{target.name}.",
|
|
192
|
+
suffix=".part",
|
|
193
|
+
)
|
|
194
|
+
temp = Path(temp_name)
|
|
195
|
+
try:
|
|
196
|
+
with os.fdopen(fd, "wb") as handle:
|
|
197
|
+
for chunk in _limited_chunks(
|
|
198
|
+
first,
|
|
199
|
+
raw_chunks,
|
|
200
|
+
limit=spec.limit,
|
|
201
|
+
controller=spec.controller,
|
|
202
|
+
action=spec.action,
|
|
203
|
+
):
|
|
204
|
+
handle.write(chunk)
|
|
205
|
+
os.chmod(temp, 0o600)
|
|
206
|
+
os.replace(temp, target)
|
|
207
|
+
except Exception:
|
|
208
|
+
with contextlib.suppress(OSError):
|
|
209
|
+
temp.unlink()
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
def _download_to_path(
|
|
213
|
+
self,
|
|
214
|
+
target: Path,
|
|
215
|
+
first: bytes,
|
|
216
|
+
raw_chunks: Iterator[bytes],
|
|
217
|
+
spec: _DownloadSpec,
|
|
218
|
+
content_type: str,
|
|
219
|
+
filename: str,
|
|
220
|
+
headers: Mapping[str, str],
|
|
221
|
+
) -> DownloadedFile:
|
|
222
|
+
if target.exists() and not spec.overwrite:
|
|
223
|
+
raise FileExistsError(target)
|
|
224
|
+
self._write_download(target, first, raw_chunks, spec)
|
|
225
|
+
return DownloadedFile(
|
|
226
|
+
content=None,
|
|
227
|
+
filename=target.name,
|
|
228
|
+
content_type=content_type or None,
|
|
229
|
+
headers=headers,
|
|
230
|
+
path=target,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def _is_download_error(
|
|
234
|
+
self,
|
|
235
|
+
response: httpx.Response,
|
|
236
|
+
content_type: str,
|
|
237
|
+
first: bytes,
|
|
238
|
+
) -> bool:
|
|
239
|
+
if response.status_code >= 400:
|
|
240
|
+
return True
|
|
241
|
+
disposition = response.headers.get("content-disposition", "").lower()
|
|
242
|
+
if "attachment" in disposition:
|
|
243
|
+
return False
|
|
244
|
+
return self._is_json_like(content_type, first.lstrip()[:512])
|
|
245
|
+
|
|
246
|
+
def _finish_download(
|
|
247
|
+
self,
|
|
248
|
+
response: httpx.Response,
|
|
249
|
+
spec: _DownloadSpec,
|
|
250
|
+
first: bytes,
|
|
251
|
+
raw_chunks: Iterator[bytes],
|
|
252
|
+
content_type: str,
|
|
253
|
+
) -> DownloadedFile:
|
|
254
|
+
with self._lock:
|
|
255
|
+
self._auth.note_response(response)
|
|
256
|
+
name = _sanitize_filename(spec.filename or _filename_from_response(response, spec.path))
|
|
257
|
+
headers = redact_headers(dict(response.headers))
|
|
258
|
+
if spec.dest is None:
|
|
259
|
+
return self._download_to_memory(first, raw_chunks, spec, content_type, name, headers)
|
|
260
|
+
target = _resolve_destination(spec.dest, name)
|
|
261
|
+
return self._download_to_path(target, first, raw_chunks, spec, content_type, name, headers)
|
|
262
|
+
|
|
263
|
+
def _handle_download_response(
|
|
264
|
+
self,
|
|
265
|
+
response: httpx.Response,
|
|
266
|
+
spec: _DownloadSpec,
|
|
267
|
+
) -> DownloadedFile:
|
|
268
|
+
self._raise_download_status(response, spec)
|
|
269
|
+
content_type = response.headers.get("content-type", "")
|
|
270
|
+
raw_chunks = response.iter_bytes(chunk_size=spec.chunk_size)
|
|
271
|
+
first = next(raw_chunks, b"")
|
|
272
|
+
if "html" in content_type.lower():
|
|
273
|
+
self._raise_download_html(response, first, raw_chunks, spec)
|
|
274
|
+
if self._is_download_error(response, content_type, first):
|
|
275
|
+
self._raise_download_payload(response, content_type, first, raw_chunks, spec)
|
|
276
|
+
return self._finish_download(response, spec, first, raw_chunks, content_type)
|
|
277
|
+
|
|
278
|
+
def _retry_download(
|
|
279
|
+
self,
|
|
280
|
+
exc: Exception,
|
|
281
|
+
state: _RetryState,
|
|
282
|
+
generation: int,
|
|
283
|
+
spec: _DownloadSpec,
|
|
284
|
+
) -> bool:
|
|
285
|
+
if isinstance(exc, CSRFError):
|
|
286
|
+
return self._retry_csrf(state)
|
|
287
|
+
return self._retry_session(
|
|
288
|
+
state,
|
|
289
|
+
generation,
|
|
290
|
+
spec.action,
|
|
291
|
+
spec.method in {"GET", "HEAD", "OPTIONS"},
|
|
292
|
+
exc,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
def download(
|
|
296
|
+
self,
|
|
297
|
+
controller: str,
|
|
298
|
+
action: str,
|
|
299
|
+
/,
|
|
300
|
+
*,
|
|
301
|
+
dest: str | Path | None = None,
|
|
302
|
+
filename: str | None = None,
|
|
303
|
+
overwrite: bool = False,
|
|
304
|
+
method: str = "GET",
|
|
305
|
+
payload: Mapping[str, Any] | None = None,
|
|
306
|
+
json_fields: Collection[str] = (),
|
|
307
|
+
timeout: float | httpx.Timeout | object | None = _UNSET,
|
|
308
|
+
strict: bool | None = None,
|
|
309
|
+
chunk_size: int = 64 * 1024,
|
|
310
|
+
max_bytes: int | None = None,
|
|
311
|
+
**params: Any,
|
|
312
|
+
) -> DownloadedFile:
|
|
313
|
+
"""Download a file and optionally save it to ``dest``."""
|
|
314
|
+
|
|
315
|
+
self._validate(controller, action, strict=strict)
|
|
316
|
+
limit = max_bytes if max_bytes is not None else self._config.max_download_bytes
|
|
317
|
+
if limit is not None and limit < 0:
|
|
318
|
+
raise ValueError("max_bytes must be non-negative")
|
|
319
|
+
spec = _DownloadSpec(
|
|
320
|
+
controller=controller,
|
|
321
|
+
action=action,
|
|
322
|
+
path=f"{controller}/{action}",
|
|
323
|
+
method=method.upper(),
|
|
324
|
+
params=_merge_params(payload, params),
|
|
325
|
+
json_fields=json_fields,
|
|
326
|
+
timeout=timeout,
|
|
327
|
+
chunk_size=chunk_size,
|
|
328
|
+
limit=limit,
|
|
329
|
+
dest=dest,
|
|
330
|
+
filename=filename,
|
|
331
|
+
overwrite=overwrite,
|
|
332
|
+
)
|
|
333
|
+
with self._lock:
|
|
334
|
+
self._prepare()
|
|
335
|
+
return self._download_loop(spec)
|
|
336
|
+
|
|
337
|
+
def _download_loop(self, spec: _DownloadSpec) -> DownloadedFile:
|
|
338
|
+
state = _RetryState()
|
|
339
|
+
while True:
|
|
340
|
+
with self._lock:
|
|
341
|
+
generation = self._auth.generation
|
|
342
|
+
body, query = self._build_download_request(spec)
|
|
343
|
+
try:
|
|
344
|
+
with self._transport.stream(
|
|
345
|
+
spec.method,
|
|
346
|
+
spec.path,
|
|
347
|
+
params=query,
|
|
348
|
+
data=body,
|
|
349
|
+
headers={**(FORM_HEADERS if body else {}), "Accept": "*/*"},
|
|
350
|
+
timeout=spec.timeout,
|
|
351
|
+
idempotent=spec.method in {"GET", "HEAD", "OPTIONS"},
|
|
352
|
+
) as response:
|
|
353
|
+
return self._handle_download_response(response, spec)
|
|
354
|
+
except (CSRFError, SessionExpiredError) as exc:
|
|
355
|
+
if not self._retry_download(exc, state, generation, spec):
|
|
356
|
+
raise
|
|
357
|
+
|
|
358
|
+
@staticmethod
|
|
359
|
+
def _restore_upload_positions(positions: list[tuple[Any, int]] | None) -> None:
|
|
360
|
+
if positions is None:
|
|
361
|
+
raise ConfigurationError("cannot retry upload with non-seekable files")
|
|
362
|
+
_restore_file_positions(positions)
|
|
363
|
+
|
|
364
|
+
def _upload_once(
|
|
365
|
+
self,
|
|
366
|
+
controller: str,
|
|
367
|
+
action: str,
|
|
368
|
+
files: Mapping[str, Any],
|
|
369
|
+
merged: Mapping[str, Any],
|
|
370
|
+
json_fields: Collection[str],
|
|
371
|
+
query: Mapping[str, Any] | None,
|
|
372
|
+
timeout: Any,
|
|
373
|
+
) -> Any:
|
|
374
|
+
with self._lock:
|
|
375
|
+
token = self._auth.ensure_csrf()
|
|
376
|
+
data = _multipart_fields(
|
|
377
|
+
encode_pairs(
|
|
378
|
+
{**merged, "csrfmiddlewaretoken": token},
|
|
379
|
+
json_fields=json_fields,
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
query_params = encode_pairs(query, json_fields=json_fields) if query else None
|
|
383
|
+
with self._lock:
|
|
384
|
+
generation = self._auth.generation
|
|
385
|
+
response = self._transport.request(
|
|
386
|
+
"POST",
|
|
387
|
+
f"{controller}/{action}",
|
|
388
|
+
params=query_params,
|
|
389
|
+
data=data,
|
|
390
|
+
files=files,
|
|
391
|
+
headers={"Accept": "application/json, text/plain, */*"},
|
|
392
|
+
idempotent=False,
|
|
393
|
+
timeout=timeout,
|
|
394
|
+
)
|
|
395
|
+
with self._lock:
|
|
396
|
+
self._auth.note_response(response)
|
|
397
|
+
try:
|
|
398
|
+
return parse_response(
|
|
399
|
+
response,
|
|
400
|
+
expect="auto",
|
|
401
|
+
controller=controller,
|
|
402
|
+
action=action,
|
|
403
|
+
)
|
|
404
|
+
except SessionExpiredError:
|
|
405
|
+
self._refresh_session(generation)
|
|
406
|
+
raise
|
|
407
|
+
|
|
408
|
+
def _upload_loop(
|
|
409
|
+
self,
|
|
410
|
+
controller: str,
|
|
411
|
+
action: str,
|
|
412
|
+
files: Mapping[str, Any],
|
|
413
|
+
merged: Mapping[str, Any],
|
|
414
|
+
json_fields: Collection[str],
|
|
415
|
+
query: Mapping[str, Any] | None,
|
|
416
|
+
timeout: Any,
|
|
417
|
+
positions: list[tuple[Any, int]] | None,
|
|
418
|
+
) -> Any:
|
|
419
|
+
csrf_retries = 0
|
|
420
|
+
attempt = 0
|
|
421
|
+
while True:
|
|
422
|
+
if attempt > 0:
|
|
423
|
+
self._restore_upload_positions(positions)
|
|
424
|
+
attempt += 1
|
|
425
|
+
try:
|
|
426
|
+
return self._upload_once(
|
|
427
|
+
controller,
|
|
428
|
+
action,
|
|
429
|
+
files,
|
|
430
|
+
merged,
|
|
431
|
+
json_fields,
|
|
432
|
+
query,
|
|
433
|
+
timeout,
|
|
434
|
+
)
|
|
435
|
+
except CSRFError:
|
|
436
|
+
if csrf_retries >= 1:
|
|
437
|
+
raise
|
|
438
|
+
csrf_retries += 1
|
|
439
|
+
with self._lock:
|
|
440
|
+
self._auth.refresh_csrf()
|
|
441
|
+
|
|
442
|
+
def upload(
|
|
443
|
+
self,
|
|
444
|
+
controller: str,
|
|
445
|
+
action: str,
|
|
446
|
+
/,
|
|
447
|
+
*,
|
|
448
|
+
files: Mapping[str, Any],
|
|
449
|
+
payload: Mapping[str, Any] | None = None,
|
|
450
|
+
json_fields: Collection[str] = (),
|
|
451
|
+
query: Mapping[str, Any] | None = None,
|
|
452
|
+
timeout: float | httpx.Timeout | object | None = _UNSET,
|
|
453
|
+
strict: bool | None = None,
|
|
454
|
+
**params: Any,
|
|
455
|
+
) -> Any:
|
|
456
|
+
"""Upload files using ``multipart/form-data``."""
|
|
457
|
+
|
|
458
|
+
self._validate(controller, action, strict=strict)
|
|
459
|
+
merged = _merge_params(payload, params)
|
|
460
|
+
with self._lock:
|
|
461
|
+
self._prepare()
|
|
462
|
+
return self._upload_loop(
|
|
463
|
+
controller,
|
|
464
|
+
action,
|
|
465
|
+
files,
|
|
466
|
+
merged,
|
|
467
|
+
json_fields,
|
|
468
|
+
query,
|
|
469
|
+
timeout,
|
|
470
|
+
_capture_file_positions(files),
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
def iter_all(
|
|
474
|
+
self,
|
|
475
|
+
controller: str,
|
|
476
|
+
action: str,
|
|
477
|
+
/,
|
|
478
|
+
*,
|
|
479
|
+
page_size: int = 100,
|
|
480
|
+
start: int = 0,
|
|
481
|
+
max_items: int | None = None,
|
|
482
|
+
max_pages: int = 1000,
|
|
483
|
+
payload: Mapping[str, Any] | None = None,
|
|
484
|
+
strict: bool | None = None,
|
|
485
|
+
**params: Any,
|
|
486
|
+
) -> Iterator[Any]:
|
|
487
|
+
"""Iterate over all items of a paginated list action."""
|
|
488
|
+
|
|
489
|
+
self._validate(controller, action, strict=strict)
|
|
490
|
+
merged = _merge_params(payload, params)
|
|
491
|
+
return paginate(
|
|
492
|
+
self,
|
|
493
|
+
controller,
|
|
494
|
+
action,
|
|
495
|
+
page_size=page_size,
|
|
496
|
+
start=start,
|
|
497
|
+
max_items=max_items,
|
|
498
|
+
max_pages=max_pages,
|
|
499
|
+
strict=strict,
|
|
500
|
+
**merged,
|
|
501
|
+
)
|