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.

@@ -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
- try:
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
- self.logger.debug(keys)
276
- self.logger.info("%d files from '%s' will be uploaded to '%s'", len(keys), self.upload_dir, self.bucket_name)
277
- self.logger.info("Initiating upload process.")
278
- for filepath, objectpath in tqdm(
279
- keys.items(), total=len(keys), unit="file", leave=True, desc=f"Uploading files from {self.upload_dir}"
280
- ):
281
- try:
282
- self._uploader(filepath=filepath, objectpath=objectpath)
283
- self.results.success += 1
284
- except ClientError as error:
285
- self.logger.error(error)
286
- self.results.failed += 1
287
- self.exit()
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
- self.logger.debug(keys)
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
- len(keys),
318
+ total_files,
301
319
  self.upload_dir,
302
320
  self.bucket_name,
303
321
  max_workers,
304
322
  )
305
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
306
- futures = [
307
- executor.submit(self._uploader, **dict(filepath=filepath, objectpath=objectpath))
308
- for filepath, objectpath in keys.items()
309
- ]
310
- for future in tqdm(
311
- iterable=as_completed(futures),
312
- total=len(futures),
313
- desc=f"Uploading files to {self.bucket_name}",
314
- unit="files",
315
- leave=True,
316
- ):
317
- try:
318
- future.result()
319
- self.results.success += 1
320
- except ClientError as error:
321
- self.logger.error(f"Upload failed: {error}")
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"
1
+ version = "0.3.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyS3Uploader
3
- Version: 0.3.1
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,,