pyaws-s3 1.0.3__py3-none-any.whl → 1.0.4__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.
pyaws_s3/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from .s3 import S3Client
pyaws_s3/s3.py ADDED
@@ -0,0 +1,428 @@
1
+ import logging
2
+ import os
3
+ import boto3
4
+ import aioboto3
5
+ import pandas as pd
6
+ import io
7
+ import matplotlib.pyplot as plt
8
+ from pandas.plotting import table
9
+ from typing import Any, Literal
10
+ from util import bytes_from_figure, html_from_figure
11
+ from fpdf import FPDF
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+ FormatFile = Literal["png", "jpeg", "svg", "html", "xlsx", "csv", "pdf"]
16
+
17
+ class S3Client:
18
+ """
19
+ A class to interact with AWS S3 for uploading and managing files.
20
+ """
21
+
22
+ aws_access_key_id : str
23
+ aws_secret_access_key : str
24
+ region_name : str
25
+ bucket_name : str
26
+
27
+ def __init__(self, **kwargs: Any) -> None:
28
+ """
29
+ Initialize the S3Client with AWS credentials and region.
30
+
31
+ Args:
32
+ aws_access_key_id (str): AWS access key ID.
33
+ aws_secret_access_key (str): AWS secret access key.
34
+ region_name (str): AWS region name.
35
+ """
36
+ # aws_access_key_id: str, aws_secret_access_key: str, region_name: str
37
+ self.aws_access_key_id = kwargs.get("aws_access_key_id", os.getenv("AWS_ACCESS_KEY_ID"))
38
+ self.aws_secret_access_key = kwargs.get("aws_secret_access_key", os.getenv("AWS_SECRET_ACCESS_KEY"))
39
+ self.region_name = kwargs.get("region_name", os.getenv("AWS_REGION"))
40
+ self.bucket_name = kwargs.get("bucket_name", os.getenv("AWS_BUCKET_NAME"))
41
+
42
+ async def _get_s3_client_async(self) -> Any:
43
+ """
44
+ Get an asynchronous S3 client using the provided AWS credentials and region.
45
+
46
+ Args:
47
+ aws_access_key_id (str): AWS access key ID.
48
+ aws_secret_access_key (str): AWS secret access key.
49
+ region_name (str): AWS region name.
50
+
51
+ Returns:
52
+ Any: Asynchronous S3 client object.
53
+ """
54
+ session = aioboto3.Session()
55
+ return session.resource(
56
+ 's3',
57
+ aws_access_key_id=self.aws_access_key_id,
58
+ aws_secret_access_key=self.aws_secret_access_key,
59
+ region_name=self.region_name
60
+ )
61
+
62
+ def _get_s3_client(self) -> Any:
63
+ """
64
+ Get an asynchronous S3 client using the provided AWS credentials and region.
65
+
66
+ Args:
67
+ aws_access_key_id (str): AWS access key ID.
68
+ aws_secret_access_key (str): AWS secret access key.
69
+ region_name (str): AWS region name.
70
+
71
+ Returns:
72
+ Any: Asynchronous S3 client object.
73
+ """
74
+ return boto3.client(
75
+ 's3',
76
+ aws_access_key_id=self.aws_access_key_id,
77
+ aws_secret_access_key=self.aws_secret_access_key,
78
+ region_name=self.region_name
79
+ )
80
+
81
+ def _get_s3_resource(self) -> Any:
82
+ """
83
+ Get an S3 client using the provided AWS credentials and region.
84
+
85
+ Args:
86
+ aws_access_key_id (str): AWS access key ID.
87
+ aws_secret_access_key (str): AWS secret access key.
88
+ region_name (str): AWS region name.
89
+
90
+ Returns:
91
+ Any: S3 client object.
92
+
93
+ Raises:
94
+ Exception: If there is an error creating the S3 client.
95
+ """
96
+ try:
97
+
98
+ return boto3.resource('s3',
99
+ aws_access_key_id=self.aws_access_key_id,
100
+ aws_secret_access_key=self.aws_secret_access_key,
101
+ region_name=self.region_name)
102
+ except Exception as e:
103
+ logger.error(f"Error getting S3 client: {str(e)}")
104
+ raise Exception(f"Error getting S3 client: {str(e)}")
105
+
106
+ def _create_url(self, s3_client, bucket_name: str, object_name: str) -> str:
107
+ """
108
+ Generate a pre-signed URL for an S3 object.
109
+
110
+ Args:
111
+ s3_client: The S3 client object.
112
+ bucket_name (str): The name of the S3 bucket.
113
+ object_name (str): The name of the S3 object.
114
+
115
+ Returns:
116
+ str: The pre-signed URL for the S3 object.
117
+ """
118
+ temp_url = s3_client.generate_presigned_url(
119
+ 'get_object',
120
+ Params={
121
+ 'Bucket': bucket_name,
122
+ 'Key': object_name
123
+ },
124
+ ExpiresIn=900 # 15 minutes
125
+ )
126
+
127
+ logger.info(f"Pre-signed URL: {temp_url}")
128
+
129
+ return temp_url
130
+
131
+ def upload_image(self, *args, **kwargs: Any) -> str:
132
+ """
133
+ Upload a Plotly Figure as a PNG image to an S3 bucket and generate a pre-signed URL.
134
+
135
+ Args:
136
+ fig (Figure): The Plotly Figure object to upload.
137
+ bucket_name (str): The name of the S3 bucket.
138
+ object_name (str): The name of the S3 object.
139
+
140
+ Keyword Args:
141
+ format_file (str): Format of the image. Defaults to 'png'.
142
+
143
+ Returns:
144
+ str: Pre-signed URL for the uploaded image.
145
+
146
+ Raises:
147
+ Exception: If there is an error uploading the image.
148
+ """
149
+ try:
150
+
151
+ fig = args[0] if len(args) > 0 else None
152
+ if fig is None:
153
+ raise Exception("Figure is None")
154
+
155
+ object_name = args[1] if len(args) > 1 else None
156
+ if object_name is None:
157
+ raise Exception("Object name is None")
158
+
159
+ format_file : FormatFile = kwargs.get("format_file", "svg")
160
+ mimetypes = "image/svg+xml"
161
+
162
+ if format_file not in ["png", "jpeg", "svg", "html"]:
163
+ raise Exception("Invalid format_file provided. Supported formats are: png, jpeg, svg, html")
164
+ if format_file == "png":
165
+ mimetypes = "image/png"
166
+ elif format_file == "jpeg":
167
+ mimetypes = "image/jpeg"
168
+ elif format_file == "svg":
169
+ mimetypes = "image/svg+xml"
170
+ elif format_file == "html":
171
+ mimetypes = "text/html"
172
+ else:
173
+ raise Exception("Invalid MIME type provided")
174
+
175
+ # Get S3 client and resource
176
+ s3_client = self._get_s3_client()
177
+ s3_resource = self._get_s3_resource()
178
+
179
+ if format_file == "html":
180
+ # Convert the figure to SVG
181
+ file_text = html_from_figure(fig)
182
+ # Upload the html text to s3
183
+ s3_resource.Bucket(self.bucket_name).Object(object_name).put(Body=file_text, ContentType=mimetypes)
184
+ else:
185
+ # Convert the figure to bytes
186
+ file_buffer = bytes_from_figure(fig, format_file=format_file)
187
+ # Upload the image bytes to S3
188
+ s3_resource.Bucket(self.bucket_name).Object(object_name).put(Body=file_buffer, ContentType=mimetypes)
189
+
190
+ # Generate and return a pre-signed URL for the uploaded image
191
+ return self._create_url(s3_client, self.bucket_name, object_name)
192
+
193
+ except Exception as e:
194
+ logger.error(f"Error uploading image: {str(e)}")
195
+ raise Exception(f"Error uploading image: {str(e)}")
196
+
197
+ def upload_from_dataframe(self, *args : Any, **kwargs: Any) -> str:
198
+ """
199
+ Upload a DataFrame as an Excel file to an S3 bucket and generate a pre-signed URL.
200
+
201
+ Args:
202
+ df (DataFrame): The DataFrame to upload.
203
+ **kwargs (Any): Additional keyword arguments for AWS credentials, bucket name, and object name.
204
+ Keyword Args:
205
+ format_file (str): Format of the file. Defaults to 'xlsx'.
206
+
207
+ Returns:
208
+ str: Pre-signed URL for the uploaded file.
209
+
210
+ Raises:
211
+ Exception: If there is an error uploading the file.
212
+ """
213
+ try:
214
+
215
+ df = args[0] if len(args) > 0 else None
216
+ if df is None:
217
+ raise Exception("Figure is None")
218
+
219
+ object_name = args[1] if len(args) > 1 else None
220
+ if object_name is None:
221
+ raise Exception("Object name is None")
222
+
223
+ format_file : FormatFile = kwargs.get("format_file", "csv")
224
+
225
+ if format_file not in ["xlsx", "csv", "pdf"]:
226
+ raise Exception("Invalid format_file provided. Supported formats are: xlsx, csv, pdf")
227
+
228
+ if format_file == "xlsx":
229
+ mimetypes = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
230
+ elif format_file == "csv":
231
+ mimetypes = "text/csv"
232
+ elif format_file == "pdf":
233
+ mimetypes = "application/pdf"
234
+ else:
235
+ raise Exception("Invalid MIME type provided")
236
+
237
+ s3_client = self._get_s3_client()
238
+ s3_resource = self._get_s3_resource()
239
+
240
+ # Create a file buffer
241
+ ext: str = ""
242
+ with io.BytesIO() as file_buffer:
243
+ if mimetypes == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
244
+ ext = "xlsx"
245
+ # Convert DataFrame to Excel
246
+ with pd.ExcelWriter(file_buffer, engine="openpyxl") as writer:
247
+ df.to_excel(writer, index=False)
248
+ elif mimetypes == "text/csv":
249
+ ext = "csv"
250
+ # Convert DataFrame to CSV
251
+ df.to_csv(file_buffer, index=False)
252
+ elif mimetypes == "application/pdf":
253
+ ext = "pdf"
254
+ # Convert DataFrame to PDF
255
+ fig, ax = plt.subplots(figsize=(12, 4)) # Set the size of the figure
256
+ ax.axis('tight')
257
+ ax.axis('off')
258
+ table(ax, df, loc='center', cellLoc='center', colWidths=[0.1] * len(df.columns))
259
+ plt.savefig(file_buffer, format='pdf')
260
+
261
+ file_buffer.seek(0)
262
+ # Append the file extension to the object name
263
+ object_name = f"{object_name}.{ext}"
264
+ # Upload the file to S3
265
+ s3_resource.Bucket(self.bucket_name).Object(object_name).put(Body=file_buffer, ContentType=mimetypes)
266
+
267
+ logger.info(f"Uploaded file to S3: {object_name}")
268
+
269
+ return self._create_url(s3_client, self.bucket_name, object_name)
270
+ except Exception as e:
271
+ logger.error(f"Error uploading file: {str(e)}")
272
+ raise Exception(f"Error uploading file: {str(e)}")
273
+
274
+ async def delete_all(self, filter : str | None = None) -> None:
275
+ """
276
+ Delete all files from an S3 bucket.
277
+
278
+ Args:
279
+ filter (str | None): Optional filter to delete specific files. If None, all files will be deleted.
280
+ Raises:
281
+ Exception: If there is an error deleting the files.
282
+ """
283
+ try:
284
+ s3_client = self._get_s3_client()
285
+
286
+ # List all objects in the bucket
287
+ objects = s3_client.list_objects_v2(Bucket=self.bucket_name)
288
+
289
+ # Check if the bucket contains any objects
290
+ if 'Contents' in objects:
291
+ for obj in objects['Contents']:
292
+ if filter in obj['Key']:
293
+ # Delete each object
294
+ s3_client.delete_object(Bucket=self.bucket_name, Key=obj['Key'])
295
+ print(f"Deleted {obj['Key']}")
296
+ except Exception as e:
297
+ logger.error(f"Error deleting files: {str(e)}")
298
+ raise Exception(f"Error deleting files: {str(e)}")
299
+
300
+ def upload_to_pdf(self, *args : Any) -> str:
301
+ """
302
+ Export the given text as a PDF and upload it to the S3 bucket.
303
+
304
+ Args:
305
+ text (str): The text to write in the PDF.
306
+ object_name (str): The name of the S3 object.
307
+ Raises:
308
+ Exception: If there is an error exporting the PDF.
309
+ Returns:
310
+ str: Pre-signed URL for the uploaded PDF.
311
+ """
312
+ try:
313
+ text = args[0] if len(args) > 0 else None
314
+ if text is None:
315
+ raise Exception("Text is None")
316
+
317
+ object_name = args[1] if len(args) > 1 else None
318
+ if object_name is None:
319
+ raise Exception("Object name is None")
320
+
321
+ mimetypes = "application/pdf"
322
+ s3_client = self._get_s3_client()
323
+ s3_resource = self._get_s3_resource()
324
+
325
+ pdf = FPDF()
326
+ pdf.add_page()
327
+ pdf.set_auto_page_break(auto=True, margin=15)
328
+ pdf.set_font("Arial", size=12)
329
+ for line in text.splitlines():
330
+ pdf.multi_cell(0, 10, line)
331
+
332
+ # Write PDF to a bytes buffer
333
+ with io.BytesIO() as pdf_buffer:
334
+ pdf_bytes = pdf.output(dest='S')
335
+ pdf_buffer.write(pdf_bytes)
336
+ pdf_buffer.seek(0)
337
+
338
+ s3_resource.Bucket(self.bucket_name).Object(object_name).put(
339
+ Body=pdf_buffer,
340
+ ContentType=mimetypes
341
+ )
342
+ return self._create_url(s3_client, self.bucket_name, object_name)
343
+ except Exception as e:
344
+ logger.error(f"Error exporting PDF: {str(e)}")
345
+ raise Exception(f"Error exporting PDF: {str(e)}")
346
+
347
+ def download(self, *args : Any, **kwargs : Any):
348
+ """
349
+ Download a file from S3 bucket.
350
+
351
+ Args:
352
+ object_name (str): The name of the S3 object to download.
353
+ **kwargs (Any): Additional keyword arguments for local path and stream.
354
+ - local_path (str): Local path to save the downloaded file. If None, the file will be streamed.
355
+ - stream (bool): If True, the file will be streamed instead of saved locally.
356
+ Raises:
357
+ Exception: If there is an error downloading the file.
358
+ Returns:
359
+ str: The local path of the downloaded file.
360
+ """
361
+ try:
362
+ object_name = args[0] if len(args) > 0 else None
363
+ if object_name is None:
364
+ raise Exception("Object name is None")
365
+
366
+ local_path = kwargs.get("local_path", None)
367
+ stream = kwargs.get("stream", False)
368
+
369
+ if not stream and local_path is None:
370
+ raise Exception("Local path is None if stream is False")
371
+
372
+ s3_client = self._get_s3_client()
373
+ response = s3_client.download_file(self.bucket_name, object_name, local_path)
374
+
375
+ if stream:
376
+ return response['Body'].read()
377
+
378
+ return local_path
379
+ except Exception as e:
380
+ logger.error(f"Error downloading file: {str(e)}")
381
+ raise Exception(f"Error downloading file: {str(e)}")
382
+
383
+ def list_files(self, *args : Any) -> list[str]:
384
+ """
385
+ List all files in the S3 bucket.
386
+
387
+ Args:
388
+ filter (str | None): Optional filter to list specific files. If None, all files will be listed.
389
+ Raises:
390
+ Exception: If there is an error listing the files.
391
+ Returns:
392
+ list[str]: List of file names in the S3 bucket.
393
+ """
394
+ try:
395
+ filter = args[0] if len(args) > 0 else None
396
+ s3_client = self._get_s3_client()
397
+ objects = s3_client.list_objects_v2(Bucket=self.bucket_name)
398
+
399
+ # Check if the bucket contains any objects
400
+ if 'Contents' in objects:
401
+ return [obj['Key'] for obj in objects['Contents'] if filter in obj['Key']]
402
+ else:
403
+ return []
404
+ except Exception as e:
405
+ logger.error(f"Error listing files: {str(e)}")
406
+ raise Exception(f"Error listing files: {str(e)}")
407
+
408
+ def delete_file(self, *args : Any) -> None:
409
+ """
410
+ Delete a file from the S3 bucket.
411
+
412
+ Args:
413
+ object_name (str): The name of the S3 object to delete.
414
+ Raises:
415
+ Exception: If there is an error deleting the file.
416
+ """
417
+ try:
418
+ object_name = args[0] if len(args) > 0 else None
419
+ if object_name is None:
420
+ raise Exception("Object name is None")
421
+
422
+ s3_client = self._get_s3_client()
423
+ s3_client.delete_object(Bucket=self.bucket_name, Key=object_name)
424
+ except Exception as e:
425
+ logger.error(f"Error deleting file: {str(e)}")
426
+ raise Exception(f"Error deleting file: {str(e)}")
427
+
428
+
pyaws_s3/util.py ADDED
@@ -0,0 +1,47 @@
1
+ import io
2
+ import logging
3
+ from plotly.graph_objs import Figure
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ def bytes_from_figure(f: Figure, **kwargs) -> bytes:
8
+ """
9
+ Convert a Plotly Figure to a PNG image as bytes.
10
+
11
+ Args:
12
+ f (Figure): The Plotly Figure object to be converted.
13
+
14
+ Returns:
15
+ bytes: The PNG image data as bytes.
16
+ :param f: The Plotly Figure object to be converted into a PNG image.
17
+ """
18
+
19
+ format_file = kwargs.get("format_file", "png") # The format of the image to be converted to
20
+ width = kwargs.get("width", 640) # The width of the image in pixels
21
+ height = kwargs.get("height", 480) # The height of the image in pixels
22
+
23
+ with io.BytesIO() as bytes_buffer:
24
+ f.write_image(bytes_buffer,
25
+ format=format_file,
26
+ width = width,
27
+ height = height) # Write the figure to the bytes buffer as a PNG image
28
+ bytes_buffer.seek(0) # Reset the buffer position to the beginning
29
+ return bytes_buffer.getvalue() # Return the bytes data
30
+
31
+ def html_from_figure(f: Figure) -> str:
32
+ """
33
+ Convert a Plotly Figure to an HTML string.
34
+
35
+ Args:
36
+ f (Figure): The Plotly Figure object to be converted.
37
+
38
+ Returns:
39
+ str: The HTML representation of the figure as a string.
40
+ """
41
+ with io.BytesIO() as bytes_buffer:
42
+ # Wrap the BytesIO with a TextIOWrapper to handle strings
43
+ with io.TextIOWrapper(bytes_buffer, encoding='utf-8') as text_buffer:
44
+ f.write_html(text_buffer) # Write the figure to the text buffer
45
+ text_buffer.flush() # Ensure all data is written
46
+ bytes_buffer.seek(0) # Reset the buffer position to the beginning
47
+ return bytes_buffer.getvalue().decode('utf-8') # Decode bytes to string and return
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyaws_s3
3
- Version: 1.0.3
3
+ Version: 1.0.4
4
4
  Summary: A Python package for AWS S3 utilities
