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.
Files changed (31) hide show
  1. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/PKG-INFO +1 -1
  2. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/__init__.py +1 -1
  3. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/api.py +342 -28
  4. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/datatypes.py +98 -0
  5. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/error.py +0 -7
  6. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/helpers.py +30 -129
  7. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/minioadmin.py +10 -0
  8. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/pyproject.toml +1 -1
  9. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/LICENSE +0 -0
  10. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/README.md +0 -0
  11. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/commonconfig.py +0 -0
  12. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/credentials/__init__.py +0 -0
  13. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/credentials/credentials.py +0 -0
  14. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/credentials/providers.py +0 -0
  15. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/crypto.py +0 -0
  16. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/deleteobjects.py +0 -0
  17. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/legalhold.py +0 -0
  18. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/lifecycleconfig.py +0 -0
  19. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/notificationconfig.py +0 -0
  20. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/objectlockconfig.py +0 -0
  21. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/py.typed +0 -0
  22. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/replicationconfig.py +0 -0
  23. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/retention.py +0 -0
  24. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/select.py +0 -0
  25. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/signer.py +0 -0
  26. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/sse.py +0 -0
  27. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/sseconfig.py +0 -0
  28. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/tagging.py +0 -0
  29. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/time.py +0 -0
  30. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/versioningconfig.py +0 -0
  31. {miniopy_async-1.23.2 → miniopy_async-1.23.4}/miniopy_async/xml.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: miniopy-async
3
- Version: 1.23.2
3
+ Version: 1.23.4
4
4
  Summary: Asynchronous MinIO Client SDK for Python
5
5
  Home-page: https://github.com/hlf20010508/miniopy-async
6
6
  License: Apache-2.0
@@ -36,7 +36,7 @@ miniopy-async - Asynchronous MinIO Client SDK for Python
36
36
 
37
37
  __title__ = "miniopy-async"
38
38
  __author__ = "L-ING"
39
- __version__ = "1.23.2"
39
+ __version__ = "1.23.4"
40
40
  __license__ = "Apache 2.0"
41
41
  __copyright__ = "(C) 2022 L-ING <hlf01@icloud.com>"
42
42
 
@@ -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 | BytesIO | Substream | None,
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, Substream):
283
- headers["Content-Length"] = str(body.size)
284
- else:
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 | BytesIO | Substream | None = None,
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
- urlunsplit(url),
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 | BytesIO | Substream | None = None,
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: Substream,
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: Substream,
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 | FileIOWrapperBase data: An object having callable read() returning bytes object.
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 = Substream(data, uploaded_size, part_size)
2521
- if part_data.size != part_size:
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.size} bytes"
2679
+ f"got: {len(part_data)} bytes"
2526
2680
  )
2527
2681
  else:
2528
- part_data = Substream(data, uploaded_size, part_size)
2529
- if part_data.size == 0:
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.size < part_size:
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 += part_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(name=name, mode=mode, fileobj=fileobj) as tar:
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 | Substream | None) -> str | None:
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, Substream)) and data.readable():
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 | Substream | None) -> str:
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, Substream)) and data.readable():
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()
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "miniopy-async"
3
- version = "1.23.2"
3
+ version = "1.23.4"
4
4
  description = "Asynchronous MinIO Client SDK for Python"
5
5
  authors = [
6
6
  "L-ING <hlf01@icloud.com>",
File without changes
File without changes