miniopy-async 1.23.2__tar.gz → 1.23.4__tar.gz
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.
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/PKG-INFO +1 -1
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/__init__.py +1 -1
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/api.py +342 -28
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/datatypes.py +98 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/error.py +0 -7
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/helpers.py +30 -129
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/minioadmin.py +10 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/pyproject.toml +1 -1
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/LICENSE +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/README.md +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/commonconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/credentials/__init__.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/credentials/credentials.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/credentials/providers.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/crypto.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/deleteobjects.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/legalhold.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/lifecycleconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/notificationconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/objectlockconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/py.typed +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/replicationconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/retention.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/select.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/signer.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/sse.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/sseconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/tagging.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/time.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/versioningconfig.py +0 -0
- {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/xml.py +0 -0
|
@@ -29,13 +29,14 @@ import ssl
|
|
|
29
29
|
import tarfile
|
|
30
30
|
from collections.abc import Iterable
|
|
31
31
|
from datetime import datetime, timedelta
|
|
32
|
-
from io import BytesIO
|
|
32
|
+
from io import BytesIO, IOBase
|
|
33
33
|
from random import random
|
|
34
34
|
from typing import Any, AsyncGenerator, BinaryIO, TextIO, cast
|
|
35
35
|
from urllib.parse import urlunsplit
|
|
36
36
|
from xml.etree import ElementTree as ET
|
|
37
37
|
|
|
38
38
|
import certifi
|
|
39
|
+
import yarl
|
|
39
40
|
from aiohttp import ClientResponse, ClientSession, ClientTimeout, TCPConnector
|
|
40
41
|
from aiohttp.typedefs import LooseHeaders
|
|
41
42
|
from aiohttp_retry import ExponentialRetry, RetryClient
|
|
@@ -54,6 +55,7 @@ from .datatypes import (
|
|
|
54
55
|
ListMultipartUploadsResult,
|
|
55
56
|
ListObjects,
|
|
56
57
|
ListPartsResult,
|
|
58
|
+
MultipartUploader,
|
|
57
59
|
Object,
|
|
58
60
|
Part,
|
|
59
61
|
PostPolicy,
|
|
@@ -72,7 +74,6 @@ from .helpers import (
|
|
|
72
74
|
DictType,
|
|
73
75
|
ObjectWriteResult,
|
|
74
76
|
ProgressType,
|
|
75
|
-
Substream,
|
|
76
77
|
check_bucket_name,
|
|
77
78
|
check_object_name,
|
|
78
79
|
check_sse,
|
|
@@ -84,6 +85,7 @@ from .helpers import (
|
|
|
84
85
|
makedirs,
|
|
85
86
|
md5sum_hash,
|
|
86
87
|
queryencode,
|
|
88
|
+
read_part_data,
|
|
87
89
|
sha256_hash,
|
|
88
90
|
)
|
|
89
91
|
from .legalhold import LegalHold
|
|
@@ -265,7 +267,7 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
265
267
|
self,
|
|
266
268
|
host: str,
|
|
267
269
|
headers: LooseHeaders | DictType | None,
|
|
268
|
-
body: bytes |
|
|
270
|
+
body: bytes | BinaryIO | None,
|
|
269
271
|
creds: Credentials | None,
|
|
270
272
|
) -> tuple[dict[str, str], datetime]:
|
|
271
273
|
"""Build headers with given parameters."""
|
|
@@ -279,10 +281,13 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
279
281
|
if body:
|
|
280
282
|
if isinstance(body, BytesIO):
|
|
281
283
|
headers["Content-Length"] = str(body.getbuffer().nbytes)
|
|
282
|
-
elif isinstance(body,
|
|
283
|
-
|
|
284
|
-
|
|
284
|
+
elif isinstance(body, IOBase):
|
|
285
|
+
stat = os.fstat(body.fileno())
|
|
286
|
+
headers["Content-Length"] = str(stat.st_size)
|
|
287
|
+
elif isinstance(body, bytes):
|
|
285
288
|
headers["Content-Length"] = str(len(body))
|
|
289
|
+
else:
|
|
290
|
+
raise ValueError("body must be bytes or BinaryIO type")
|
|
286
291
|
if creds:
|
|
287
292
|
if self._base_url.is_https:
|
|
288
293
|
sha256 = "UNSIGNED-PAYLOAD"
|
|
@@ -307,7 +312,7 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
307
312
|
region: str,
|
|
308
313
|
bucket_name: str | None = None,
|
|
309
314
|
object_name: str | None = None,
|
|
310
|
-
body: bytes |
|
|
315
|
+
body: bytes | BinaryIO | None = None,
|
|
311
316
|
headers: LooseHeaders | DictType | None = None,
|
|
312
317
|
query_params: DictType | None = None,
|
|
313
318
|
no_body_trace: bool = False,
|
|
@@ -361,9 +366,10 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
361
366
|
self._ensure_session()
|
|
362
367
|
session = cast(ClientSession | RetryClient, self._session)
|
|
363
368
|
|
|
369
|
+
yarl_url = yarl.URL(urlunsplit(url), encoded=True)
|
|
364
370
|
response = await session.request(
|
|
365
371
|
method,
|
|
366
|
-
|
|
372
|
+
yarl_url,
|
|
367
373
|
data=body,
|
|
368
374
|
headers=http_headers,
|
|
369
375
|
)
|
|
@@ -498,7 +504,7 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
498
504
|
method: str,
|
|
499
505
|
bucket_name: str | None = None,
|
|
500
506
|
object_name: str | None = None,
|
|
501
|
-
body: bytes |
|
|
507
|
+
body: bytes | BinaryIO | None = None,
|
|
502
508
|
headers: DictType | None = None,
|
|
503
509
|
query_params: DictType | None = None,
|
|
504
510
|
no_body_trace: bool = False,
|
|
@@ -2296,11 +2302,140 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2296
2302
|
element = ET.fromstring(await response.text())
|
|
2297
2303
|
return cast(str, findtext(element, "UploadId", True))
|
|
2298
2304
|
|
|
2305
|
+
def multipart_uploader(
|
|
2306
|
+
self,
|
|
2307
|
+
bucket_name: str,
|
|
2308
|
+
object_name: str,
|
|
2309
|
+
content_type: str = "application/octet-stream",
|
|
2310
|
+
metadata: DictType | None = None,
|
|
2311
|
+
sse: Sse | None = None,
|
|
2312
|
+
tags: Tags | None = None,
|
|
2313
|
+
retention: Retention | None = None,
|
|
2314
|
+
legal_hold: bool = False,
|
|
2315
|
+
extra_headers: DictType | None = None,
|
|
2316
|
+
) -> MultipartUploader:
|
|
2317
|
+
"""
|
|
2318
|
+
Create a multipart uploader for large data.
|
|
2319
|
+
|
|
2320
|
+
:param str bucket_name: Name of the bucket.
|
|
2321
|
+
:param str object_name: Object name in the bucket.
|
|
2322
|
+
:param str content_type: Content type of the object.
|
|
2323
|
+
:param DictType | None metadata: Any additional metadata to be uploaded along
|
|
2324
|
+
with your PUT request.
|
|
2325
|
+
:param Sse | None sse: Server-side encryption.
|
|
2326
|
+
:param Tags | None tags: :class:`Tags` for the object.
|
|
2327
|
+
:param Retention | None retention: :class:`Retention` configuration object.
|
|
2328
|
+
:param bool legal_hold: Flag to set legal hold for the object.
|
|
2329
|
+
:param DictType | None extra_headers: Any additional headers to be added with GET request.
|
|
2330
|
+
:return: :class:`MultipartUploader` object.
|
|
2331
|
+
:rtype: MultipartUploader
|
|
2332
|
+
|
|
2333
|
+
.. code-block:: python
|
|
2334
|
+
|
|
2335
|
+
import asyncio
|
|
2336
|
+
|
|
2337
|
+
from miniopy_async import Minio
|
|
2338
|
+
|
|
2339
|
+
client = Minio(
|
|
2340
|
+
"play.min.io",
|
|
2341
|
+
access_key="Q3AM3UQ867SPQQA43P2F",
|
|
2342
|
+
secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
|
|
2343
|
+
secure=True, # http for False, https for True
|
|
2344
|
+
)
|
|
2345
|
+
|
|
2346
|
+
|
|
2347
|
+
async def main():
|
|
2348
|
+
# Use context manager
|
|
2349
|
+
print("example one")
|
|
2350
|
+
async with client.multipart_uploader(
|
|
2351
|
+
"my-bucket",
|
|
2352
|
+
"my-object",
|
|
2353
|
+
) as uploader:
|
|
2354
|
+
await uploader.upload_part(b"hello" * 1024 * 1024, 1)
|
|
2355
|
+
await uploader.upload_part(b"world", 2)
|
|
2356
|
+
result = uploader.result
|
|
2357
|
+
print(
|
|
2358
|
+
"created {0} object; etag: {1}, version-id: {2}".format(
|
|
2359
|
+
result.object_name,
|
|
2360
|
+
result.etag,
|
|
2361
|
+
result.version_id,
|
|
2362
|
+
),
|
|
2363
|
+
)
|
|
2364
|
+
|
|
2365
|
+
# Use directly
|
|
2366
|
+
print("example two")
|
|
2367
|
+
uploader = client.multipart_uploader(
|
|
2368
|
+
"my-bucket",
|
|
2369
|
+
"my-object",
|
|
2370
|
+
)
|
|
2371
|
+
await uploader.upload_part(b"hello" * 1024 * 1024, 1)
|
|
2372
|
+
await uploader.upload_part(b"world", 2)
|
|
2373
|
+
result = await uploader.complete()
|
|
2374
|
+
print(
|
|
2375
|
+
"created {0} object; etag: {1}, version-id: {2}".format(
|
|
2376
|
+
result.object_name,
|
|
2377
|
+
result.etag,
|
|
2378
|
+
result.version_id,
|
|
2379
|
+
),
|
|
2380
|
+
)
|
|
2381
|
+
|
|
2382
|
+
# Abort
|
|
2383
|
+
print("example three")
|
|
2384
|
+
uploader = client.multipart_uploader(
|
|
2385
|
+
"my-bucket",
|
|
2386
|
+
"my-object",
|
|
2387
|
+
)
|
|
2388
|
+
await uploader.upload_part(b"hello", 1)
|
|
2389
|
+
await uploader.abort()
|
|
2390
|
+
|
|
2391
|
+
# Parallel upload
|
|
2392
|
+
print("example four")
|
|
2393
|
+
tasks = []
|
|
2394
|
+
async with client.multipart_uploader(
|
|
2395
|
+
"my-bucket",
|
|
2396
|
+
"my-object",
|
|
2397
|
+
) as uploader:
|
|
2398
|
+
for i in range(1, 11):
|
|
2399
|
+
data = (f"part{i}" * 1024 * 1024).encode("utf-8")
|
|
2400
|
+
tasks.append(uploader.upload_part(data, i))
|
|
2401
|
+
await asyncio.gather(*tasks)
|
|
2402
|
+
result = uploader.result
|
|
2403
|
+
print(
|
|
2404
|
+
"created {0} object; etag: {1}, version-id: {2}".format(
|
|
2405
|
+
result.object_name,
|
|
2406
|
+
result.etag,
|
|
2407
|
+
result.version_id,
|
|
2408
|
+
),
|
|
2409
|
+
)
|
|
2410
|
+
|
|
2411
|
+
|
|
2412
|
+
asyncio.run(main())
|
|
2413
|
+
"""
|
|
2414
|
+
check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host)
|
|
2415
|
+
check_object_name(object_name)
|
|
2416
|
+
check_sse(sse)
|
|
2417
|
+
if tags is not None and not isinstance(tags, Tags):
|
|
2418
|
+
raise ValueError("tags must be Tags type")
|
|
2419
|
+
if retention is not None and not isinstance(retention, Retention):
|
|
2420
|
+
raise ValueError("retention must be Retention type")
|
|
2421
|
+
|
|
2422
|
+
headers = genheaders(metadata, sse, tags, retention, legal_hold)
|
|
2423
|
+
headers["Content-Type"] = content_type or "application/octet-stream"
|
|
2424
|
+
if extra_headers:
|
|
2425
|
+
headers.update(extra_headers)
|
|
2426
|
+
|
|
2427
|
+
return MultipartUploader(
|
|
2428
|
+
self,
|
|
2429
|
+
bucket_name,
|
|
2430
|
+
object_name,
|
|
2431
|
+
headers,
|
|
2432
|
+
)
|
|
2433
|
+
|
|
2299
2434
|
async def _put_object(
|
|
2300
2435
|
self,
|
|
2301
2436
|
bucket_name: str,
|
|
2302
2437
|
object_name: str,
|
|
2303
|
-
data:
|
|
2438
|
+
data: bytes | BinaryIO,
|
|
2304
2439
|
headers: DictType | None,
|
|
2305
2440
|
query_params: DictType | None = None,
|
|
2306
2441
|
) -> ObjectWriteResult:
|
|
@@ -2326,7 +2461,7 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2326
2461
|
self,
|
|
2327
2462
|
bucket_name: str,
|
|
2328
2463
|
object_name: str,
|
|
2329
|
-
data:
|
|
2464
|
+
data: bytes | BytesIO,
|
|
2330
2465
|
headers: DictType | None,
|
|
2331
2466
|
upload_id: str,
|
|
2332
2467
|
part_number: int,
|
|
@@ -2371,13 +2506,14 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2371
2506
|
tags: Tags | None = None,
|
|
2372
2507
|
retention: Retention | None = None,
|
|
2373
2508
|
legal_hold: bool = False,
|
|
2509
|
+
write_offset: int | None = None,
|
|
2374
2510
|
) -> ObjectWriteResult:
|
|
2375
2511
|
"""
|
|
2376
2512
|
Uploads data from a stream to an object in a bucket.
|
|
2377
2513
|
|
|
2378
2514
|
:param str bucket_name: Name of the bucket.
|
|
2379
2515
|
:param str object_name: Object name in the bucket.
|
|
2380
|
-
:param BinaryIO
|
|
2516
|
+
:param BinaryIO data: An object having callable read() returning bytes object.
|
|
2381
2517
|
:param int length: Data size; -1 for unknown size and set valid part_size.
|
|
2382
2518
|
:param str content_type: Content type of the object.
|
|
2383
2519
|
:param DictType | None metadata: Any additional metadata to be uploaded along
|
|
@@ -2389,6 +2525,7 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2389
2525
|
:param Tags | None tags: :class:`Tags` for the object.
|
|
2390
2526
|
:param Retention | None retention: :class:`Retention` configuration object.
|
|
2391
2527
|
:param bool legal_hold: Flag to set legal hold for the object.
|
|
2528
|
+
:param int | None write_offset: Offset byte for appending data to existing object.
|
|
2392
2529
|
:return: :class:`ObjectWriteResult` object.
|
|
2393
2530
|
:rtype: ObjectWriteResult
|
|
2394
2531
|
:raise ValueError: If tags is not of type :class:`Tags`.
|
|
@@ -2493,14 +2630,31 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2493
2630
|
raise ValueError("retention must be Retention type")
|
|
2494
2631
|
if not callable(getattr(data, "read")):
|
|
2495
2632
|
raise ValueError("input data must have callable read()")
|
|
2633
|
+
if write_offset is not None:
|
|
2634
|
+
if write_offset < 0:
|
|
2635
|
+
raise ValueError("write offset should not be negative")
|
|
2636
|
+
if length < 0:
|
|
2637
|
+
raise ValueError("length must be provided for write offset")
|
|
2638
|
+
part_size = length if length > MIN_PART_SIZE else MIN_PART_SIZE
|
|
2639
|
+
|
|
2640
|
+
headers = genheaders(metadata, sse, tags, retention, legal_hold)
|
|
2641
|
+
headers["Content-Type"] = content_type or "application/octet-stream"
|
|
2642
|
+
if write_offset:
|
|
2643
|
+
headers["x-amz-write-offset-bytes"] = str(write_offset)
|
|
2644
|
+
|
|
2645
|
+
if part_size == length:
|
|
2646
|
+
return await self._put_object(
|
|
2647
|
+
bucket_name,
|
|
2648
|
+
object_name,
|
|
2649
|
+
data,
|
|
2650
|
+
headers,
|
|
2651
|
+
)
|
|
2652
|
+
|
|
2496
2653
|
part_size, part_count = get_part_info(length, part_size)
|
|
2497
2654
|
if progress:
|
|
2498
2655
|
# Set progress bar length and object name before upload
|
|
2499
2656
|
await progress.set_meta(object_name=object_name, total_length=length)
|
|
2500
2657
|
|
|
2501
|
-
headers = genheaders(metadata, sse, tags, retention, legal_hold)
|
|
2502
|
-
headers["Content-Type"] = content_type or "application/octet-stream"
|
|
2503
|
-
|
|
2504
2658
|
object_size = length
|
|
2505
2659
|
uploaded_size = 0
|
|
2506
2660
|
part_number = 0
|
|
@@ -2517,26 +2671,22 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2517
2671
|
if part_number == part_count:
|
|
2518
2672
|
part_size = object_size - uploaded_size
|
|
2519
2673
|
stop = True
|
|
2520
|
-
part_data =
|
|
2521
|
-
if part_data
|
|
2674
|
+
part_data = await read_part_data(data, part_size, progress=progress)
|
|
2675
|
+
if len(part_data) != part_size:
|
|
2522
2676
|
raise IOError(
|
|
2523
2677
|
f"stream having not enough data;"
|
|
2524
2678
|
f"expected: {part_size}, "
|
|
2525
|
-
f"got: {part_data
|
|
2679
|
+
f"got: {len(part_data)} bytes"
|
|
2526
2680
|
)
|
|
2527
2681
|
else:
|
|
2528
|
-
part_data =
|
|
2529
|
-
if part_data
|
|
2682
|
+
part_data = await read_part_data(data, part_size, progress=progress)
|
|
2683
|
+
if len(part_data) == 0:
|
|
2530
2684
|
break
|
|
2531
|
-
if part_data
|
|
2685
|
+
if len(part_data) < part_size:
|
|
2532
2686
|
part_count = part_number
|
|
2533
2687
|
stop = True
|
|
2534
|
-
part_size = part_data.size
|
|
2535
|
-
|
|
2536
|
-
if progress:
|
|
2537
|
-
await progress.update(part_size)
|
|
2538
2688
|
|
|
2539
|
-
uploaded_size +=
|
|
2689
|
+
uploaded_size += len(part_data)
|
|
2540
2690
|
|
|
2541
2691
|
if part_count == 1:
|
|
2542
2692
|
return await self._put_object(
|
|
@@ -2606,6 +2756,169 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
2606
2756
|
)
|
|
2607
2757
|
raise exc
|
|
2608
2758
|
|
|
2759
|
+
async def append_object(
|
|
2760
|
+
self,
|
|
2761
|
+
bucket_name: str,
|
|
2762
|
+
object_name: str,
|
|
2763
|
+
data: BinaryIO,
|
|
2764
|
+
length: int,
|
|
2765
|
+
chunk_size: int | None = None,
|
|
2766
|
+
progress: ProgressType | None = None,
|
|
2767
|
+
extra_headers: DictType | None = None,
|
|
2768
|
+
) -> ObjectWriteResult:
|
|
2769
|
+
"""
|
|
2770
|
+
Appends from a stream to existing object in a bucket.
|
|
2771
|
+
|
|
2772
|
+
:param str bucket_name: Name of the bucket.
|
|
2773
|
+
:param str object_name: Object name in the bucket.
|
|
2774
|
+
:param BinaryIO data: An object having callable read() returning bytes object.
|
|
2775
|
+
:param int length: Data size; -1 for unknown size.
|
|
2776
|
+
:param int chunk_size: Chunk size to optimize uploads.
|
|
2777
|
+
:param ProgressType | None progress: A progress object.
|
|
2778
|
+
:param DictType | None extra_headers: Any additional headers to be added with GET request.
|
|
2779
|
+
:return: :class:`ObjectWriteResult` object.
|
|
2780
|
+
:rtype: ObjectWriteResult
|
|
2781
|
+
:raise ValueError: If length is 0.
|
|
2782
|
+
:raise ValueError: If chunk_size is less than 5 MiB or greater than 5 GiB.
|
|
2783
|
+
:raise IOError: If stream doesn't have enough data to read.
|
|
2784
|
+
|
|
2785
|
+
.. code-block:: python
|
|
2786
|
+
|
|
2787
|
+
import asyncio
|
|
2788
|
+
import io
|
|
2789
|
+
from urllib.request import urlopen
|
|
2790
|
+
|
|
2791
|
+
from miniopy_async import Minio
|
|
2792
|
+
|
|
2793
|
+
client = Minio(
|
|
2794
|
+
"play.min.io",
|
|
2795
|
+
access_key="Q3AM3UQ867SPQQA43P2F",
|
|
2796
|
+
secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
|
|
2797
|
+
secure=True, # http for False, https for True
|
|
2798
|
+
)
|
|
2799
|
+
|
|
2800
|
+
|
|
2801
|
+
async def main():
|
|
2802
|
+
# Upload data.
|
|
2803
|
+
result = await client.put_object(
|
|
2804
|
+
"my-bucket",
|
|
2805
|
+
"my-object",
|
|
2806
|
+
io.BytesIO(b"hello, "),
|
|
2807
|
+
7,
|
|
2808
|
+
)
|
|
2809
|
+
print(f"created {result.object_name} object; etag: {result.etag}")
|
|
2810
|
+
|
|
2811
|
+
# Append data.
|
|
2812
|
+
result = await client.append_object(
|
|
2813
|
+
"my-bucket",
|
|
2814
|
+
"my-object",
|
|
2815
|
+
io.BytesIO(b"world"),
|
|
2816
|
+
5,
|
|
2817
|
+
)
|
|
2818
|
+
print(f"appended {result.object_name} object; etag: {result.etag}")
|
|
2819
|
+
|
|
2820
|
+
# Append data in chunks.
|
|
2821
|
+
data = urlopen(
|
|
2822
|
+
"https://www.kernel.org/pub/linux/kernel/v6.x/linux-6.13.12.tar.xz",
|
|
2823
|
+
)
|
|
2824
|
+
result = await client.append_object(
|
|
2825
|
+
"my-bucket",
|
|
2826
|
+
"my-object",
|
|
2827
|
+
data,
|
|
2828
|
+
148611164,
|
|
2829
|
+
5 * 1024 * 1024,
|
|
2830
|
+
)
|
|
2831
|
+
print(f"appended {result.object_name} object; etag: {result.etag}")
|
|
2832
|
+
|
|
2833
|
+
# Append unknown sized data.
|
|
2834
|
+
data = urlopen(
|
|
2835
|
+
"https://www.kernel.org/pub/linux/kernel/v6.x/linux-6.14.3.tar.xz",
|
|
2836
|
+
)
|
|
2837
|
+
result = await client.append_object(
|
|
2838
|
+
"my-bucket",
|
|
2839
|
+
"my-object",
|
|
2840
|
+
data,
|
|
2841
|
+
149426584,
|
|
2842
|
+
5 * 1024 * 1024,
|
|
2843
|
+
)
|
|
2844
|
+
print(f"appended {result.object_name} object; etag: {result.etag}")
|
|
2845
|
+
|
|
2846
|
+
|
|
2847
|
+
asyncio.run(main())
|
|
2848
|
+
"""
|
|
2849
|
+
if length == 0:
|
|
2850
|
+
raise ValueError("length should not be zero")
|
|
2851
|
+
if chunk_size is not None:
|
|
2852
|
+
if chunk_size < MIN_PART_SIZE:
|
|
2853
|
+
raise ValueError("chunk size must be minimum of 5 MiB")
|
|
2854
|
+
if chunk_size > MAX_PART_SIZE:
|
|
2855
|
+
raise ValueError("chunk size must be less than 5 GiB")
|
|
2856
|
+
else:
|
|
2857
|
+
chunk_size = length if length > MIN_PART_SIZE else MIN_PART_SIZE
|
|
2858
|
+
|
|
2859
|
+
chunk_count = -1
|
|
2860
|
+
if length > 0:
|
|
2861
|
+
chunk_count = int(length / chunk_size)
|
|
2862
|
+
if (chunk_count * chunk_size) < length:
|
|
2863
|
+
chunk_count += 1
|
|
2864
|
+
chunk_count = chunk_count or 1
|
|
2865
|
+
|
|
2866
|
+
object_size = length
|
|
2867
|
+
uploaded_size = 0
|
|
2868
|
+
chunk_number = 0
|
|
2869
|
+
stop = False
|
|
2870
|
+
|
|
2871
|
+
stat = await self.stat_object(bucket_name, object_name)
|
|
2872
|
+
write_offset = cast(int, stat.size)
|
|
2873
|
+
upload_result = None
|
|
2874
|
+
|
|
2875
|
+
while not stop:
|
|
2876
|
+
chunk_number += 1
|
|
2877
|
+
if chunk_count > 0:
|
|
2878
|
+
if chunk_number == chunk_count:
|
|
2879
|
+
chunk_size = object_size - uploaded_size
|
|
2880
|
+
stop = True
|
|
2881
|
+
chunk_data = await read_part_data(
|
|
2882
|
+
data, cast(int, chunk_size), progress=progress
|
|
2883
|
+
)
|
|
2884
|
+
if len(chunk_data) != chunk_size:
|
|
2885
|
+
raise IOError(
|
|
2886
|
+
f"stream having not enough data;"
|
|
2887
|
+
f"expected: {chunk_size}, "
|
|
2888
|
+
f"got: {len(chunk_data)} bytes"
|
|
2889
|
+
)
|
|
2890
|
+
else:
|
|
2891
|
+
chunk_data = await read_part_data(
|
|
2892
|
+
data, cast(int, chunk_size), progress=progress
|
|
2893
|
+
)
|
|
2894
|
+
if len(chunk_data) == 0:
|
|
2895
|
+
break
|
|
2896
|
+
if len(chunk_data) < chunk_size:
|
|
2897
|
+
chunk_count = chunk_number
|
|
2898
|
+
stop = True
|
|
2899
|
+
|
|
2900
|
+
uploaded_size += len(chunk_data)
|
|
2901
|
+
|
|
2902
|
+
headers = extra_headers or {}
|
|
2903
|
+
headers["x-amz-write-offset-bytes"] = str(write_offset)
|
|
2904
|
+
upload_result = await self._put_object(
|
|
2905
|
+
bucket_name,
|
|
2906
|
+
object_name,
|
|
2907
|
+
chunk_data,
|
|
2908
|
+
headers=headers,
|
|
2909
|
+
)
|
|
2910
|
+
write_offset += chunk_size
|
|
2911
|
+
|
|
2912
|
+
upload_result = cast(ObjectWriteResult, upload_result)
|
|
2913
|
+
return ObjectWriteResult(
|
|
2914
|
+
cast(str, upload_result.bucket_name),
|
|
2915
|
+
cast(str, upload_result.object_name),
|
|
2916
|
+
upload_result.version_id,
|
|
2917
|
+
upload_result.etag,
|
|
2918
|
+
upload_result.http_headers,
|
|
2919
|
+
location=upload_result.location,
|
|
2920
|
+
)
|
|
2921
|
+
|
|
2609
2922
|
def list_objects(
|
|
2610
2923
|
self,
|
|
2611
2924
|
bucket_name: str,
|
|
@@ -4228,9 +4541,10 @@ class Minio: # pylint: disable=too-many-public-methods
|
|
|
4228
4541
|
metadata["X-Amz-Meta-Snowball-Auto-Extract"] = "true"
|
|
4229
4542
|
|
|
4230
4543
|
name = staging_filename
|
|
4231
|
-
mode = "w:gz" if compression else "w"
|
|
4232
4544
|
fileobj = None if name else BytesIO()
|
|
4233
|
-
with tarfile.open(
|
|
4545
|
+
with tarfile.open(
|
|
4546
|
+
name=name, mode="w:gz" if compression else "w", fileobj=fileobj
|
|
4547
|
+
) as tar:
|
|
4234
4548
|
for obj in object_list:
|
|
4235
4549
|
if obj.filename:
|
|
4236
4550
|
tar.add(obj.filename, obj.object_name)
|
|
@@ -21,6 +21,7 @@ Response of ListBuckets, ListObjects, ListObjectsV2 and ListObjectVersions API.
|
|
|
21
21
|
from __future__ import absolute_import, annotations
|
|
22
22
|
|
|
23
23
|
import base64
|
|
24
|
+
import io
|
|
24
25
|
import itertools
|
|
25
26
|
import json
|
|
26
27
|
from collections import OrderedDict
|
|
@@ -1286,3 +1287,100 @@ class DeleteErrors:
|
|
|
1286
1287
|
async for error in self:
|
|
1287
1288
|
errors.append(error)
|
|
1288
1289
|
return errors
|
|
1290
|
+
|
|
1291
|
+
|
|
1292
|
+
class MultipartUploader:
|
|
1293
|
+
def __init__(
|
|
1294
|
+
self,
|
|
1295
|
+
client: Minio,
|
|
1296
|
+
bucket_name: str,
|
|
1297
|
+
object_name: str,
|
|
1298
|
+
headers: DictType,
|
|
1299
|
+
):
|
|
1300
|
+
self._client = client
|
|
1301
|
+
self._bucket_name = bucket_name
|
|
1302
|
+
self._object_name = object_name
|
|
1303
|
+
self._headers = headers
|
|
1304
|
+
self._upload_id = ""
|
|
1305
|
+
self._parts = []
|
|
1306
|
+
self._result: CompleteMultipartUploadResult | None = None
|
|
1307
|
+
|
|
1308
|
+
@property
|
|
1309
|
+
def result(self) -> CompleteMultipartUploadResult:
|
|
1310
|
+
"""
|
|
1311
|
+
Get multipart upload result.
|
|
1312
|
+
|
|
1313
|
+
:return: Multipart upload result.
|
|
1314
|
+
:rtype: CompleteMultipartUploadResult
|
|
1315
|
+
:raises ValueError: If multipart upload is not completed.
|
|
1316
|
+
"""
|
|
1317
|
+
if not self._result:
|
|
1318
|
+
raise ValueError("Multipart upload not completed")
|
|
1319
|
+
return self._result
|
|
1320
|
+
|
|
1321
|
+
async def _init(self):
|
|
1322
|
+
self._upload_id = await self._client._create_multipart_upload(
|
|
1323
|
+
self._bucket_name, self._object_name, self._headers
|
|
1324
|
+
)
|
|
1325
|
+
|
|
1326
|
+
async def complete(self) -> CompleteMultipartUploadResult:
|
|
1327
|
+
"""
|
|
1328
|
+
Complete multipart upload.
|
|
1329
|
+
|
|
1330
|
+
:return: Multipart upload result.
|
|
1331
|
+
:rtype: CompleteMultipartUploadResult
|
|
1332
|
+
"""
|
|
1333
|
+
self._result = await self._client._complete_multipart_upload(
|
|
1334
|
+
self._bucket_name, self._object_name, self._upload_id, self._parts
|
|
1335
|
+
)
|
|
1336
|
+
return self.result
|
|
1337
|
+
|
|
1338
|
+
async def abort(self):
|
|
1339
|
+
"""Abort multipart upload."""
|
|
1340
|
+
await self._client._abort_multipart_upload(
|
|
1341
|
+
self._bucket_name, self._object_name, self._upload_id
|
|
1342
|
+
)
|
|
1343
|
+
|
|
1344
|
+
async def __aenter__(self):
|
|
1345
|
+
await self._init()
|
|
1346
|
+
return self
|
|
1347
|
+
|
|
1348
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
1349
|
+
await self.complete()
|
|
1350
|
+
|
|
1351
|
+
async def upload_part(self, data: bytes | io.BytesIO, part_number: int) -> Part:
|
|
1352
|
+
"""
|
|
1353
|
+
Upload a part of the object.
|
|
1354
|
+
|
|
1355
|
+
The data size must be larger than 5 MiB and less than 5 GiB except the last part.
|
|
1356
|
+
|
|
1357
|
+
:param bytes | io.BytesIO data: Data to upload as part.
|
|
1358
|
+
:param int part_number: Number of the part.
|
|
1359
|
+
:return: Part information.
|
|
1360
|
+
:rtype: Part
|
|
1361
|
+
:raises ValueError: If part_number is not a positive integer.
|
|
1362
|
+
"""
|
|
1363
|
+
if not self._upload_id:
|
|
1364
|
+
await self._init()
|
|
1365
|
+
try:
|
|
1366
|
+
if not isinstance(part_number, int) or part_number < 1:
|
|
1367
|
+
raise ValueError("part_number must be a positive integer")
|
|
1368
|
+
|
|
1369
|
+
etag = await self._client._upload_part(
|
|
1370
|
+
self._bucket_name,
|
|
1371
|
+
self._object_name,
|
|
1372
|
+
data,
|
|
1373
|
+
self._headers,
|
|
1374
|
+
self._upload_id,
|
|
1375
|
+
part_number,
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
part = Part(part_number, etag)
|
|
1379
|
+
self._parts.append(part)
|
|
1380
|
+
|
|
1381
|
+
return part
|
|
1382
|
+
|
|
1383
|
+
except Exception as e:
|
|
1384
|
+
await self.abort()
|
|
1385
|
+
|
|
1386
|
+
raise e
|
|
@@ -170,10 +170,3 @@ class MinioAdminException(Exception):
|
|
|
170
170
|
|
|
171
171
|
def __reduce__(self):
|
|
172
172
|
return type(self), (self._code, self._body)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
class UnreachableError(MinioException):
|
|
176
|
-
"""Raised when unreachable code reached."""
|
|
177
|
-
|
|
178
|
-
def __init__(self):
|
|
179
|
-
super().__init__("unreachable code reached, please report this issue.")
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
from __future__ import absolute_import, annotations, division, unicode_literals
|
|
20
20
|
|
|
21
|
+
import asyncio
|
|
21
22
|
import base64
|
|
22
23
|
import errno
|
|
23
24
|
import hashlib
|
|
@@ -32,8 +33,6 @@ from typing import BinaryIO, Dict, List, Protocol, Tuple, Union
|
|
|
32
33
|
|
|
33
34
|
from aiohttp.typedefs import LooseHeaders
|
|
34
35
|
|
|
35
|
-
from miniopy_async.error import UnreachableError
|
|
36
|
-
|
|
37
36
|
from . import __title__, __version__
|
|
38
37
|
from .sse import Sse, SseCustomerKey
|
|
39
38
|
from .time import to_iso8601utc
|
|
@@ -209,6 +208,31 @@ class ProgressType(Protocol):
|
|
|
209
208
|
"""Set current progress length."""
|
|
210
209
|
|
|
211
210
|
|
|
211
|
+
async def read_part_data(
|
|
212
|
+
stream: BinaryIO,
|
|
213
|
+
size: int,
|
|
214
|
+
progress: ProgressType | None = None,
|
|
215
|
+
) -> bytes:
|
|
216
|
+
"""Read part data of given size from stream."""
|
|
217
|
+
if not callable(getattr(stream, "read")):
|
|
218
|
+
raise ValueError("stream must have callable read()")
|
|
219
|
+
|
|
220
|
+
part_data = b""
|
|
221
|
+
while size:
|
|
222
|
+
data = stream.read(size)
|
|
223
|
+
if asyncio.iscoroutine(data):
|
|
224
|
+
data = await data
|
|
225
|
+
if not data:
|
|
226
|
+
break # EOF reached
|
|
227
|
+
if not isinstance(data, bytes):
|
|
228
|
+
raise ValueError("read() must return 'bytes' object")
|
|
229
|
+
part_data += data
|
|
230
|
+
size -= len(data)
|
|
231
|
+
if progress:
|
|
232
|
+
await progress.update(len(data))
|
|
233
|
+
return part_data
|
|
234
|
+
|
|
235
|
+
|
|
212
236
|
def makedirs(path: str):
|
|
213
237
|
"""Wrapper of os.makedirs() ignores errno.EEXIST."""
|
|
214
238
|
try:
|
|
@@ -307,7 +331,7 @@ def check_sse(sse: Sse | None):
|
|
|
307
331
|
raise ValueError("Sse type is required")
|
|
308
332
|
|
|
309
333
|
|
|
310
|
-
def md5sum_hash(data: str | bytes | BinaryIO |
|
|
334
|
+
def md5sum_hash(data: str | bytes | BinaryIO | None) -> str | None:
|
|
311
335
|
"""Compute MD5 of data and return hash as Base64 encoded value."""
|
|
312
336
|
if data is None:
|
|
313
337
|
return None
|
|
@@ -320,7 +344,7 @@ def md5sum_hash(data: str | bytes | BinaryIO | Substream | None) -> str | None:
|
|
|
320
344
|
)
|
|
321
345
|
if isinstance(data, (str, bytes)):
|
|
322
346
|
hasher.update(data.encode() if isinstance(data, str) else data)
|
|
323
|
-
elif isinstance(data, (io.BytesIO,
|
|
347
|
+
elif isinstance(data, (io.BytesIO, io.IOBase)) and data.readable():
|
|
324
348
|
pos = data.tell()
|
|
325
349
|
data.seek(0)
|
|
326
350
|
|
|
@@ -341,13 +365,13 @@ def md5sum_hash(data: str | bytes | BinaryIO | Substream | None) -> str | None:
|
|
|
341
365
|
return md5sum.decode()
|
|
342
366
|
|
|
343
367
|
|
|
344
|
-
def sha256_hash(data: str | bytes | BinaryIO |
|
|
368
|
+
def sha256_hash(data: str | bytes | BinaryIO | None) -> str:
|
|
345
369
|
"""Compute SHA-256 of data and return hash as hex encoded value."""
|
|
346
370
|
data = data or b""
|
|
347
371
|
hasher = hashlib.sha256()
|
|
348
372
|
if isinstance(data, (str, bytes)):
|
|
349
373
|
hasher.update(data.encode() if isinstance(data, str) else data)
|
|
350
|
-
elif isinstance(data, (io.BytesIO,
|
|
374
|
+
elif isinstance(data, (io.BytesIO, io.IOBase)) and data.readable():
|
|
351
375
|
pos = data.tell()
|
|
352
376
|
data.seek(0)
|
|
353
377
|
|
|
@@ -856,126 +880,3 @@ class ObjectWriteResult:
|
|
|
856
880
|
def location(self) -> str | None:
|
|
857
881
|
"""Get location."""
|
|
858
882
|
return self._location
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
class Substream(io.IOBase):
|
|
862
|
-
def __init__(self, base: BinaryIO, offset: int, part_size: int):
|
|
863
|
-
if not (hasattr(base, "read") and hasattr(base, "seek")):
|
|
864
|
-
raise TypeError("Base object must support read() and seek()")
|
|
865
|
-
|
|
866
|
-
self._base = base
|
|
867
|
-
self._start = offset
|
|
868
|
-
self._size = self._get_size(part_size)
|
|
869
|
-
self._pos = 0
|
|
870
|
-
self._closed = False
|
|
871
|
-
|
|
872
|
-
def _get_size(self, part_size: int) -> int:
|
|
873
|
-
"""Get size of the substream using bisection."""
|
|
874
|
-
head = self._start
|
|
875
|
-
tail = self._start + part_size - 1
|
|
876
|
-
|
|
877
|
-
self._base.seek(tail)
|
|
878
|
-
data = self._base.read(1)
|
|
879
|
-
if data:
|
|
880
|
-
return part_size
|
|
881
|
-
|
|
882
|
-
self._base.seek(head)
|
|
883
|
-
data = self._base.read(1)
|
|
884
|
-
if not data:
|
|
885
|
-
return 0
|
|
886
|
-
|
|
887
|
-
# bisection
|
|
888
|
-
while tail >= head:
|
|
889
|
-
center = (head + tail) // 2
|
|
890
|
-
self._base.seek(center)
|
|
891
|
-
data = self._base.read(2)
|
|
892
|
-
if not data:
|
|
893
|
-
tail = center - 1
|
|
894
|
-
elif len(data) == 2:
|
|
895
|
-
head = center + 1
|
|
896
|
-
else:
|
|
897
|
-
return center - self._start + 1
|
|
898
|
-
|
|
899
|
-
raise UnreachableError
|
|
900
|
-
|
|
901
|
-
@property
|
|
902
|
-
def size(self) -> int:
|
|
903
|
-
"""Get size of the substream."""
|
|
904
|
-
return self._size
|
|
905
|
-
|
|
906
|
-
def read(self, size: int = -1) -> bytes:
|
|
907
|
-
"""Read data from the substream."""
|
|
908
|
-
self._check_closed()
|
|
909
|
-
self._base.seek(self._start + self._pos)
|
|
910
|
-
|
|
911
|
-
remaining = self._size - self._pos
|
|
912
|
-
if remaining <= 0:
|
|
913
|
-
return b""
|
|
914
|
-
if size < 0 or size > remaining:
|
|
915
|
-
size = remaining
|
|
916
|
-
data = self._base.read(size)
|
|
917
|
-
self._pos += len(data)
|
|
918
|
-
return data
|
|
919
|
-
|
|
920
|
-
def is_empty(self) -> bool:
|
|
921
|
-
"""Check if the substream is empty."""
|
|
922
|
-
pos = self._pos
|
|
923
|
-
|
|
924
|
-
self.seek(0)
|
|
925
|
-
if not self.read(1):
|
|
926
|
-
return True
|
|
927
|
-
|
|
928
|
-
self._pos = pos
|
|
929
|
-
return False
|
|
930
|
-
|
|
931
|
-
def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
|
|
932
|
-
"""Seek to a position in the substream."""
|
|
933
|
-
if whence == io.SEEK_SET:
|
|
934
|
-
new_pos = offset
|
|
935
|
-
elif whence == io.SEEK_CUR:
|
|
936
|
-
new_pos = self._pos + offset
|
|
937
|
-
elif whence == io.SEEK_END:
|
|
938
|
-
new_pos = self._size + offset
|
|
939
|
-
else:
|
|
940
|
-
raise ValueError(f"Invalid whence: {whence!r}")
|
|
941
|
-
|
|
942
|
-
if not 0 <= new_pos <= self._size:
|
|
943
|
-
raise ValueError("seek position out of range")
|
|
944
|
-
|
|
945
|
-
self._pos = new_pos
|
|
946
|
-
return self._pos
|
|
947
|
-
|
|
948
|
-
def tell(self) -> int:
|
|
949
|
-
"""Return the current position in the substream."""
|
|
950
|
-
self._check_closed()
|
|
951
|
-
return self._pos
|
|
952
|
-
|
|
953
|
-
def readable(self) -> bool:
|
|
954
|
-
"""Check if the substream is readable."""
|
|
955
|
-
return True
|
|
956
|
-
|
|
957
|
-
def seekable(self) -> bool:
|
|
958
|
-
"""Check if the substream is seekable."""
|
|
959
|
-
return True
|
|
960
|
-
|
|
961
|
-
def close(self) -> None:
|
|
962
|
-
self._closed = True
|
|
963
|
-
|
|
964
|
-
@property
|
|
965
|
-
def closed(self) -> bool:
|
|
966
|
-
return self._closed
|
|
967
|
-
|
|
968
|
-
def _check_closed(self) -> None:
|
|
969
|
-
if self._closed:
|
|
970
|
-
raise ValueError("I/O operation on closed file.")
|
|
971
|
-
|
|
972
|
-
def __enter__(self):
|
|
973
|
-
self._check_closed()
|
|
974
|
-
return self
|
|
975
|
-
|
|
976
|
-
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
977
|
-
self.close()
|
|
978
|
-
|
|
979
|
-
def remaining(self) -> int:
|
|
980
|
-
"""Bytes left to read."""
|
|
981
|
-
return max(0, self._size - self._pos)
|
|
@@ -55,6 +55,7 @@ from .signer import sign_v4_s3
|
|
|
55
55
|
class _COMMAND(Enum):
|
|
56
56
|
"""Admin Command enumerations."""
|
|
57
57
|
|
|
58
|
+
ACCOUNT_INFO = "accountinfo"
|
|
58
59
|
ADD_USER = "add-user"
|
|
59
60
|
USER_INFO = "user-info"
|
|
60
61
|
LIST_USERS = "list-users"
|
|
@@ -324,6 +325,15 @@ class MinioAdmin:
|
|
|
324
325
|
)
|
|
325
326
|
return await response.text()
|
|
326
327
|
|
|
328
|
+
async def account_info(self, prefix_usage: bool = False) -> str:
|
|
329
|
+
"""Get usage information for the authenticating account"""
|
|
330
|
+
response = await self._url_open(
|
|
331
|
+
"GET",
|
|
332
|
+
_COMMAND.ACCOUNT_INFO,
|
|
333
|
+
query_params={"prefix-usage": "true"} if prefix_usage else None,
|
|
334
|
+
)
|
|
335
|
+
return await response.text()
|
|
336
|
+
|
|
327
337
|
async def user_add(self, access_key: str, secret_key: str) -> str:
|
|
328
338
|
"""Create user with access and secret keys"""
|
|
329
339
|
body = json.dumps({"status": "enabled", "secretKey": secret_key}).encode()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|