5
5
  Author: Giuseppe Zileni
6
6
  Author-email: Giuseppe Zileni <giuseppe.zileni@gmail.com>
@@ -9,6 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Operating System :: OS Independent
10
10
  Requires-Python: >=3.12
11
11
  Description-Content-Type: text/markdown
12
+ License-File: LICENSE.md
12
13
  Requires-Dist: aioboto3==14.3.0
13
14
  Requires-Dist: aiobotocore==2.22.0
14
15
  Requires-Dist: aiofiles==24.1.0
@@ -46,6 +47,7 @@ Requires-Dist: urllib3==2.4.0
46
47
  Requires-Dist: wrapt==1.17.2
47
48
  Requires-Dist: yarl==1.20.0
48
49
  Dynamic: author
50
+ Dynamic: license-file
49
51
  Dynamic: requires-python
50
52
 
51
53
  # PYAWS_S3
@@ -0,0 +1,8 @@
1
+ pyaws_s3/__init__.py,sha256=Tr7xJiCKOMWYydOJ4kxHlA7AR1X3pRsJ8MjxJev2wsw,24
2
+ pyaws_s3/s3.py,sha256=c-sOzLxH3U0cQYpQMuIffOmE6T2oCS6ASQMBlvvLIeU,16326
3
+ pyaws_s3/util.py,sha256=2DRhpZADoDpAr-bTwzXfZ_SNG3Bno5xVk1wfwmNxknU,1822
4
+ pyaws_s3-1.0.4.dist-info/licenses/LICENSE.md,sha256=7WXohDebeZpcVn_nH2aIaLhFZRvZBdcPSqWsO12lhwM,1074
5
+ pyaws_s3-1.0.4.dist-info/METADATA,sha256=lKVf3wkeY6fp6Nt2Cm9QxU1MBrGiRp8H1Vg3L_FzCkg,5299
6
+ pyaws_s3-1.0.4.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
7
+ pyaws_s3-1.0.4.dist-info/top_level.txt,sha256=MxSSC4Q8Vr32wKgrUAlwT4BTXwqUaG_CAWoBuPeXYjQ,9
8
+ pyaws_s3-1.0.4.dist-info/RECORD,,
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2025 Giuseppe Zileni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ pyaws_s3
@@ -1,4 +0,0 @@
1
- pyaws_s3-1.0.3.dist-info/METADATA,sha256=GzSmAWUoaZRL0FQtg6S2jp_Dnth4IA8tIjKIfS4kEZI,5252
2
- pyaws_s3-1.0.3.dist-info/WHEEL,sha256=Nw36Djuh_5VDukK0H78QzOX-_FQEo6V37m3nkm96gtU,91
3
- pyaws_s3-1.0.3.dist-info/top_level.txt,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
4
- pyaws_s3-1.0.3.dist-info/RECORD,,
@@ -1 +0,0 @@
1
-