miniopy-async 1.23.2__tar.gz → 1.23.3__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.3}/PKG-INFO +1 -1
  2. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/__init__.py +1 -1
  3. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/api.py +311 -5
  4. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/datatypes.py +98 -0
  5. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/helpers.py +55 -30
  6. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/minioadmin.py +10 -0
  7. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/pyproject.toml +1 -1
  8. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/LICENSE +0 -0
  9. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/README.md +0 -0
  10. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/commonconfig.py +0 -0
  11. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/credentials/__init__.py +0 -0
  12. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/credentials/credentials.py +0 -0
  13. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/credentials/providers.py +0 -0
  14. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/crypto.py +0 -0
  15. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/deleteobjects.py +0 -0
  16. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/error.py +0 -0
  17. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/legalhold.py +0 -0
  18. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/lifecycleconfig.py +0 -0
  19. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/notificationconfig.py +0 -0
  20. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/objectlockconfig.py +0 -0
  21. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/py.typed +0 -0
  22. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/replicationconfig.py +0 -0
  23. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/retention.py +0 -0
  24. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/select.py +0 -0
  25. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/signer.py +0 -0
  26. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/sse.py +0 -0
  27. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/sseconfig.py +0 -0
  28. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/tagging.py +0 -0
  29. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/time.py +0 -0
  30. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/miniopy_async/versioningconfig.py +0 -0
  31. {miniopy_async-1.23.2 → miniopy_async-1.23.3}/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.3
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.3"
40
40
  __license__ = "Apache 2.0"
41
41
  __copyright__ = "(C) 2022 L-ING <hlf01@icloud.com>"
42
42
 
@@ -36,6 +36,7 @@ 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,
@@ -361,9 +363,10 @@ class Minio: # pylint: disable=too-many-public-methods
361
363
  self._ensure_session()
362
364
  session = cast(ClientSession | RetryClient, self._session)
363
365
 
366
+ yarl_url = yarl.URL(urlunsplit(url), encoded=True)
364
367
  response = await session.request(
365
368
  method,
366
- urlunsplit(url),
369
+ yarl_url,
367
370
  data=body,
368
371
  headers=http_headers,
369
372
  )
@@ -2296,11 +2299,140 @@ class Minio: # pylint: disable=too-many-public-methods
2296
2299
  element = ET.fromstring(await response.text())
2297
2300
  return cast(str, findtext(element, "UploadId", True))
2298
2301
 
