PyS3Uploader 0.3.1__py3-none-any.whl → 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of PyS3Uploader might be problematic. Click here for more details.
- pys3uploader/progress.py +39 -0
- pys3uploader/uploader.py +58 -41
- pys3uploader/version.py +1 -1
- {pys3uploader-0.3.1.dist-info → pys3uploader-0.3.2.dist-info}/METADATA +2 -2
- pys3uploader-0.3.2.dist-info/RECORD +13 -0
- pys3uploader-0.3.1.dist-info/RECORD +0 -12
- {pys3uploader-0.3.1.dist-info → pys3uploader-0.3.2.dist-info}/LICENSE +0 -0
- {pys3uploader-0.3.1.dist-info → pys3uploader-0.3.2.dist-info}/WHEEL +0 -0
- {pys3uploader-0.3.1.dist-info → pys3uploader-0.3.2.dist-info}/top_level.txt +0 -0
pys3uploader/progress.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
from alive_progress import alive_bar
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ProgressPercentage:
|
|
7
|
+
"""Tracks progress of a file upload to S3 and updates the alive_bar.
|
|
8
|
+
|
|
9
|
+
>>> ProgressPercentage
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, filename: str, size: int, bar: alive_bar):
|
|
14
|
+
"""Initializes the progress tracker.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
filename: Name of the file being uploaded.
|
|
18
|
+
size: Total size of the file in bytes.
|
|
19
|
+
bar: alive_bar instance to update progress.
|
|
20
|
+
"""
|
|
21
|
+
self._filename = filename
|
|
22
|
+
self._size = size
|
|
23
|
+
self._seen_so_far = 0
|
|
24
|
+
self._lock = threading.Lock()
|
|
25
|
+
self._bar = bar
|
|
26
|
+
|
|
27
|
+
def __call__(self, bytes_amount: int) -> None:
|
|
28
|
+
"""Callback method to update progress.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
bytes_amount: Number of bytes transferred in the last chunk.
|
|
32
|
+
"""
|
|
33
|
+
with self._lock:
|
|
34
|
+
self._seen_so_far += bytes_amount
|
|
35
|
+
percent = (self._seen_so_far / self._size) * 100
|
|
36
|
+
bar_len = 20
|
|
37
|
+
filled = int(bar_len * percent / 100)
|
|
38
|
+
bar_str = "█" * filled + "." * (bar_len - filled)
|
|
39
|
+
self._bar.text(f" || {self._filename} [{bar_str}] {percent:.0f}%")
|
pys3uploader/uploader.py
CHANGED
|
@@ -6,12 +6,13 @@ from typing import Dict, Iterable
|
|
|
6
6
|
|
|
7
7
|
import boto3.resources.factory
|
|
8
8
|
import dotenv
|
|
9
|
+
from alive_progress import alive_bar
|
|
9
10
|
from botocore.config import Config
|
|
10
11
|
from botocore.exceptions import ClientError
|
|
11
|
-
from tqdm import tqdm
|
|
12
12
|
|
|
13
13
|
from pys3uploader.exceptions import BucketNotFound
|
|
14
14
|
from pys3uploader.logger import LogHandler, LogLevel, setup_logger
|
|
15
|
+
from pys3uploader.progress import ProgressPercentage
|
|
15
16
|
from pys3uploader.utils import (
|
|
16
17
|
RETRY_CONFIG,
|
|
17
18
|
UploadResults,
|
|
@@ -176,6 +177,22 @@ class Uploader:
|
|
|
176
177
|
)
|
|
177
178
|
self.logger.info("Run time: %s", convert_seconds(time.time() - self.start))
|
|
178
179
|
|
|
180
|
+
def filesize(self, filepath: str) -> int:
|
|
181
|
+
"""Gets the file size of a given filepath.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
filepath: Full path of the file.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
int:
|
|
188
|
+
Returns the file size in bytes.
|
|
189
|
+
"""
|
|
190
|
+
try:
|
|
191
|
+
return os.path.getsize(filepath)
|
|
192
|
+
except (OSError, PermissionError) as error:
|
|
193
|
+
self.logger.error(error)
|
|
194
|
+
return 0
|
|
195
|
+
|
|
179
196
|
def _proceed_to_upload(self, filepath: str, objectpath: str) -> bool:
|
|
180
197
|
"""Compares file size if the object already exists in S3.
|
|
181
198
|
|
|
@@ -189,11 +206,7 @@ class Uploader:
|
|
|
189
206
|
"""
|
|
190
207
|
if self.overwrite:
|
|
191
208
|
return True
|
|
192
|
-
|
|
193
|
-
file_size = os.path.getsize(filepath)
|
|
194
|
-
except (OSError, PermissionError) as error:
|
|
195
|
-
self.logger.error(error)
|
|
196
|
-
file_size = 0
|
|
209
|
+
file_size = self.filesize(filepath)
|
|
197
210
|
# Indicates that the object path already exists in S3
|
|
198
211
|
if object_size := self.object_size_map.get(objectpath):
|
|
199
212
|
if object_size == file_size:
|
|
@@ -220,7 +233,7 @@ class Uploader:
|
|
|
220
233
|
)
|
|
221
234
|
return True
|
|
222
235
|
|
|
223
|
-
def _uploader(self, filepath: str, objectpath: str) -> None:
|
|
236
|
+
def _uploader(self, filepath: str, objectpath: str, callback: ProgressPercentage) -> None:
|
|
224
237
|
"""Uploads the filepath to the specified S3 bucket.
|
|
225
238
|
|
|
226
239
|
Args:
|
|
@@ -228,7 +241,7 @@ class Uploader:
|
|
|
228
241
|
objectpath: Object path ref in S3.
|
|
229
242
|
"""
|
|
230
243
|
if self._proceed_to_upload(filepath, objectpath):
|
|
231
|
-
self.bucket.upload_file(filepath, objectpath)
|
|
244
|
+
self.bucket.upload_file(filepath, objectpath, Callback=callback)
|
|
232
245
|
|
|
233
246
|
def _get_files(self) -> Dict[str, str]:
|
|
234
247
|
"""Get a mapping for all the file path and object paths in upload directory.
|
|
@@ -272,19 +285,23 @@ class Uploader:
|
|
|
272
285
|
"""Initiates object upload in a traditional loop."""
|
|
273
286
|
self.init()
|
|
274
287
|
keys = self._get_files()
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
+
total_files = len(keys)
|
|
289
|
+
|
|
290
|
+
with alive_bar(total_files, title="Progress", bar="smooth", spinner="dots") as overall_bar:
|
|
291
|
+
for filepath, objectpath in keys.items():
|
|
292
|
+
progress_callback = ProgressPercentage(
|
|
293
|
+
filename=os.path.basename(filepath), size=self.filesize(filepath), bar=overall_bar
|
|
294
|
+
)
|
|
295
|
+
try:
|
|
296
|
+
self._uploader(filepath, objectpath, progress_callback)
|
|
297
|
+
self.results.success += 1
|
|
298
|
+
except ClientError as error:
|
|
299
|
+
self.logger.error(error)
|
|
300
|
+
self.results.failed += 1
|
|
301
|
+
except KeyboardInterrupt:
|
|
302
|
+
self.logger.warning("Upload interrupted by user")
|
|
303
|
+
break
|
|
304
|
+
overall_bar() # increment overall progress bar
|
|
288
305
|
|
|
289
306
|
def run_in_parallel(self, max_workers: int = 5) -> None:
|
|
290
307
|
"""Initiates upload in multi-threading.
|
|
@@ -294,32 +311,32 @@ class Uploader:
|
|
|
294
311
|
"""
|
|
295
312
|
self.init()
|
|
296
313
|
keys = self._get_files()
|
|
297
|
-
|
|
314
|
+
total_files = len(keys)
|
|
315
|
+
|
|
298
316
|
self.logger.info(
|
|
299
317
|
"%d files from '%s' will be uploaded to '%s' with maximum concurrency of: %d",
|
|
300
|
-
|
|
318
|
+
total_files,
|
|
301
319
|
self.upload_dir,
|
|
302
320
|
self.bucket_name,
|
|
303
321
|
max_workers,
|
|
304
322
|
)
|
|
305
|
-
with
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
for filepath, objectpath in keys.items()
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
self.results.failed += 1
|
|
323
|
+
with alive_bar(total_files, title="Progress", bar="smooth", spinner="dots") as overall_bar:
|
|
324
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
325
|
+
futures = []
|
|
326
|
+
for filepath, objectpath in keys.items():
|
|
327
|
+
progress_callback = ProgressPercentage(
|
|
328
|
+
filename=os.path.basename(filepath), size=self.filesize(filepath), bar=overall_bar
|
|
329
|
+
)
|
|
330
|
+
futures.append(executor.submit(self._uploader, filepath, objectpath, callback=progress_callback))
|
|
331
|
+
|
|
332
|
+
for future in as_completed(futures):
|
|
333
|
+
try:
|
|
334
|
+
future.result()
|
|
335
|
+
self.results.success += 1
|
|
336
|
+
except ClientError as error:
|
|
337
|
+
self.logger.error(f"Upload failed: {error}")
|
|
338
|
+
self.results.failed += 1
|
|
339
|
+
overall_bar() # Increment overall bar after each upload finishes
|
|
323
340
|
self.exit()
|
|
324
341
|
|
|
325
342
|
def get_bucket_structure(self) -> str:
|
pys3uploader/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
version = "0.3.
|
|
1
|
+
version = "0.3.2"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.2
|
|
2
2
|
Name: PyS3Uploader
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Python module to upload objects to an S3 bucket.
|
|
5
5
|
Author-email: Vignesh Rao <svignesh1793@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -39,9 +39,9 @@ Classifier: Topic :: Internet :: File Transfer Protocol (FTP)
|
|
|
39
39
|
Requires-Python: >=3.11
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
License-File: LICENSE
|
|
42
|
+
Requires-Dist: alive-progress==3.3.*
|
|
42
43
|
Requires-Dist: boto3==1.40.*
|
|
43
44
|
Requires-Dist: python-dotenv==1.1.*
|
|
44
|
-
Requires-Dist: tqdm==4.67.*
|
|
45
45
|
Provides-Extra: dev
|
|
46
46
|
Requires-Dist: sphinx==5.1.1; extra == "dev"
|
|
47
47
|
Requires-Dist: pre-commit; extra == "dev"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pys3uploader/__init__.py,sha256=EqMScWbJNV4UWeMg4fMko2KB18xL2CO3a3o_od0H0Lc,124
|
|
2
|
+
pys3uploader/exceptions.py,sha256=hH3jlMOe8yjBatQK9EdndWZz4QESU74KSY_iDhQ37SY,2585
|
|
3
|
+
pys3uploader/logger.py,sha256=igwMubdTQ_GrMkwie5DAIvmxIcgj6a9UA_EGFrwFYiQ,2571
|
|
4
|
+
pys3uploader/progress.py,sha256=IladNMXLBhkPpxOntpANTam_hC9OWosmNDmdbweDNYM,1195
|
|
5
|
+
pys3uploader/tree.py,sha256=DiQ2ekMMaj2m_P3-iKkEqSuJCJZ_UZxcAwHtAoPVa5c,1824
|
|
6
|
+
pys3uploader/uploader.py,sha256=1X0MCWwzm2O2tY9R_tVZl-CC8qnoGt32XlyQ0LIROYw,15460
|
|
7
|
+
pys3uploader/utils.py,sha256=T-TIUtzQ6sYEfZqwxH_t-Cfhw5wuzr_REQ-OBkdcg00,5357
|
|
8
|
+
pys3uploader/version.py,sha256=zjuPzQ2OlxNtrLXb5A3OlPsaRb_b_Ln51AWN6r9VRho,18
|
|
9
|
+
pys3uploader-0.3.2.dist-info/LICENSE,sha256=8k-hEraOzyum0GvmmK65YxNRTFXK7eIFHJ0OshJXeTk,1068
|
|
10
|
+
pys3uploader-0.3.2.dist-info/METADATA,sha256=RmwnvqWSdcLYLbWz2WXfzilw6FOpSG8ydfOtii4t16Q,8048
|
|
11
|
+
pys3uploader-0.3.2.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
12
|
+
pys3uploader-0.3.2.dist-info/top_level.txt,sha256=lVIFMMoUx7dj_myetBmOUQTJiOzz5VyDqchnQElmrWw,13
|
|
13
|
+
pys3uploader-0.3.2.dist-info/RECORD,,
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
pys3uploader/__init__.py,sha256=EqMScWbJNV4UWeMg4fMko2KB18xL2CO3a3o_od0H0Lc,124
|
|
2
|
-
pys3uploader/exceptions.py,sha256=hH3jlMOe8yjBatQK9EdndWZz4QESU74KSY_iDhQ37SY,2585
|
|
3
|
-
pys3uploader/logger.py,sha256=igwMubdTQ_GrMkwie5DAIvmxIcgj6a9UA_EGFrwFYiQ,2571
|
|
4
|
-
pys3uploader/tree.py,sha256=DiQ2ekMMaj2m_P3-iKkEqSuJCJZ_UZxcAwHtAoPVa5c,1824
|
|
5
|
-
pys3uploader/uploader.py,sha256=FW2Mg1yoMV6Qaq9F2n-EXnP1vqi3AGtWUuIWVMev65o,14644
|
|
6
|
-
pys3uploader/utils.py,sha256=T-TIUtzQ6sYEfZqwxH_t-Cfhw5wuzr_REQ-OBkdcg00,5357
|
|
7
|
-
pys3uploader/version.py,sha256=sEAhGxRzEBE5t0VjAcJ-336II62pGIQ0eLrs42I-sGU,18
|
|
8
|
-
pys3uploader-0.3.1.dist-info/LICENSE,sha256=8k-hEraOzyum0GvmmK65YxNRTFXK7eIFHJ0OshJXeTk,1068
|
|
9
|
-
pys3uploader-0.3.1.dist-info/METADATA,sha256=-moIOf7765V6qYgXcOpJtawcnlriAlUOQ2a-abgGt5o,8039
|
|
10
|
-
pys3uploader-0.3.1.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
|
|
11
|
-
pys3uploader-0.3.1.dist-info/top_level.txt,sha256=lVIFMMoUx7dj_myetBmOUQTJiOzz5VyDqchnQElmrWw,13
|
|
12
|
-
pys3uploader-0.3.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|