PyS3Uploader 0.3.0__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/__init__.py CHANGED
@@ -1,4 +1,2 @@
1
1
  from pys3uploader.logger import LogHandler, LogLevel # noqa: F401
2
2
  from pys3uploader.uploader import Uploader # noqa: F401
3
-
4
- version = "0.3.0"
@@ -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,18 +6,20 @@ 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,
18
19
  convert_seconds,
19
20
  convert_to_folder_structure,
20
21
  getenv,
22
+ size_converter,
21
23
  urljoin,
22
24
  )
23
25
 
@@ -175,6 +177,22 @@ class Uploader:
175
177
  )
176
178
  self.logger.info("Run time: %s", convert_seconds(time.time() - self.start))
177
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
+
178
196
  def _proceed_to_upload(self, filepath: str, objectpath: str) -> bool:
179
197
  """Compares file size if the object already exists in S3.
180
198
 
@@ -188,24 +206,34 @@ class Uploader:
188
206
  """
189
207
  if self.overwrite:
190
208
  return True
191
- try:
192
- file_size = os.path.getsize(filepath)
193
- except (OSError, PermissionError) as error:
194
- self.logger.error(error)
195
- file_size = 0
209
+ file_size = self.filesize(filepath)
196
210
  # Indicates that the object path already exists in S3
197
211
  if object_size := self.object_size_map.get(objectpath):
198
212
  if object_size == file_size:
199
- self.logger.info("S3 object %s exists, and size [%d] matches, skipping..", objectpath, object_size)
213
+ self.logger.info(
214
+ "S3 object %s exists, and size [%d bytes / %s] matches, skipping..",
215
+ objectpath,
216
+ object_size,
217
+ size_converter(object_size),
218
+ )
200
219
  return False
201
220
  self.logger.info(
202
- "S3 object %s exists, but size mismatch. Local: [%d], S3: [%d]", objectpath, file_size, object_size
221
+ "S3 object %s exists, but size mismatch. Local: [%d bytes / %s], S3: [%d bytes / %s]",
222
+ objectpath,
223
+ file_size,
224
+ object_size,
225
+ size_converter(object_size),
203
226
  )
204
227
  else:
205
- self.logger.debug("S3 object '%s' of size [%d bytes] doesn't exist, uploading..", objectpath, file_size)
228
+ self.logger.debug(
229
+ "S3 object '%s' of size [%d bytes / %s] doesn't exist, uploading..",
230
+ objectpath,
231
+ file_size,
232
+ size_converter(file_size),
233
+ )
206
234
  return True
207
235
 
208
- def _uploader(self, filepath: str, objectpath: str) -> None:
236
+ def _uploader(self, filepath: str, objectpath: str, callback: ProgressPercentage) -> None:
209
237
  """Uploads the filepath to the specified S3 bucket.
210
238
 
211
239
  Args:
@@ -213,7 +241,7 @@ class Uploader:
213
241
  objectpath: Object path ref in S3.
214
242
  """
215
243
  if self._proceed_to_upload(filepath, objectpath):
216
- self.bucket.upload_file(filepath, objectpath)
244
+ self.bucket.upload_file(filepath, objectpath, Callback=callback)
217
245
 
218
246
  def _get_files(self) -> Dict[str, str]:
219
247
  """Get a mapping for all the file path and object paths in upload directory.
@@ -257,19 +285,23 @@ class Uploader:
257
285
  """Initiates object upload in a traditional loop."""
258
286
  self.init()
259
287
  keys = self._get_files()
260
- self.logger.debug(keys)
261
- self.logger.info("%d files from '%s' will be uploaded to '%s'", len(keys), self.upload_dir, self.bucket_name)
262
- self.logger.info("Initiating upload process.")
263
- for filepath, objectpath in tqdm(
264
- keys.items(), total=len(keys), unit="file", leave=True, desc=f"Uploading files from {self.upload_dir}"
265
- ):
266
- try:
267
- self._uploader(filepath=filepath, objectpath=objectpath)
268
- self.results.success += 1
269
- except ClientError as error:
270
- self.logger.error(error)
271
- self.results.failed += 1
272
- 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
273
305
 
274
306
  def run_in_parallel(self, max_workers: int = 5) -> None:
275
307
  """Initiates upload in multi-threading.