2302
+ def multipart_uploader(
2303
+ self,
2304
+ bucket_name: str,
2305
+ object_name: str,
2306
+ content_type: str = "application/octet-stream",
2307
+ metadata: DictType | None = None,
2308
+ sse: Sse | None = None,
2309
+ tags: Tags | None = None,
2310
+ retention: Retention | None = None,
2311
+ legal_hold: bool = False,
2312
+ extra_headers: DictType | None = None,
2313
+ ) -> MultipartUploader:
2314
+ """
2315
+ Create a multipart uploader for large data.
2316
+
2317
+ :param str bucket_name: Name of the bucket.
2318
+ :param str object_name: Object name in the bucket.
2319
+ :param str content_type: Content type of the object.
2320
+ :param DictType | None metadata: Any additional metadata to be uploaded along
2321
+ with your PUT request.
2322
+ :param Sse | None sse: Server-side encryption.
2323
+ :param Tags | None tags: :class:`Tags` for the object.
2324
+ :param Retention | None retention: :class:`Retention` configuration object.
2325
+ :param bool legal_hold: Flag to set legal hold for the object.
2326
+ :param DictType | None extra_headers: Any additional headers to be added with GET request.
2327
+ :return: :class:`MultipartUploader` object.
2328
+ :rtype: MultipartUploader
2329
+
2330
+ .. code-block:: python
2331
+
2332
+ import asyncio
2333
+
2334
+ from miniopy_async import Minio
2335
+
2336
+ client = Minio(
2337
+ "play.min.io",
2338
+ access_key="Q3AM3UQ867SPQQA43P2F",
2339
+ secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
2340
+ secure=True, # http for False, https for True
2341
+ )
2342
+
2343
+
2344
+ async def main():
2345
+ # Use context manager
2346
+ print("example one")
2347
+ async with client.multipart_uploader(
2348
+ "my-bucket",
2349
+ "my-object",
2350
+ ) as uploader:
2351
+ await uploader.upload_part(b"hello" * 1024 * 1024, 1)
2352
+ await uploader.upload_part(b"world", 2)
2353
+ result = uploader.result
2354
+ print(
2355
+ "created {0} object; etag: {1}, version-id: {2}".format(
2356
+ result.object_name,
2357
+ result.etag,
2358
+ result.version_id,
2359
+ ),
2360
+ )
2361
+
2362
+ # Use directly
2363
+ print("example two")
2364
+ uploader = client.multipart_uploader(
2365
+ "my-bucket",
2366
+ "my-object",
2367
+ )
2368
+ await uploader.upload_part(b"hello" * 1024 * 1024, 1)
2369
+ await uploader.upload_part(b"world", 2)
2370
+ result = await uploader.complete()
2371
+ print(
2372
+ "created {0} object; etag: {1}, version-id: {2}".format(
2373
+ result.object_name,
2374
+ result.etag,
2375
+ result.version_id,
2376
+ ),
2377
+ )
2378
+
2379
+ # Abort
2380
+ print("example three")
2381
+ uploader = client.multipart_uploader(
2382
+ "my-bucket",
2383
+ "my-object",
2384
+ )
2385
+ await uploader.upload_part(b"hello", 1)
2386
+ await uploader.abort()
2387
+
2388
+ # Parallel upload
2389
+ print("example four")
2390
+ tasks = []
2391
+ async with client.multipart_uploader(
2392
+ "my-bucket",
2393
+ "my-object",
2394
+ ) as uploader:
2395
+ for i in range(1, 11):
2396
+ data = (f"part{i}" * 1024 * 1024).encode("utf-8")
2397
+ tasks.append(uploader.upload_part(data, i))
2398
+ await asyncio.gather(*tasks)
2399
+ result = uploader.result
2400
+ print(
2401
+ "created {0} object; etag: {1}, version-id: {2}".format(
2402
+ result.object_name,
2403
+ result.etag,
2404
+ result.version_id,
2405
+ ),
2406
+ )
2407
+
2408
+
2409
+ asyncio.run(main())
2410
+ """
2411
+ check_bucket_name(bucket_name, s3_check=self._base_url.is_aws_host)
2412
+ check_object_name(object_name)
2413
+ check_sse(sse)
2414
+ if tags is not None and not isinstance(tags, Tags):
2415
+ raise ValueError("tags must be Tags type")
2416
+ if retention is not None and not isinstance(retention, Retention):
2417
+ raise ValueError("retention must be Retention type")
2418
+
2419
+ headers = genheaders(metadata, sse, tags, retention, legal_hold)
2420
+ headers["Content-Type"] = content_type or "application/octet-stream"
2421
+ if extra_headers:
2422
+ headers.update(extra_headers)
2423
+
2424
+ return MultipartUploader(
2425
+ self,
2426
+ bucket_name,
2427
+ object_name,
2428
+ headers,
2429
+ )
2430
+
2299
2431
  async def _put_object(
2300
2432
  self,
2301
2433
  bucket_name: str,
2302
2434
  object_name: str,
2303
- data: Substream,
2435
+ data: bytes | BytesIO | Substream,
2304
2436
  headers: DictType | None,
2305
2437
  query_params: DictType | None = None,
2306
2438
  ) -> ObjectWriteResult:
@@ -2326,7 +2458,7 @@ class Minio: # pylint: disable=too-many-public-methods
2326
2458
  self,
2327
2459
  bucket_name: str,
2328
2460
  object_name: str,
2329
- data: Substream,
2461
+ data: bytes | BytesIO | Substream,
2330
2462
  headers: DictType | None,
2331
2463
  upload_id: str,
2332
2464
  part_number: int,
@@ -2371,6 +2503,7 @@ class Minio: # pylint: disable=too-many-public-methods
2371
2503
  tags: Tags | None = None,
2372
2504
  retention: Retention | None = None,
2373
2505
  legal_hold: bool = False,
2506
+ write_offset: int | None = None,
2374
2507
  ) -> ObjectWriteResult:
2375
2508
  """
2376
2509
  Uploads data from a stream to an object in a bucket.
@@ -2389,6 +2522,7 @@ class Minio: # pylint: disable=too-many-public-methods
2389
2522
  :param Tags | None tags: :class:`Tags` for the object.
2390
2523
  :param Retention | None retention: :class:`Retention` configuration object.
2391
2524
  :param bool legal_hold: Flag to set legal hold for the object.
2525
+ :param int | None write_offset: Offset byte for appending data to existing object.
2392
2526
  :return: :class:`ObjectWriteResult` object.
2393
2527
  :rtype: ObjectWriteResult
2394
2528
  :raise ValueError: If tags is not of type :class:`Tags`.
@@ -2493,6 +2627,12 @@ class Minio: # pylint: disable=too-many-public-methods
2493
2627
  raise ValueError("retention must be Retention type")
2494
2628
  if not callable(getattr(data, "read")):
2495
2629
  raise ValueError("input data must have callable read()")
2630
+ if write_offset is not None:
2631
+ if write_offset < 0:
2632
+ raise ValueError("write offset should not be negative")
2633
+ if length < 0:
2634
+ raise ValueError("length must be provided for write offset")
2635
+ part_size = length if length > MIN_PART_SIZE else MIN_PART_SIZE
2496
2636
  part_size, part_count = get_part_info(length, part_size)
2497
2637
  if progress:
2498
2638
  # Set progress bar length and object name before upload
@@ -2500,6 +2640,8 @@ class Minio: # pylint: disable=too-many-public-methods
2500
2640
 
2501
2641
  headers = genheaders(metadata, sse, tags, retention, legal_hold)
2502
2642
  headers["Content-Type"] = content_type or "application/octet-stream"
2643
+ if write_offset:
2644
+ headers["x-amz-write-offset-bytes"] = str(write_offset)
2503
2645
 
2504
2646
  object_size = length
2505
2647
  uploaded_size = 0
@@ -2606,6 +2748,169 @@ class Minio: # pylint: disable=too-many-public-methods
2606
2748
  )
2607
2749
  raise exc
2608
2750
 
2751
+ async def append_object(
2752
+ self,
2753
+ bucket_name: str,
2754
+ object_name: str,
2755
+ data: BinaryIO,
2756
+ length: int,
2757
+ chunk_size: int | None = None,
2758
+ progress: ProgressType | None = None,
2759
+ extra_headers: DictType | None = None,
2760
+ ) -> ObjectWriteResult:
2761
+ """
2762
+ Appends from a stream to existing object in a bucket.
2763
+
2764
+ :param str bucket_name: Name of the bucket.
2765
+ :param str object_name: Object name in the bucket.
2766
+ :param BinaryIO data: An object having callable read() returning bytes object.
2767
+ :param int length: Data size; -1 for unknown size.
2768
+ :param int chunk_size: Chunk size to optimize uploads.
2769
+ :param ProgressType | None progress: A progress object.
2770
+ :param DictType | None extra_headers: Any additional headers to be added with GET request.
2771
+ :return: :class:`ObjectWriteResult` object.
2772
+ :rtype: ObjectWriteResult
2773
+ :raise ValueError: If length is 0.
2774
+ :raise ValueError: If chunk_size is less than 5 MiB or greater than 5 GiB.
2775
+ :raise IOError: If stream doesn't have enough data to read.
2776
+
2777
+ .. code-block:: python
2778
+
2779
+ import asyncio
2780
+ import io
2781
+ from urllib.request import urlopen
2782
+
2783
+ from miniopy_async import Minio
2784
+
2785
+ client = Minio(
2786
+ "play.min.io",
2787
+ access_key="Q3AM3UQ867SPQQA43P2F",
2788
+ secret_key="zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG",
2789
+ secure=True, # http for False, https for True
2790
+ )
2791
+
2792
+
2793
+ async def main():
2794
+ # Upload data.
2795
+ result = await client.put_object(
2796
+ "my-bucket",
2797
+ "my-object",
2798
+ io.BytesIO(b"hello, "),
2799
+ 7,
2800
+ )
2801
+ print(f"created {result.object_name} object; etag: {result.etag}")
2802
+
2803
+ # Append data.
2804
+ result = await client.append_object(
2805
+ "my-bucket",
2806
+ "my-object",
2807
+ io.BytesIO(b"world"),
2808
+ 5,
2809
+ )
2810
+ print(f"appended {result.object_name} object; etag: {result.etag}")
2811
+
2812
+ # Append data in chunks.
2813
+ data = urlopen(
2814
+ "https://www.kernel.org/pub/linux/kernel/v6.x/linux-6.13.12.tar.xz",
2815
+ )
2816
+ result = await client.append_object(
2817
+ "my-bucket",
2818
+ "my-object",
2819
+ data,
2820
+ 148611164,
2821
+ 5 * 1024 * 1024,
2822
+ )
2823
+ print(f"appended {result.object_name} object; etag: {result.etag}")
2824
+
2825
+ # Append unknown sized data.
2826
+ data = urlopen(
2827
+ "https://www.kernel.org/pub/linux/kernel/v6.x/linux-6.14.3.tar.xz",
2828
+ )
2829
+ result = await client.append_object(
2830
+ "my-bucket",
2831
+ "my-object",
2832
+ data,
2833
+ 149426584,
2834
+ 5 * 1024 * 1024,
2835
+ )
2836
+ print(f"appended {result.object_name} object; etag: {result.etag}")
2837
+
2838
+
2839
+ asyncio.run(main())
2840
+ """
2841
+ if length == 0:
2842
+ raise ValueError("length should not be zero")
2843
+ if chunk_size is not None:
2844
+ if chunk_size < MIN_PART_SIZE:
2845
+ raise ValueError("chunk size must be minimum of 5 MiB")
2846
+ if chunk_size > MAX_PART_SIZE:
2847
+ raise ValueError("chunk size must be less than 5 GiB")
2848
+ else:
2849
+ chunk_size = length if length > MIN_PART_SIZE else MIN_PART_SIZE
2850
+
2851
+ chunk_count = -1
2852
+ if length > 0:
2853
+ chunk_count = int(length / chunk_size)
2854
+ if (chunk_count * chunk_size) < length:
2855
+ chunk_count += 1
2856
+ chunk_count = chunk_count or 1
2857
+
2858
+ object_size = length
2859
+ uploaded_size = 0
2860
+ chunk_number = 0
2861
+ stop = False
2862
+
2863
+ stat = await self.stat_object(bucket_name, object_name)
2864
+ write_offset = cast(int, stat.size)
2865
+ upload_result = None
2866
+
2867
+ while not stop:
2868
+ chunk_number += 1
2869
+ if chunk_count > 0:
2870
+ if chunk_number == chunk_count:
2871
+ chunk_size = object_size - uploaded_size
2872
+ stop = True
2873
+ chunk_data = Substream(data, uploaded_size, chunk_size)
2874
+ if chunk_data.size != chunk_size:
2875
+ raise IOError(
2876
+ f"stream having not enough data;"
2877
+ f"expected: {chunk_size}, "
2878
+ f"got: {chunk_data.size} bytes"
2879
+ )
2880
+ else:
2881
+ chunk_data = Substream(data, uploaded_size, chunk_size)
2882
+ if chunk_data.size == 0:
2883
+ break
2884
+ if chunk_data.size < chunk_size:
2885
+ chunk_count = chunk_number
2886
+ stop = True
2887
+ chunk_size = chunk_data.size
2888
+
2889
+ if progress:
2890
+ await progress.update(chunk_size)
2891
+
2892
+ uploaded_size += chunk_size
2893
+
2894
+ headers = extra_headers or {}
2895
+ headers["x-amz-write-offset-bytes"] = str(write_offset)
2896
+ upload_result = await self._put_object(
2897
+ bucket_name,
2898
+ object_name,
2899
+ chunk_data,
2900
+ headers=headers,
2901
+ )
2902
+ write_offset += chunk_size
2903
+
2904
+ upload_result = cast(ObjectWriteResult, upload_result)
2905
+ return ObjectWriteResult(
2906
+ cast(str, upload_result.bucket_name),
2907
+ cast(str, upload_result.object_name),
2908
+ upload_result.version_id,
2909
+ upload_result.etag,
2910
+ upload_result.http_headers,
2911
+ location=upload_result.location,
2912
+ )
2913
+
2609
2914
  def list_objects(
2610
2915
  self,
2611
2916
  bucket_name: str,
@@ -4228,9 +4533,10 @@ class Minio: # pylint: disable=too-many-public-methods
4228
4533
  metadata["X-Amz-Meta-Snowball-Auto-Extract"] = "true"
4229
4534
 
4230
4535
  name = staging_filename
4231
- mode = "w:gz" if compression else "w"
4232
4536
  fileobj = None if name else BytesIO()
4233
- with tarfile.open(name=name, mode=mode, fileobj=fileobj) as tar:
4537
+ with tarfile.open(
4538
+ name=name, mode="w:gz" if compression else "w", fileobj=fileobj
4539
+ ) as tar:
4234
4540
  for obj in object_list:
4235
4541
  if obj.filename:
4236
4542
  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
@@ -26,9 +26,10 @@ import math
26
26
  import os
27
27
  import platform
28
28
  import re
29
+ import threading
29
30
  import urllib.parse
30
31
  from datetime import datetime
31
- from typing import BinaryIO, Dict, List, Protocol, Tuple, Union
32
+ from typing import BinaryIO, Dict, List, Protocol, Tuple, Union, cast
32
33
 
33
34
  from aiohttp.typedefs import LooseHeaders
34
35
 
@@ -865,36 +866,67 @@ class Substream(io.IOBase):
865
866
 
866
867
  self._base = base
867
868
  self._start = offset
868
- self._size = self._get_size(part_size)
869
869
  self._pos = 0
870
870
  self._closed = False
871
871
 
872
+ self._lock = threading.RLock()
873
+ self._fd: int | None = None
874
+ self._has_pread: bool = False
875
+ try:
876
+ if hasattr(base, "fileno"):
877
+ self._fd = base.fileno()
878
+ self._has_pread = self._fd is not None and hasattr(os, "pread")
879
+ except Exception:
880
+ self._fd = None
881
+ self._has_pread = False
882
+
883
+ self._size = self._get_size(part_size)
884
+
885
+ def _read_at(self, abs_offset: int, n: int) -> bytes:
886
+ """Read n bytes from the underlying stream at absolute position abs_offset without changing other readers' pointers."""
887
+ if n <= 0:
888
+ return b""
889
+ if self._has_pread:
890
+ # POSIX: thread-safe, does not affect file pointer
891
+ return os.pread(cast(int, self._fd), n, abs_offset)
892
+ # Fallback: save/restore pointer + lock, ensure atomicity within this object
893
+ with self._lock:
894
+ curr = self._base.tell()
895
+ try:
896
+ self._base.seek(abs_offset)
897
+ return self._base.read(n)
898
+ finally:
899
+ try:
900
+ self._base.seek(curr)
901
+ except Exception:
902
+ pass
903
+
872
904
  def _get_size(self, part_size: int) -> int:
