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 +0 -2
- pys3uploader/progress.py +39 -0
- pys3uploader/uploader.py +76 -44
- pys3uploader/utils.py +31 -0
- pys3uploader/version.py +1 -0
- {pys3uploader-0.3.0.dist-info → pys3uploader-0.3.2.dist-info}/METADATA +2 -2
- pys3uploader-0.3.2.dist-info/RECORD +13 -0
- pys3uploader-0.3.0.dist-info/RECORD +0 -11
- {pys3uploader-0.3.0.dist-info → pys3uploader-0.3.2.dist-info}/LICENSE +0 -0
- {pys3uploader-0.3.0.dist-info → pys3uploader-0.3.2.dist-info}/WHEEL +0 -0
- {pys3uploader-0.3.0.dist-info → pys3uploader-0.3.2.dist-info}/top_level.txt +0 -0
pys3uploader/__init__.py
CHANGED
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,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
|
-
|
|
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(
|
|
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]",
|
|
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(
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
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
|
-
|
|
318
|
+
total_files,
|
|
286
319
|
self.upload_dir,
|
|
287
320
|
self.bucket_name,
|
|
288
321
|
max_workers,
|
|
289
322
|
)
|
|
290
|
-
with
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
for filepath, objectpath in keys.items()
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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]}"
|
pys3uploader/version.py
ADDED
|
@@ -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.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|