@@ -279,32 +311,32 @@ class Uploader:
279
311
  """
280
312
  self.init()
281
313
  keys = self._get_files()
282
- self.logger.debug(keys)
314
+ total_files = len(keys)
315
+
283
316
  self.logger.info(
284
317
  "%d files from '%s' will be uploaded to '%s' with maximum concurrency of: %d",
285
- len(keys),
318
+ total_files,
286
319
  self.upload_dir,
287
320
  self.bucket_name,
288
321
  max_workers,
289
322
  )
290
- with ThreadPoolExecutor(max_workers=max_workers) as executor:
291
- futures = [
292
- executor.submit(self._uploader, **dict(filepath=filepath, objectpath=objectpath))
293
- for filepath, objectpath in keys.items()
294
- ]
295
- for future in tqdm(
296
- iterable=as_completed(futures),
297
- total=len(futures),
298
- desc=f"Uploading files to {self.bucket_name}",
299
- unit="files",
300
- leave=True,
301
- ):
302
- try:
303
- future.result()
304
- self.results.success += 1
305
- except ClientError as error:
306
- self.logger.error(f"Upload failed: {error}")
307
- 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
308
340
  self.exit()
309
341
 
310
342
  def get_bucket_structure(self) -> str:
pys3uploader/utils.py CHANGED
@@ -1,3 +1,4 @@
1
+ import math
1
2
  import os
2
3
  from typing import Dict, Set
3
4
 
@@ -145,3 +146,33 @@ def convert_seconds(seconds: int | float, n_elem: int = 2) -> str:
145
146
 
146
147
  list_ = time_parts[:n_elem]
147
148
  return ", and ".join([", ".join(list_[:-1]), list_[-1]] if len(list_) > 2 else list_)
149
+
150
+
151
+ def format_nos(input_: float) -> int | float:
152
+ """Removes ``.0`` float values.
153
+
154
+ Args:
155
+ input_: Strings or integers with ``.0`` at the end.
156
+
157
+ Returns:
158
+ int | float:
159
+ Int if found, else returns the received float value.
160
+ """
161
+ return int(input_) if isinstance(input_, float) and input_.is_integer() else input_
162
+
163
+
164
+ def size_converter(byte_size: int | float) -> str:
165
+ """Gets the current memory consumed and converts it to human friendly format.
166
+
167
+ Args:
168
+ byte_size: Receives byte size as argument.
169
+
170
+ Returns:
171
+ str:
172
+ Converted understandable size.
173
+ """
174
+ if not byte_size:
175
+ return "0 B"
176
+ size_name = ("B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
177
+ index = int(math.floor(math.log(byte_size, 1024)))
178
+ return f"{format_nos(round(byte_size / pow(1024, index), 2))} {size_name[index]}"
@@ -0,0 +1 @@
1
+ version = "0.3.2"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: PyS3Uploader
3
- Version: 0.3.0
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,11 +0,0 @@
1
- pys3uploader/__init__.py,sha256=QszR24KVfcJXbrJI0oxZCIaMs5GOtwnIbtfrY2Zj7Nk,143
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=-PRwu2RvzExjLTKbw9oQuFcyNI0Uxi1IsoJu6e6wayY,14258
6
- pys3uploader/utils.py,sha256=NbF28CYviK_St5qd1EOumMVyus9BvQON7clUFeR_SEQ,4473
7
- pys3uploader-0.3.0.dist-info/LICENSE,sha256=8k-hEraOzyum0GvmmK65YxNRTFXK7eIFHJ0OshJXeTk,1068
8
- pys3uploader-0.3.0.dist-info/METADATA,sha256=0eS5eoAsatnmMi_wanq_kzHTZGVTvwo2c4pR_CIa3T4,8039
9
- pys3uploader-0.3.0.dist-info/WHEEL,sha256=beeZ86-EfXScwlR_HKu4SllMC9wUEj_8Z_4FJ3egI2w,91
10
- pys3uploader-0.3.0.dist-info/top_level.txt,sha256=lVIFMMoUx7dj_myetBmOUQTJiOzz5VyDqchnQElmrWw,13
11
- pys3uploader-0.3.0.dist-info/RECORD,,