plexus-python-common 1.0.34__tar.gz → 1.0.35__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 (82) hide show
  1. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/PKG-INFO +2 -3
  2. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/pyproject.toml +2 -4
  3. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/s3utils.py +148 -115
  4. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus_python_common.egg-info/PKG-INFO +2 -3
  5. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus_python_common.egg-info/requires.txt +1 -2
  6. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/utils/s3utils_test.py +16 -16
  7. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/.editorconfig +0 -0
  8. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/.github/workflows/pr.yml +0 -0
  9. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/.github/workflows/push.yml +0 -0
  10. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/.gitignore +0 -0
  11. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/MANIFEST.in +0 -0
  12. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/README.md +0 -0
  13. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/VERSION +0 -0
  14. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/jsonutils/dummy.0.jsonl +0 -0
  15. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/jsonutils/dummy.1.jsonl +0 -0
  16. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/jsonutils/dummy.2.jsonl +0 -0
  17. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils/dir.baz/file.bar.baz +0 -0
  18. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils/dir.baz/file.foo.bar +0 -0
  19. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils/dir.baz/file.foo.baz +0 -0
  20. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  21. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  22. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  23. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  24. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils/dir.foo/file.bar +0 -0
  25. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils/dir.foo/file.baz +0 -0
  26. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils/dir.foo/file.foo +0 -0
  27. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils_archive/archive.compressed.zip +0 -0
  28. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/s3utils_archive/archive.uncompressed.zip +0 -0
  29. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/0-dummy +0 -0
  30. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/1-dummy +0 -0
  31. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/2-dummy +0 -0
  32. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.0.0.jsonl +0 -0
  33. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.0.0.vol-0.jsonl +0 -0
  34. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.0.jsonl +0 -0
  35. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.1.1.jsonl +0 -0
  36. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.1.1.vol-1.jsonl +0 -0
  37. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.1.jsonl +0 -0
  38. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.2.2.jsonl +0 -0
  39. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.2.2.vol-2.jsonl +0 -0
  40. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.2.jsonl +0 -0
  41. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.csv.part0 +0 -0
  42. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.csv.part1 +0 -0
  43. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.csv.part2 +0 -0
  44. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/resources/unittest/shutils/dummy.txt +0 -0
  45. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/setup.cfg +0 -0
  46. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/setup.py +0 -0
  47. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/__init__.py +0 -0
  48. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/carto/OSMFile.py +0 -0
  49. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/carto/OSMNode.py +0 -0
  50. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/carto/OSMTags.py +0 -0
  51. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/carto/OSMWay.py +0 -0
  52. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/carto/__init__.py +0 -0
  53. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/pose.py +0 -0
  54. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/proj.py +0 -0
  55. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/__init__.py +0 -0
  56. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/apiutils.py +0 -0
  57. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/bagutils.py +0 -0
  58. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/config.py +0 -0
  59. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/datautils.py +0 -0
  60. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/jsonutils.py +0 -0
  61. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/ormutils.py +0 -0
  62. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/shutils.py +0 -0
  63. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/sqlutils.py +0 -0
  64. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/strutils.py +0 -0
  65. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus/common/utils/testutils.py +0 -0
  66. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus_python_common.egg-info/SOURCES.txt +0 -0
  67. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus_python_common.egg-info/dependency_links.txt +0 -0
  68. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus_python_common.egg-info/not-zip-safe +0 -0
  69. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/src/plexus_python_common.egg-info/top_level.txt +0 -0
  70. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_test.py +0 -0
  71. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/__init__.py +0 -0
  72. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/carto/osm_file_test.py +0 -0
  73. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/carto/osm_tags_test.py +0 -0
  74. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/pose_test.py +0 -0
  75. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/proj_test.py +0 -0
  76. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/utils/bagutils_test.py +0 -0
  77. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/utils/datautils_test.py +0 -0
  78. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/utils/jsonutils_test.py +0 -0
  79. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/utils/ormutils_test.py +0 -0
  80. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/utils/shutils_test.py +0 -0
  81. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/utils/strutils_test.py +0 -0
  82. {plexus_python_common-1.0.34 → plexus_python_common-1.0.35}/test/plexus_tests/common/utils/testutils_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-common