873
905
  """Get size of the substream using bisection."""
874
- head = self._start
875
- tail = self._start + part_size - 1
906
+ if part_size == 0:
907
+ return 0
908
+
909
+ start = self._start
910
+ end = start + part_size
876
911
 
877
- self._base.seek(tail)
878
- data = self._base.read(1)
879
- if data:
912
+ if self._read_at(end - 1, 1):
880
913
  return part_size
881
914
 
882
- self._base.seek(head)
883
- data = self._base.read(1)
884
- if not data:
915
+ if not self._read_at(start, 1):
885
916
  return 0
886
917
 
887
918
  # 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
919
+ lo = start
920
+ hi = end - 1
921
+ while lo <= hi:
922
+ mid = (lo + hi) // 2
923
+ if self._read_at(mid, 1):
924
+ if self._read_at(mid + 1, 1):
925
+ lo = mid + 1
926
+ else:
927
+ return mid - start + 1
896
928
  else:
897
- return center - self._start + 1
929
+ hi = mid - 1
898
930
 
899
931
  raise UnreachableError
900
932
 
@@ -906,27 +938,20 @@ class Substream(io.IOBase):
906
938
  def read(self, size: int = -1) -> bytes:
907
939
  """Read data from the substream."""
908
940
  self._check_closed()
909
- self._base.seek(self._start + self._pos)
910
-
911
941
  remaining = self._size - self._pos
912
942
  if remaining <= 0:
913
943
  return b""
914
944
  if size < 0 or size > remaining:
915
945
  size = remaining
916
- data = self._base.read(size)
946
+ abs_off = self._start + self._pos
947
+ data = self._read_at(abs_off, size)
948
+
917
949
  self._pos += len(data)
918
950
  return data
919
951
 
920
952
  def is_empty(self) -> bool:
921
953
  """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
954
+ return not bool(self._read_at(self._start, 1))
930
955
 
931
956
  def seek(self, offset: int, whence: int = io.SEEK_SET) -> int:
932
957
  """Seek to a position in the substream."""
@@ -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.3"
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