3
- Version: 1.0.34
3
+ Version: 1.0.35
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -8,8 +8,7 @@ Classifier: Programming Language :: Python :: 3.14
8
8
  Requires-Python: <3.15,>=3.12
9
9
  Requires-Dist: aiobotocore<2.27,>=2.26
10
10
  Requires-Dist: boto3<1.42,>=1.41
11
- Requires-Dist: boto3-stubs<1.42,>=1.41
12
- Requires-Dist: cloudpathlib>=0.21
11
+ Requires-Dist: boto3-stubs[s3]<1.42,>=1.41
13
12
  Requires-Dist: fsspec>=2025.10
14
13
  Requires-Dist: lxml>=6.0
15
14
  Requires-Dist: numpy>=2.3
@@ -10,8 +10,7 @@ build-backend = "setuptools.build_meta"
10
10
  dev = [
11
11
  "aiobotocore>=2.26,<2.27",
12
12
  "boto3>=1.41,<1.42",
13
- "boto3-stubs>=1.41,<1.42",
14
- "cloudpathlib>=0.21",
13
+ "boto3-stubs[s3]>=1.41,<1.42",
15
14
  "fsspec>=2025.10",
16
15
  "lxml>=6.0",
17
16
  "numpy>=2.3",
@@ -53,8 +52,7 @@ classifiers = [
53
52
  dependencies = [
54
53
  "aiobotocore>=2.26,<2.27",
55
54
  "boto3>=1.41,<1.42",
56
- "boto3-stubs>=1.41,<1.42",
57
- "cloudpathlib>=0.21",
55
+ "boto3-stubs[s3]>=1.41,<1.42",
58
56
  "fsspec>=2025.10",
59
57
  "lxml>=6.0",
60
58
  "numpy>=2.3",
@@ -13,21 +13,21 @@ import typing
13
13
  import zipfile
14
14
  import zlib
15
15
  from collections.abc import Callable, Generator
16
- from pathlib import Path
17
16
  from typing import Any, Literal
18
17
 
19
18
  import boto3
20
19
  import fsspec
21
20
  import fsspec.utils
22
- from cloudpathlib import CloudPath, S3Client, S3Path
23
21
  from iker.common.utils.sequtils import chunk_between, head, last
24
22
  from iker.common.utils.shutils import glob_match, listfile, path_depth
25
23
  from iker.common.utils.strutils import is_empty, trim_to_none
26
- from rich.progress import BarColumn, DownloadColumn, Progress, TaskID, TextColumn, TransferSpeedColumn
24
+ from mypy_boto3_s3 import S3Client
25
+ from rich.progress import BarColumn, DownloadColumn, Progress, TextColumn, TransferSpeedColumn
27
26
 
28
27
  __all__ = [
29
28
  "S3ObjectMeta",
30
29
  "s3_make_client",
30
+ "s3_head_object",
31
31
  "s3_list_objects",
32
32
  "s3_listfile",
33
33
  "s3_cp_download",
@@ -36,10 +36,8 @@ __all__ = [
36
36
  "s3_sync_upload",
37
37
  "s3_pull_text",
38
38
  "s3_push_text",
39
- "S3TransferCallbackClient",
40
- "ArchiveMemberChunk",
41
- "s3_make_progress_callback",
42
39
  "s3_make_progressed_client",
40
+ "ArchiveMemberChunk",
43
41
  "s3_archive_member_tree",
44
42
  "s3_archive_listfile",
45
43
  "s3_archive_open_member",
@@ -84,12 +82,28 @@ def s3_make_client(
84
82
  session = boto3.Session(aws_access_key_id=trim_to_none(access_key_id),
85
83
  aws_secret_access_key=trim_to_none(secret_access_key),
86
84
  region_name=trim_to_none(region_name))
87
- client = S3Client(boto3_session=session, endpoint_url=trim_to_none(endpoint_url))
85
+ client = session.client("s3", endpoint_url=trim_to_none(endpoint_url))
88
86
  try:
89
87
  yield client
90
88
  finally:
91
- if hasattr(client, "close"):
92
- client.close()
89
+ client.close()
90
+
91
+
92
+ def s3_head_object(
93
+ client: S3Client,
94
+ bucket: str,
95
+ key: str,
96
+ ) -> S3ObjectMeta:
97
+ """
98
+ Retrieves metadata of an object from the given S3 ``bucket`` and ``key``.
99
+
100
+ :param client: An instance of ``S3Client``.
101
+ :param bucket: Bucket name.
102
+ :param key: Object key.
103
+ :return: An ``S3ObjectMeta`` object representing the S3 object.
104
+ """
105
+ response = client.head_object(Bucket=bucket, Key=key)
106
+ return S3ObjectMeta(key=key, last_modified=response["LastModified"], size=response["ContentLength"])
93
107
 
94
108
 
95
109
  def s3_list_objects(
@@ -111,12 +125,12 @@ def s3_list_objects(
111
125
  count = 0
112
126
  while True:
113
127
  if is_empty(continuation_token):
114
- response = client.client.list_objects_v2(MaxKeys=1000, Bucket=bucket, Prefix=prefix)
128
+ response = client.list_objects_v2(MaxKeys=1000, Bucket=bucket, Prefix=prefix)
115
129
  else:
116
- response = client.client.list_objects_v2(MaxKeys=1000,
117
- Bucket=bucket,
118
- Prefix=prefix,
119
- ContinuationToken=continuation_token)
130
+ response = client.list_objects_v2(MaxKeys=1000,
131
+ Bucket=bucket,
132
+ Prefix=prefix,
133
+ ContinuationToken=continuation_token)
120
134
 
121
135
  contents = response.get("Contents", [])
122
136
  count += len(contents)
@@ -175,7 +189,7 @@ def s3_cp_download(client: S3Client, bucket: str, key: str, file_path: str | os.
175
189
  :param key: Object key.
176
190
  :param file_path: Local file path to save the object.
177
191
  """
178
- client.client.download_file(bucket, key, file_path)
192
+ client.download_file(bucket, key, file_path)
179
193
 
180
194
 
181
195
  def s3_cp_upload(client: S3Client, file_path: str | os.PathLike[str], bucket: str, key: str):
@@ -188,10 +202,10 @@ def s3_cp_upload(client: S3Client, file_path: str | os.PathLike[str], bucket: st
188
202
  :param key: Object key for the uploaded file.
189
203
  """
190
204
  t, _ = mimetypes.MimeTypes().guess_type(file_path)
191
- client.client.upload_file(file_path,
192
- bucket,
193
- key,
194
- ExtraArgs={"ContentType": "binary/octet-stream" if t is None else t})
205
+ client.upload_file(file_path,
206
+ bucket,
207
+ key,
208
+ ExtraArgs={"ContentType": "binary/octet-stream" if t is None else t})
195
209
 
196
210
 
197
211
  def s3_sync_download(
@@ -311,7 +325,7 @@ def s3_pull_text(client: S3Client, bucket: str, key: str, encoding: str = None)
311
325
  :return: The decoded text content.
312
326
  """
313
327
  with tempfile.TemporaryFile() as fp:
314
- client.client.download_fileobj(bucket, key, fp)
328
+ client.download_fileobj(bucket, key, fp)
315
329
  fp.seek(0)
316
330
  return fp.read().decode(encoding or "utf-8")
317
331
 
@@ -329,95 +343,111 @@ def s3_push_text(client: S3Client, text: str, bucket: str, key: str, encoding: s
329
343
  with tempfile.TemporaryFile() as fp:
330
344
  fp.write(text.encode(encoding or "utf-8"))
331
345
  fp.seek(0)
332
- client.client.upload_fileobj(fp, bucket, key)
346
+ client.upload_fileobj(fp, bucket, key)
333
347
 
334
348
 
335
- TransferDirection = Literal["download", "upload"]
336
- TransferState = Literal["start", "update", "stop"]
349
+ class S3ClientProgressProxy(object):
350
+ def __init__(self, client: S3Client, progress: Progress):
351
+ self.client = client
352
+ self.progress = progress
337
353
 
354
+ @contextlib.contextmanager
355
+ def make_transfer_callback(self, key: str, bytes_total: int, direction: Literal["download", "upload"]):
356
+ task_id = self.progress.add_task(direction, total=bytes_total, key=key)
357
+ try:
358
+ yield lambda bytes_sent: self.progress.update(task_id, advance=bytes_sent)
359
+ finally:
360
+ self.progress.remove_task(task_id)
338
361
 
339
- @contextlib.contextmanager
340
- def make_transfer_callback(
341
- callback: Callable[[CloudPath, TransferDirection, TransferState, int], None],
342
- path: Path | CloudPath,
343
- direction: TransferDirection,
344
- ):
345
- if callback is None:
346
- yield None
347
- return
348
-
349
- callback(path, direction, "start", 0)
350
- try:
351
- yield functools.partial(callback, path, direction, "update")
352
- finally:
353
- callback(path, direction, "stop", 0)
362
+ def __getattr__(self, name):
363
+ return getattr(self.client, name)
354
364
 
355
-
356
- class S3TransferCallbackClient(S3Client):
357
- def __init__(
365
+ def download_file(
358
366
  self,
359
- *args,
360
- transfer_callback: Callable[[Path | CloudPath, TransferDirection, TransferState, int], None],
361
- **kwargs,
367
+ Bucket,
368
+ Key,
369
+ Filename,
370
+ ExtraArgs=None,
371
+ Callback=None,
372
+ Config=None,
362
373
  ):
363
- super().__init__(*args, **kwargs)
364
- self.transfer_callback = transfer_callback
365
-
366
- def _download_file(self, cloud_path: S3Path, local_path: str | os.PathLike[str]) -> Path:
367
- local_path = Path(local_path)
368
-
369
- obj = self.s3.Object(cloud_path.bucket, cloud_path.key)
370
-
371
- with make_transfer_callback(self.transfer_callback, cloud_path, "download") as callback:
372
- obj.download_file(
373
- str(local_path),
374
- Config=self.boto3_transfer_config,
375
- ExtraArgs=self.boto3_dl_extra_args,
376
- Callback=callback,
377
- )
378
- return local_path
379
-
380
- def _upload_file(self, local_path: str | os.PathLike[str], cloud_path: S3Path) -> S3Path:
381
- local_path = Path(local_path)
382
-
383
- obj = self.s3.Object(cloud_path.bucket, cloud_path.key)
384
-
385
- extra_args = self.boto3_ul_extra_args.copy()
386
-
387
- if self.content_type_method is not None:
388
- content_type, content_encoding = self.content_type_method(str(local_path))
389
- if content_type is not None:
390
- extra_args["ContentType"] = content_type
391
- if content_encoding is not None:
392
- extra_args["ContentEncoding"] = content_encoding
393
-
394
- with make_transfer_callback(self.transfer_callback, local_path, "upload") as callback:
395
- obj.upload_file(
396
- str(local_path),
397
- Config=self.boto3_transfer_config,
398
- ExtraArgs=extra_args,
399
- Callback=callback,
400
- )
401
- return cloud_path
402
-
403
-
404
- def s3_make_progress_callback(
405
- progress: Progress,
406
- ) -> Callable[[Path | CloudPath, TransferDirection, TransferState, int], None]:
407
- task_ids: dict[Path | CloudPath, TaskID] = {}
408
-
409
- def progress_callback(path: Path | CloudPath, direction: TransferDirection, state: TransferState, bytes_sent: int):
410
- if state == "start":
411
- size = path.stat().st_size
412
- task_ids[path] = progress.add_task(direction, total=size, filename=path.name)
413
- elif state == "stop":
414
- if path in task_ids:
415
- progress.remove_task(task_ids[path])
416
- del task_ids[path]
417
- else:
418
- progress.update(task_ids[path], advance=bytes_sent)
419
-
420
- return progress_callback
374
+ object_meta = s3_head_object(self.client, Bucket, Key)
375
+ with (
376
+ contextlib.nullcontext(Callback) if Callback is not None
377
+ else self.make_transfer_callback(Key, object_meta.size, "download")
378
+ ) as callback:
379
+ return self.client.download_file(Bucket,
380
+ Key,
381
+ Filename,
382
+ ExtraArgs=ExtraArgs,
383
+ Callback=callback,
384
+ Config=Config)
385
+
386
+ def download_fileobj(
387
+ self,
388
+ Bucket,
389
+ Key,
390
+ Fileobj,
391
+ ExtraArgs=None,
392
+ Callback=None,
393
+ Config=None,
394
+ ):
395
+ object_meta = s3_head_object(self.client, Bucket, Key)
396
+ with (
397
+ contextlib.nullcontext(Callback) if Callback is not None
398
+ else self.make_transfer_callback(Key, object_meta.size, "download")
399
+ ) as callback:
400
+ return self.client.download_fileobj(Bucket,
401
+ Key,
402
+ Fileobj,
403
+ ExtraArgs=ExtraArgs,
404
+ Callback=callback,
405
+ Config=Config)
406
+
407
+ def upload_file(
408
+ self,
409
+ Filename,
410
+ Bucket,
411
+ Key,
412
+ ExtraArgs=None,
413
+ Callback=None,
414
+ Config=None,
415
+ ):
416
+ bytes_total = os.path.getsize(Filename)
417
+ with (
418
+ contextlib.nullcontext(Callback) if Callback is not None
419
+ else self.make_transfer_callback(Key, bytes_total, "upload")
420
+ ) as callback:
421
+ return self.client.upload_file(Filename,
422
+ Bucket,
423
+ Key,
424
+ ExtraArgs=ExtraArgs,
425
+ Callback=callback,
426
+ Config=Config)
427
+
428
+ def upload_fileobj(
429
+ self,
430
+ Fileobj,
431
+ Bucket,
432
+ Key,
433
+ ExtraArgs=None,
434
+ Callback=None,
435
+ Config=None,
436
+ ):
437
+ current_pos = Fileobj.tell()
438
+ Fileobj.seek(0, os.SEEK_END)
439
+ bytes_total = Fileobj.tell() - current_pos
440
+ Fileobj.seek(current_pos)
441
+ with (
442
+ contextlib.nullcontext(Callback) if Callback is not None
443
+ else self.make_transfer_callback(Key, bytes_total, "upload")
444
+ ) as callback:
445
+ return self.client.upload_fileobj(Fileobj,
446
+ Bucket,
447
+ Key,
448
+ ExtraArgs=ExtraArgs,
449
+ Callback=callback,
450
+ Config=Config)
421
451
 
422
452
 
423
453
  if typing.TYPE_CHECKING:
@@ -435,18 +465,18 @@ def s3_make_progressed_client(
435
465
  secret_access_key: str = None,
436
466
  region_name: str = None,
437
467
  endpoint_url: str = None,
438
- ) -> Generator[S3Client, None, None]:
468
+ ) -> Generator[S3ClientProgressProxy, None, None]:
439
469
  """
440
- Creates an S3 client with progress callback as a context manager for safe resource handling.
470
+ Creates an S3 client with progress reporting as a context manager.
441
471
 
442
472
  :param access_key_id: AWS access key ID.
443
473
  :param secret_access_key: AWS secret access key.
444
474
  :param region_name: AWS service region name.
445
475
  :param endpoint_url: AWS service endpoint URL.
446
- :return: An instance of ``S3TransferCallbackClient``.
476
+ :return: An instance of ``S3Client`` with progress reporting.
447
477
  """
448
478
  with Progress(
449
- TextColumn("[blue]{task.fields[filename]}"),
479
+ TextColumn("[blue]{task.fields[key]}"),
450
480
  BarColumn(),
451
481
  DownloadColumn(),
452
482
  TransferSpeedColumn(),
@@ -454,9 +484,12 @@ def s3_make_progressed_client(
454
484
  session = boto3.Session(aws_access_key_id=trim_to_none(access_key_id),
455
485
  aws_secret_access_key=trim_to_none(secret_access_key),
456
486
  region_name=trim_to_none(region_name))
457
- yield S3TransferCallbackClient(boto3_session=session,
458
- endpoint_url=trim_to_none(endpoint_url),
459
- transfer_callback=s3_make_progress_callback(progress))
487
+ client = session.client("s3", endpoint_url=trim_to_none(endpoint_url))
488
+ proxy = S3ClientProgressProxy(client, progress)
489
+ try:
490
+ yield proxy
491
+ finally:
492
+ proxy.close()
460
493
 
461
494
 
462
495
  def s3_options_from_s3_client(client: S3Client) -> dict[str, Any]:
@@ -466,12 +499,12 @@ def s3_options_from_s3_client(client: S3Client) -> dict[str, Any]:
466
499
  :param client: An instance of ``S3Client``.
467
500
  :return: A dictionary of S3 connection options.
468
501
  """
469
- if client.sess is None:
502
+ if client is None:
470
503
  return {}
471
504
 
472
505
  s3_options: dict[str, Any] = {}
473
506
 
474
- credentials = client.sess.get_credentials()
507
+ credentials = client._request_signer._credentials
475
508
  if credentials is not None:
476
509
  if credentials.access_key:
477
510
  s3_options["key"] = credentials.access_key
@@ -481,10 +514,10 @@ def s3_options_from_s3_client(client: S3Client) -> dict[str, Any]:
481
514
  s3_options["token"] = credentials.token
482
515
 
483
516
  client_kwargs = {}
484
- if client.sess.region_name or client.client.meta.region_name:
485
- client_kwargs["region_name"] = client.sess.region_name or client.client.meta.region_name
486
- if client.client.meta.endpoint_url:
487
- client_kwargs["endpoint_url"] = client.client.meta.endpoint_url
517
+ if client.meta.region_name:
518
+ client_kwargs["region_name"] = client.meta.region_name
519
+ if client.meta.endpoint_url:
520
+ client_kwargs["endpoint_url"] = client.meta.endpoint_url
488
521
 
489
522
  if client_kwargs:
490
523
  s3_options["client_kwargs"] = client_kwargs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plexus-python-common
3
- Version: 1.0.34
3
+ Version: 1.0.35
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -8,8 +8,7 @@ Classifier: Programming Language :: Python :: 3.14
8
8
  Requires-Python: <3.15,>=3.12
9
9
  Requires-Dist: aiobotocore<2.27,>=2.26
10
10
  Requires-Dist: boto3<1.42,>=1.41
11
- Requires-Dist: boto3-stubs<1.42,>=1.41
12
- Requires-Dist: cloudpathlib>=0.21
11
+ Requires-Dist: boto3-stubs[s3]<1.42,>=1.41
13
12
  Requires-Dist: fsspec>=2025.10
14
13
  Requires-Dist: lxml>=6.0
15
14
  Requires-Dist: numpy>=2.3
@@ -1,7 +1,6 @@
1
1
  aiobotocore<2.27,>=2.26
2
2
  boto3<1.42,>=1.41
3
- boto3-stubs<1.42,>=1.41
4
- cloudpathlib>=0.21
3
+ boto3-stubs[s3]<1.42,>=1.41
5
4
  fsspec>=2025.10
6
5
  lxml>=6.0
7
6
  numpy>=2.3
@@ -31,7 +31,7 @@ class S3UtilsTest(unittest.TestCase):
31
31
 
32
32
  def test_s3_list_object__random_text_files(self):
33
33
  with moto.mock_aws(), s3_make_client() as client:
34
- client.client.create_bucket(Bucket="dummy-bucket")
34
+ client.create_bucket(Bucket="dummy-bucket")
35
35
 
36
36
  data = []
37
37
  for i in range(0, 2000):
@@ -49,7 +49,7 @@ class S3UtilsTest(unittest.TestCase):
49
49
  self.assertEqual(s3_pull_text(client, "dummy-bucket", key), text)
50
50
 
51
51
  with moto.mock_aws(), s3_make_progressed_client() as client:
52
- client.client.create_bucket(Bucket="dummy-bucket")
52
+ client.create_bucket(Bucket="dummy-bucket")
53
53
 
54
54
  data = []
55
55
  for i in range(0, 2000):
@@ -58,17 +58,17 @@ class S3UtilsTest(unittest.TestCase):
58
58
  data.append((key, text))
59
59
 
60
60
  for key, text in data:
61
- client.CloudPath("s3://dummy-bucket/", key).write_text(text)
61
+ s3_push_text(client, text, "dummy-bucket", key)
62
62
 
63
63
  result = list(s3_list_objects(client, "dummy-bucket", "dummy_prefix/"))
64
64
 
65
65
  for key, text in data:
66
66
  self.assertTrue(any(o.key == key for o in result))
67
- self.assertEqual(client.CloudPath("s3://dummy-bucket", key).read_text(), text)
67
+ self.assertEqual(s3_pull_text(client, "dummy-bucket", key), text)
68
68
 
69
69
  def test_s3_listfile__random_text_files(self):
70
70
  with moto.mock_aws(), s3_make_client() as client:
71
- client.client.create_bucket(Bucket="dummy-bucket")
71
+ client.create_bucket(Bucket="dummy-bucket")
72
72
 
73
73
  data = []
74
74
  for i in range(0, 2000):
@@ -86,7 +86,7 @@ class S3UtilsTest(unittest.TestCase):
86
86
  self.assertEqual(s3_pull_text(client, "dummy-bucket", key), text)
87
87
 
88
88
  with moto.mock_aws(), s3_make_progressed_client() as client:
89
- client.client.create_bucket(Bucket="dummy-bucket")
89
+ client.create_bucket(Bucket="dummy-bucket")
90
90
 
91
91
  data = []
92
92
  for i in range(0, 2000):
@@ -95,13 +95,13 @@ class S3UtilsTest(unittest.TestCase):
95
95
  data.append((key, text))
96
96
 
97
97
  for key, text in data:
98
- client.CloudPath("s3://dummy-bucket/", key).write_text(text)
98
+ s3_push_text(client, text, "dummy-bucket", key)
99
99
 
100
100
  result = list(s3_listfile(client, "dummy-bucket", "dummy_prefix/"))
101
101
 
102
102
  for key, text in data:
103
103
  self.assertTrue(any(o.key == key for o in result))
104
- self.assertEqual(client.CloudPath("s3://dummy-bucket", key).read_text(), text)
104
+ self.assertEqual(s3_pull_text(client, "dummy-bucket", key), text)
105
105
 
106
106
  data_s3_listfile = [
107
107
  (
@@ -236,7 +236,7 @@ class S3UtilsTest(unittest.TestCase):
236
236
  @ddt.unpack
237
237
  def test_s3_listfile(self, src, bucket, prefix, include_patterns, exclude_patterns, depth, expect):
238
238
  with moto.mock_aws(), s3_make_client() as client:
239
- client.client.create_bucket(Bucket=bucket)
239
+ client.create_bucket(Bucket=bucket)
240
240
 
241
241
  s3_sync_upload(client,
242
242
  os.path.join(resources_directory, src),
@@ -394,7 +394,7 @@ class S3UtilsTest(unittest.TestCase):
394
394
  @ddt.unpack
395
395
  def test_s3_sync(self, src, dst, bucket, prefix, include_patterns, exclude_patterns, depth, expect):
396
396
  with moto.mock_aws(), s3_make_client() as client:
397
- client.client.create_bucket(Bucket=bucket)
397
+ client.create_bucket(Bucket=bucket)
398
398
 
399
399
  s3_sync_upload(client,
400
400
  os.path.join(resources_directory, src),
@@ -414,7 +414,7 @@ class S3UtilsTest(unittest.TestCase):
414
414
  set(map(lambda x: norm_path(os.path.join(temp_directory, x)), expect)))
415
415
 
416
416
  with moto.mock_aws(), s3_make_client() as client:
417
- client.client.create_bucket(Bucket=bucket)
417
+ client.create_bucket(Bucket=bucket)
418
418
 
419
419
  s3_sync_upload(client,
420
420
  os.path.join(resources_directory, src),
@@ -434,7 +434,7 @@ class S3UtilsTest(unittest.TestCase):
434
434
  set(map(lambda x: norm_path(os.path.join(temp_directory, x)), expect)))
435
435
 
436
436
  with moto.mock_aws(), s3_make_client() as client:
437
- client.client.create_bucket(Bucket=bucket)
437
+ client.create_bucket(Bucket=bucket)
438
438
 
439
439
  s3_sync_upload(client,
440
440
  os.path.join(resources_directory, src),
@@ -467,7 +467,7 @@ class S3UtilsTest(unittest.TestCase):
467
467
  @ddt.unpack
468
468
  def test_s3_text(self, bucket, key, text, encoding):
469
469
  with moto.mock_aws(), s3_make_client() as client:
470
- client.client.create_bucket(Bucket=bucket)
470
+ client.create_bucket(Bucket=bucket)
471
471
 
472
472
  s3_push_text(client, text, bucket, key, encoding=encoding)
473
473
  self.assertEqual(s3_pull_text(client, bucket, key, encoding=encoding), text)
@@ -478,7 +478,7 @@ class S3UtilsTest(unittest.TestCase):
478
478
  moto_server_endpoint_url() as endpoint_url,
479
479
  s3_make_client(endpoint_url=endpoint_url) as client,
480
480
  ):
481
- client.client.create_bucket(Bucket="dummy-bucket")
481
+ client.create_bucket(Bucket="dummy-bucket")
482
482
 
483
483
  s3_sync_upload(client,
484
484
  os.path.join(resources_directory, "unittest", "s3utils_archive"),
@@ -538,7 +538,7 @@ class S3UtilsTest(unittest.TestCase):
538
538
  moto_server_endpoint_url() as endpoint_url,
539
539
  s3_make_client(endpoint_url=endpoint_url) as client,
540
540
  ):
541
- client.client.create_bucket(Bucket="dummy-bucket")
541
+ client.create_bucket(Bucket="dummy-bucket")
542
542
 
543
543
  s3_sync_upload(client,
544
544
  os.path.join(resources_directory, "unittest", "s3utils_archive"),
@@ -565,7 +565,7 @@ class S3UtilsTest(unittest.TestCase):
565
565
  moto_server_endpoint_url() as endpoint_url,
566
566
  s3_make_client(endpoint_url=endpoint_url) as client,
567
567
  ):
568
- client.client.create_bucket(Bucket="dummy-bucket")
568
+ client.create_bucket(Bucket="dummy-bucket")
569
569
 
570
570
  s3_sync_upload(client,
571
571
  os.path.join(resources_directory, "unittest", "s3utils_archive"),