pypomes-s3 0.3.1__tar.gz

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.
@@ -0,0 +1,16 @@
1
+ .cache
2
+ .env
3
+ .venv
4
+ .idea
5
+ .vscode
6
+ env
7
+ venv
8
+ temp
9
+ tmp
10
+ **/__pycache__
11
+ *.py[cod]
12
+ *.sh
13
+ *.ps1
14
+ /dist
15
+ /env
16
+ deploy.txt
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 GT Nunes
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,19 @@
1
+ Metadata-Version: 2.3
2
+ Name: pypomes_s3
3
+ Version: 0.3.1
4
+ Summary: A collection of Python pomes, pennyeach (S3 storage modules)
5
+ Project-URL: Homepage, https://github.com/TheWiseCoder/PyPomes-S3
6
+ Project-URL: Bug Tracker, https://github.com/TheWiseCoder/PyPomes-S3/issues
7
+ Author-email: GT Nunes <wisecoder01@gmail.com>
8
+ License-File: LICENSE
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Programming Language :: Python :: 3
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: boto3>=1.34.108
14
+ Requires-Dist: minio>=7.2.5
15
+ Requires-Dist: pip>=24.0
16
+ Requires-Dist: pypomes-core>=1.0.4
17
+ Requires-Dist: setuptools>=68.0.0
18
+ Requires-Dist: unidecode>=1.3.6
19
+ Requires-Dist: wheel>=0.42.0
File without changes
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = [
3
+ "hatchling>=1.22.2"
4
+ ]
5
+ build-backend = "hatchling.build"
6
+
7
+ [project]
8
+ name = "pypomes_s3"
9
+ version = "0.3.1"
10
+ authors = [
11
+ { name="GT Nunes", email="wisecoder01@gmail.com" }
12
+ ]
13
+ description = "A collection of Python pomes, pennyeach (S3 storage modules)"
14
+ readme = "README.md"
15
+ requires-python = ">=3.10"
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "License :: OSI Approved :: MIT License",
19
+ "Operating System :: OS Independent"
20
+ ]
21
+ dependencies = [
22
+ "boto3>=1.34.108",
23
+ "minio>=7.2.5",
24
+ "pip>=24.0",
25
+ "pypomes_core>=1.0.4",
26
+ "setuptools>=68.0.0",
27
+ "Unidecode>=1.3.6",
28
+ "wheel>=0.42.0"
29
+ ]
30
+
31
+ [project.urls]
32
+ "Homepage" = "https://github.com/TheWiseCoder/PyPomes-S3"
33
+ "Bug Tracker" = "https://github.com/TheWiseCoder/PyPomes-S3/issues"
File without changes
@@ -0,0 +1,24 @@
1
+ from .minio_client import (
2
+ MinioCM
3
+ )
4
+ from .minio_pomes import (
5
+ MINIO_BUCKET_NAME, MINIO_ENDPOINT_URL, MINIO_ACCESS_KEY, MINIO_SECRET_KEY, MINIO_SECURE_ACCESS, MINIO_TEMP_PATH,
6
+ minio_access, minio_file_store, minio_object_store, minio_object_stat,
7
+ minio_object_delete, minio_objects_list, minio_object_retrieve, minio_object_exists,
8
+ minio_object_tags_retrieve, minio_file_retrieve, minio_setup,
9
+ )
10
+
11
+ __all__ = [
12
+ # minio_client
13
+ "MinioCM",
14
+ # minio_pomes
15
+ "MINIO_BUCKET_NAME", "MINIO_ENDPOINT_URL", "MINIO_ACCESS_KEY",
16
+ "MINIO_SECRET_KEY", "MINIO_SECURE_ACCESS", "MINIO_TEMP_PATH",
17
+ "minio_access", "minio_file_store", "minio_object_store", "minio_object_stat",
18
+ "minio_object_delete", "minio_objects_list", "minio_object_retrieve", "minio_object_exists",
19
+ "minio_object_tags_retrieve", "minio_file_retrieve", "minio_setup",
20
+ ]
21
+
22
+ from importlib.metadata import version
23
+ __version__ = version("pypomes_s3")
24
+ __version_info__ = tuple(int(i) for i in __version__.split(".") if i.isdigit())
@@ -0,0 +1,42 @@
1
+ # import pickle
2
+ # import uuid
3
+ from boto3.session import Session
4
+ from botocore.client import BaseClient
5
+ from logging import Logger
6
+ from pathlib import Path
7
+ from pypomes_core import (
8
+ APP_PREFIX, TEMP_DIR,
9
+ env_get_str, env_get_path
10
+ )
11
+ from typing import Final
12
+ # from unidecode import unidecode
13
+
14
+ S3_BUCKET_NAME: Final[str] = env_get_str(f"{APP_PREFIX}_S3_BUCKET_NAME")
15
+ S3_ACCESS_KEY: Final[str] = env_get_str(f"{APP_PREFIX}_S3_ACCESS_KEY")
16
+ S3_SECRET_KEY: Final[str] = env_get_str(f"{APP_PREFIX}_S3_SECRET_KEY")
17
+ S3_ENDPOINT_URL: Final[str] = env_get_str(f"{APP_PREFIX}_S3_ENDPOINT_URL")
18
+ S3_TEMP_PATH: Final[Path] = env_get_path(f"{APP_PREFIX}_S3_TEMP_PATH", TEMP_DIR)
19
+
20
+
21
+ def s3_access(errors: list[str],
22
+ logger: Logger = None) -> BaseClient:
23
+ """
24
+ Obtain and return a *S3* client object.
25
+
26
+ :param errors: incidental error messages
27
+ :param logger: optional logger
28
+ :return: the S3 client object
29
+ """
30
+ # initialize the return variable
31
+ result: BaseClient | None = None
32
+
33
+ try:
34
+ result = Session().client(service_name="s3",
35
+ aws_access_key_id=S3_ACCESS_KEY,
36
+ aws_secret_access_key=S3_SECRET_KEY,
37
+ endpoint_url=S3_ENDPOINT_URL)
38
+ except Exception as e:
39
+ errors.append(str(e))
40
+ logger.debug(e)
41
+
42
+ return result
@@ -0,0 +1,67 @@
1
+ from minio import Minio
2
+ from minio.credentials.providers import Provider
3
+ from types import TracebackType
4
+ from urllib3 import PoolManager
5
+
6
+
7
+ class MinioCM:
8
+ """
9
+ Simple Storage Service (*S3*) client, extended to provide context-managed operations.
10
+
11
+ Please, refer to the *MinIO* package documentation for details on its implementation.
12
+ """
13
+ def __init__(self,
14
+ endpoint: str,
15
+ access_key: str | None = None,
16
+ secret_key: str | None = None,
17
+ session_token: str | None = None,
18
+ secure: bool = True,
19
+ region: str | None = None,
20
+ http_client: PoolManager | None = None,
21
+ credentials: Provider | None = None,
22
+ cert_check: bool = True) -> None:
23
+ """
24
+ Extend *MinIO* package's implementation of a Simple Storage Service (*S3*) client, .
25
+
26
+ The aim is to provide context-managed *MinIO* clients.
27
+ Please, refer to the *MinIO* package documentation for details on its implementation.
28
+
29
+ :param endpoint: Hostname of a S3 service
30
+ :param access_key: Access key (aka user ID) of your account in S3 service
31
+ :param secret_key: Secret Key (aka password) of your account in S3 service
32
+ :param session_token: Session token of your account in S3 service
33
+ :param secure: Flag to indicate to use secure (TLS) connection to S3 service or not
34
+ :param region: Region name of buckets in S3 service
35
+ :param http_client: Customized HTTP client
36
+ :param credentials: Credentials provider of your account in S3 service
37
+ :param cert_check: Flag to indicate to verify SSL certificate or not
38
+ """
39
+ self.minio_client: Minio = Minio(endpoint=endpoint,
40
+ access_key=access_key,
41
+ secret_key=secret_key,
42
+ session_token=session_token,
43
+ secure=secure,
44
+ region=region,
45
+ http_client=http_client,
46
+ credentials=credentials,
47
+ cert_check=cert_check)
48
+
49
+ def __enter__(self) -> Minio:
50
+ """
51
+ First step in providing a context-managed *MinIO* client.
52
+
53
+ :return: a Minio client object
54
+ """
55
+ return self.minio_client
56
+
57
+ def __exit__(self,
58
+ exception_type: type[BaseException],
59
+ exception_value: BaseException,
60
+ traceback: TracebackType) -> bool:
61
+ """
62
+ Last step in providing a context-managed *MinIO* client.
63
+ """
64
+ self.minio_client = None
65
+
66
+ # make sure an eventual exception is propagated
67
+ return exception_type is None
@@ -0,0 +1,578 @@
1
+ import pickle
2
+ import uuid
3
+ from collections.abc import Iterator
4
+ from logging import Logger
5
+ from minio import Minio
6
+ from minio.datatypes import Object as MinioObject
7
+ from minio.commonconfig import Tags
8
+ from pathlib import Path
9
+ from pypomes_core import (
10
+ APP_PREFIX, TEMP_DIR,
11
+ env_get_bool, env_get_str, env_get_path
12
+ )
13
+ from typing import Final
14
+ from unidecode import unidecode
15
+
16
+ MINIO_BUCKET_NAME: Final[str] = env_get_str(f"{APP_PREFIX}_MINIO_BUCKET_URL")
17
+ MINIO_ENDPOINT_URL: Final[str] = env_get_str(f"{APP_PREFIX}_MINIO_ENDPOINT_URL")
18
+ MINIO_ACCESS_KEY: Final[str] = env_get_str(f"{APP_PREFIX}_MINIO_ACCESS_KEY")
19
+ MINIO_SECRET_KEY: Final[str] = env_get_str(f"{APP_PREFIX}_MINIO_SECRET_KEY")
20
+ MINIO_SECURE_ACCESS: Final[bool] = env_get_bool(f"{APP_PREFIX}_MINIO_SECURE_ACCESS")
21
+ MINIO_TEMP_PATH: Final[Path] = env_get_path(f"{APP_PREFIX}_MINIO_TEMP_PATH", TEMP_DIR)
22
+
23
+
24
+ def minio_access(errors: list[str],
25
+ logger: Logger = None) -> Minio:
26
+ """
27
+ Obtain and return a *MinIO* client object.
28
+
29
+ :param errors: incidental error messages
30
+ :param logger: optional logger
31
+ :return: the MinIO client object
32
+ """
33
+ # initialize the return variable
34
+ result: Minio | None = None
35
+
36
+ # obtain the MinIO client
37
+ try:
38
+ result = Minio(endpoint=MINIO_ENDPOINT_URL,
39
+ access_key=MINIO_ACCESS_KEY,
40
+ secret_key=MINIO_SECRET_KEY,
41
+ secure=MINIO_SECURE_ACCESS)
42
+ if logger:
43
+ logger.debug("Minio client created")
44
+
45
+ except Exception as e:
46
+ __minio_except_msg(errors=errors,
47
+ exception=e,
48
+ logger=logger)
49
+
50
+ return result
51
+
52
+
53
+ def minio_setup(errors: list[str],
54
+ bucket: str = MINIO_BUCKET_NAME,
55
+ client: Minio = None,
56
+ logger: Logger = None) -> bool:
57
+ """
58
+ Prepare the *MinIO* client for operations.
59
+
60
+ This function should be called just once, at startup,
61
+ to make sure the interaction with the MinIo service is fully functional.
62
+
63
+ :param errors: incidental error messages
64
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
65
+ :param client: optional MinIO client (obtains a new one, if not provided)
66
+ :param logger: optional logger
67
+ :return: True if service is fully functional
68
+ """
69
+ # initialize the return variable
70
+ result: bool = False
71
+
72
+ # make sure to have a MinIO client
73
+ curr_client: Minio = client or minio_access(errors=errors,
74
+ logger=logger)
75
+ # was the MinIO client obtained ?
76
+ if curr_client:
77
+ # yes, proceed
78
+ try:
79
+ if not curr_client.bucket_exists(bucket_name=bucket):
80
+ curr_client.make_bucket(bucket_name=bucket)
81
+ result = True
82
+ if logger:
83
+ logger.debug(f"Setup MinIO, endpoint={MINIO_ENDPOINT_URL}, bucket={bucket}, "
84
+ f"access key={MINIO_ACCESS_KEY}, secure={MINIO_SECURE_ACCESS}")
85
+ except Exception as e:
86
+ __minio_except_msg(errors=errors,
87
+ exception=e,
88
+ logger=logger)
89
+
90
+ return result
91
+
92
+
93
+ def minio_file_store(errors: list[str],
94
+ basepath: Path | str,
95
+ identifier: str,
96
+ filepath: Path | str,
97
+ mimetype: str,
98
+ tags: dict = None,
99
+ bucket: str = MINIO_BUCKET_NAME,
100
+ client: Minio = None,
101
+ logger: Logger = None) -> None:
102
+ """
103
+ Store a file at the *MinIO* store.
104
+
105
+ :param errors: incidental error messages
106
+ :param basepath: the path specifying the location to store the file at
107
+ :param identifier: the file identifier, tipically a file name
108
+ :param filepath: the path specifying where the file is
109
+ :param mimetype: the file mimetype
110
+ :param tags: optional metadata describing the file
111
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
112
+ :param client: optional MinIO client (obtains a new one, if not provided)
113
+ :param logger: optional logger
114
+ """
115
+ # make sure to have a MinIO client
116
+ curr_client: Minio = client or minio_access(errors=errors,
117
+ logger=logger)
118
+ # was the MinIO client obtained ?
119
+ if curr_client:
120
+ # yes, proceed
121
+ remotepath: Path = Path(basepath) / identifier
122
+ # have tags been defined ?
123
+ if tags is None or len(tags) == 0:
124
+ # no
125
+ doc_tags = None
126
+ else:
127
+ # sim, store them
128
+ doc_tags = Tags(for_object=True)
129
+ for key, value in tags.items():
130
+ # normalize text, by removing all diacritics
131
+ doc_tags[key] = unidecode(value)
132
+ # store the file
133
+ try:
134
+ curr_client.fput_object(bucket_name=bucket,
135
+ object_name=f"{remotepath}",
136
+ file_path=filepath,
137
+ content_type=mimetype,
138
+ tags=doc_tags)
139
+ if logger:
140
+ logger.debug(f"Stored {remotepath}, "
141
+ f"content type {mimetype}, tags {tags}, bucket {bucket}")
142
+ except Exception as e:
143
+ __minio_except_msg(errors=errors,
144
+ exception=e,
145
+ logger=logger)
146
+
147
+
148
+ def minio_file_retrieve(errors: list[str],
149
+ basepath: Path | str,
150
+ identifier: str,
151
+ filepath: Path | str,
152
+ bucket: str = MINIO_BUCKET_NAME,
153
+ client: Minio = None,
154
+ logger: Logger = None) -> any:
155
+ """
156
+ Retrieve a file from the *MinIO* store.
157
+
158
+ :param errors: incidental error messages
159
+ :param basepath: the path specifying the location to retrieve the file from
160
+ :param identifier: the file identifier, tipically a file name
161
+ :param filepath: the path to save the retrieved file at
162
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
163
+ :param client: optional MinIO client (obtains a new one, if not provided)
164
+ :param logger: optional logger
165
+ :return: information about the file retrieved
166
+ """
167
+ # initialize the return variable
168
+ result: any = None
169
+
170
+ # make sure to have a MinIO client
171
+ curr_client: Minio = client or minio_access(errors=errors,
172
+ logger=logger)
173
+ # was the MinIO client obtained ?
174
+ if curr_client:
175
+ # yes, proceed
176
+ remotepath: Path = Path(basepath) / identifier
177
+ try:
178
+ result = curr_client.fget_object(bucket_name=bucket,
179
+ object_name=f"{remotepath}",
180
+ file_path=filepath)
181
+ if logger:
182
+ logger.debug(f"Retrieved {remotepath}, bucket {bucket}")
183
+ except Exception as e:
184
+ if not hasattr(e, "code") or e.code != "NoSuchKey":
185
+ __minio_except_msg(errors=errors,
186
+ exception=e,
187
+ logger=logger)
188
+
189
+ return result
190
+
191
+
192
+ def minio_object_exists(errors: list[str],
193
+ basepath: Path | str,
194
+ identifier: str = None,
195
+ bucket: str = MINIO_BUCKET_NAME,
196
+ client: Minio = None,
197
+ logger: Logger = None) -> bool:
198
+ """
199
+ Determine if a given object exists in the *MinIO* store.
200
+
201
+ :param errors: incidental error messages
202
+ :param basepath: the path specifying the location to locate the object at
203
+ :param identifier: the object identifier
204
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
205
+ :param client: optional MinIO client (obtains a new one, if not provided)
206
+ :param logger: optional logger
207
+ :return: True if the object was found
208
+ """
209
+ # initialize the return variable
210
+ result: bool = False
211
+
212
+ # make sure to have a MinIO client
213
+ curr_client: Minio = client or minio_access(errors=errors,
214
+ logger=logger)
215
+ # proceed, if the MinIO client eas obtained
216
+ if curr_client:
217
+ # was the identifier provided ?
218
+ if identifier is None:
219
+ # no, object is a folder
220
+ objs: Iterator = minio_objects_list(errors=errors,
221
+ basepath=basepath,
222
+ recursive=False,
223
+ bucket=bucket,
224
+ client=curr_client,
225
+ logger=logger)
226
+ for _ in objs:
227
+ result = True
228
+ break
229
+ # verify the status of the object
230
+ elif minio_object_stat(errors=errors,
231
+ basepath=basepath,
232
+ identifier=identifier,
233
+ bucket=bucket,
234
+ client=curr_client,
235
+ logger=logger):
236
+ result = True
237
+ if logger:
238
+ remotepath: Path = Path(basepath) / identifier
239
+ existence: str = "exists" if result else "do not exist"
240
+ logger.debug(f"Object {remotepath}, bucket {bucket}, {existence}")
241
+
242
+ return result
243
+
244
+
245
+ def minio_object_stat(errors: list[str],
246
+ basepath: Path | str,
247
+ identifier: str,
248
+ bucket: str = MINIO_BUCKET_NAME,
249
+ client: Minio = None,
250
+ logger: Logger = None) -> MinioObject:
251
+ """
252
+ Retrieve and return the information about an object in the *MinIO* store.
253
+
254
+ :param errors: incidental error messages
255
+ :param basepath: the path specifying where to locate the object
256
+ :param identifier: the object identifier
257
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
258
+ :param client: optional MinIO client (obtains a new one, if not provided)
259
+ :param logger: optional logger
260
+ :return: metadata and information about the object
261
+ """
262
+ # initialize the return variable
263
+ result: MinioObject | None = None
264
+
265
+ # make sure to have a MinIO client
266
+ curr_client: Minio = client or minio_access(errors=errors,
267
+ logger=logger)
268
+ # was the MinIO client obtained ?
269
+ if curr_client:
270
+ # yes, proceed
271
+ remotepath: Path = Path(basepath) / identifier
272
+ try:
273
+ result = curr_client.stat_object(bucket_name=bucket,
274
+ object_name=f"{remotepath}")
275
+ if logger:
276
+ logger.debug(f"Stat'ed {remotepath}, bucket {bucket}")
277
+ except Exception as e:
278
+ if not hasattr(e, "code") or e.code != "NoSuchKey":
279
+ __minio_except_msg(errors=errors,
280
+ exception=e,
281
+ logger=logger)
282
+
283
+ return result
284
+
285
+
286
+ def minio_object_store(errors: list[str],
287
+ basepath: Path | str,
288
+ identifier: str,
289
+ obj: any,
290
+ tags: dict = None,
291
+ bucket: str = MINIO_BUCKET_NAME,
292
+ client: Minio = None,
293
+ logger: Logger = None) -> None:
294
+ """
295
+ Store an object at the *MinIO* store.
296
+
297
+ :param errors: incidental error messages
298
+ :param basepath: the path specifying the location to store the object at
299
+ :param identifier: the object identifier
300
+ :param obj: object to be stored
301
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
302
+ :param client: optional MinIO client (obtains a new one, if not provided)
303
+ :param logger: optional logger
304
+ :param tags: optional metadata describing the object
305
+ """
306
+ # make sure to have a MinIO client
307
+ curr_client: Minio = client or minio_access(errors=errors,
308
+ logger=logger)
309
+ # proceed, if the MinIO client was obtained
310
+ if curr_client:
311
+ # serialize the object into a file
312
+ filepath: Path = Path(MINIO_TEMP_PATH) / f"{uuid.uuid4()}.pickle"
313
+ with filepath.open("wb") as f:
314
+ pickle.dump(obj, f)
315
+
316
+ # store the file
317
+ op_errors: list[str] = []
318
+ minio_file_store(errors=op_errors,
319
+ basepath=basepath,
320
+ identifier=identifier,
321
+ filepath=filepath,
322
+ mimetype="application/octet-stream",
323
+ tags=tags,
324
+ bucket=bucket,
325
+ client=curr_client,
326
+ logger=logger)
327
+
328
+ # errors ?
329
+ if op_errors:
330
+ # yes, report them
331
+ errors.extend(op_errors)
332
+ storage: str = "Unable to store"
333
+ else:
334
+ # no, remove the file from the file system
335
+ filepath.unlink()
336
+ storage: str = "Stored "
337
+
338
+ if logger:
339
+ remotepath: Path = Path(basepath) / identifier
340
+ logger.debug(f"{storage} {remotepath}, bucket {bucket}")
341
+
342
+
343
+ def minio_object_retrieve(errors: list[str],
344
+ basepath: Path,
345
+ identifier: str,
346
+ bucket: str = MINIO_BUCKET_NAME,
347
+ client: Minio = None,
348
+ logger: Logger = None) -> any:
349
+ """
350
+ Retrieve an object from the *MinIO* store.
351
+
352
+ :param errors: incidental error messages
353
+ :param basepath: the path specifying the location to retrieve the object from
354
+ :param identifier: the object identifier
355
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
356
+ :param client: optional MinIO client (obtains a new one, if not provided)
357
+ :param logger: optional logger
358
+ :return: the object retrieved
359
+ """
360
+ # initialize the return variable
361
+ result: any = None
362
+
363
+ # make sure to have a MinIO client
364
+ curr_client: Minio = client or minio_access(errors=errors,
365
+ logger=logger)
366
+ # proceed, if the MinIO client was obtained
367
+ if curr_client:
368
+ # retrieve the file containg the serialized object
369
+ filepath: Path = Path(MINIO_TEMP_PATH) / f"{uuid.uuid4()}.pickle"
370
+ stat: any = minio_file_retrieve(errors=errors,
371
+ basepath=basepath,
372
+ identifier=identifier,
373
+ filepath=filepath,
374
+ bucket=bucket,
375
+ client=curr_client,
376
+ logger=logger)
377
+
378
+ # was the file retrieved ?
379
+ if stat:
380
+ # yes, umarshall the corresponding object
381
+ with filepath.open("rb") as f:
382
+ result = pickle.load(f)
383
+ filepath.unlink()
384
+
385
+ if logger:
386
+ retrieval: str = "Retrieved" if result else "Unable to retrieve"
387
+ remotepath: Path = Path(basepath) / identifier
388
+ logger.debug(f"{retrieval} {remotepath}, bucket {bucket}")
389
+
390
+ return result
391
+
392
+
393
+ def minio_object_delete(errors: list[str],
394
+ basepath: str,
395
+ identifier: str = None,
396
+ bucket: str = MINIO_BUCKET_NAME,
397
+ client: Minio = None,
398
+ logger: Logger = None) -> None:
399
+ """
400
+ Remove an object from the *MinIO* store.
401
+
402
+ :param errors: incidental error messages
403
+ :param basepath: the path specifying the location to retrieve the object from
404
+ :param identifier: the object identifier
405
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
406
+ :param client: optional MinIO client (obtains a new one, if not provided)
407
+ :param logger: optional logger
408
+ """
409
+ # make sure to have a MinIO client
410
+ curr_client: Minio = client or minio_access(errors=errors,
411
+ logger=logger)
412
+ # proceed, if the MinIO client was obtained
413
+ if curr_client:
414
+ # was the identifier provided ?
415
+ if identifier is None:
416
+ # no, remove the folder
417
+ __minio_folder_delete(errors=errors,
418
+ client=curr_client,
419
+ basepath=basepath,
420
+ bucket=bucket,
421
+ logger=logger)
422
+ else:
423
+ # yes, remove the object
424
+ remotepath: Path = Path(basepath) / identifier
425
+ try:
426
+ curr_client.remove_object(bucket_name=bucket,
427
+ object_name=f"{remotepath}")
428
+ if logger:
429
+ logger.debug(f"Deleted {remotepath}, bucket {bucket}")
430
+ except Exception as e:
431
+ if not hasattr(e, "code") or e.code != "NoSuchKey":
432
+ __minio_except_msg(errors=errors,
433
+ exception=e,
434
+ logger=logger)
435
+
436
+
437
+ # recupera as tags do objeto
438
+ def minio_object_tags_retrieve(errors: list[str],
439
+ basepath: str,
440
+ identifier: str,
441
+ bucket: str = MINIO_BUCKET_NAME,
442
+ client: Minio = None,
443
+ logger: Logger = None) -> dict:
444
+ """
445
+ Retrieve and return the metadata information for an object in the *MinIO* store.
446
+
447
+ :param errors: incidental error messages
448
+ :param basepath: the path specifying the location to retrieve the object from
449
+ :param identifier: the object identifier
450
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
451
+ :param client: optional MinIO client (obtains a new one, if not provided)
452
+ :param logger: optional logger
453
+ :return: the metadata about the object
454
+ """
455
+ # initialize the return variable
456
+ result: dict | None = None
457
+
458
+ # make sure to have a MinIO client
459
+ curr_client: Minio = client or minio_access(errors=errors,
460
+ logger=logger)
461
+ # was the MinIO client obtained ?
462
+ if curr_client:
463
+ # yes, proceed
464
+ remotepath: Path = Path(basepath) / identifier
465
+ try:
466
+ tags: Tags = curr_client.get_object_tags(bucket_name=bucket,
467
+ object_name=f"{remotepath}")
468
+ if tags is not None and len(tags) > 0:
469
+ result = {}
470
+ for key, value in tags.items():
471
+ result[key] = value
472
+ if logger:
473
+ logger.debug(f"Retrieved {remotepath}, bucket {bucket}, tags {result}")
474
+ except Exception as e:
475
+ if not hasattr(e, "code") or e.code != "NoSuchKey":
476
+ __minio_except_msg(errors=errors,
477
+ exception=e,
478
+ logger=logger)
479
+
480
+ return result
481
+
482
+
483
+ def minio_objects_list(errors: list[str],
484
+ basepath: str,
485
+ recursive: bool = False,
486
+ bucket: str = MINIO_BUCKET_NAME,
487
+ client: Minio = None,
488
+ logger: Logger = None) -> Iterator:
489
+ """
490
+ Retrieve and return an iterator into the list of objects at *basepath*, in the *MinIO* store.
491
+
492
+ :param errors: incidental error messages
493
+ :param basepath: the path specifying the location to iterate from
494
+ :param recursive: whether the location is iterated recursively
495
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
496
+ :param client: optional MinIO client (obtains a new one, if not provided)
497
+ :param logger: optional logger
498
+ :return: the iterator into the list of objects, 'None' if the folder does not exist
499
+ """
500
+ # initialize the return variable
501
+ result: Iterator | None = None
502
+
503
+ # make sure to have a MinIO client
504
+ curr_client: Minio = client or minio_access(errors=errors,
505
+ logger=logger)
506
+ # was the MinIO client obtained ?
507
+ if curr_client:
508
+ # yes, proceed
509
+ try:
510
+ result = curr_client.list_objects(bucket_name=bucket,
511
+ prefix=basepath,
512
+ recursive=recursive)
513
+ if logger:
514
+ logger.debug(f"Listed {basepath}, bucket {bucket}")
515
+ except Exception as e:
516
+ __minio_except_msg(errors=errors,
517
+ exception=e,
518
+ logger=logger)
519
+
520
+ return result
521
+
522
+
523
+ def __minio_folder_delete(errors: list[str],
524
+ client: Minio,
525
+ basepath: str,
526
+ bucket: str = MINIO_BUCKET_NAME,
527
+ logger: Logger = None) -> None:
528
+ """
529
+ Traverse the folders recursively, removing its objects.
530
+
531
+ :param errors: incidental error messages
532
+ :param client: the MinIO client object
533
+ :param basepath: the path specifying the location to delete the objects at
534
+ :param bucket: the bucket to use (defaults to MINIO_BUCKET)
535
+ :param logger: optional logger
536
+ """
537
+ # obtain the list of entries in the given folder
538
+ objs: Iterator = minio_objects_list(errors=errors,
539
+ basepath=basepath,
540
+ recursive=True,
541
+ bucket=bucket,
542
+ logger=logger)
543
+ # was the list obtained ?
544
+ if objs:
545
+ # yes, proceed
546
+ for obj in objs:
547
+ try:
548
+ client.remove_object(bucket_name=bucket,
549
+ object_name=obj.object_name)
550
+ except Exception as e:
551
+ # SANITY CHECK: in case of concurrent exclusion
552
+ if not hasattr(e, "code") or e.code != "NoSuchKey":
553
+ __minio_except_msg(errors=errors,
554
+ exception=e,
555
+ logger=logger)
556
+ if logger:
557
+ logger.debug(msg=f"Removed folder {basepath}, bucket {bucket}")
558
+
559
+
560
+ def __minio_except_msg(errors: list[str],
561
+ exception: Exception,
562
+ logger: Logger) -> None:
563
+ """
564
+ Format and return an error message from *exception*.
565
+
566
+ :param errors: incidental error messages
567
+ :param exception: the reference exception
568
+ :param logger: optional logger
569
+ :return: the error message
570
+ """
571
+ # interaction with MinIO raised the exception "<class 'exception_class'>"
572
+ cls: str = str(exception.__class__)
573
+ exc_msg: str = f"{cls[7:-1]} - {exception}"
574
+ err_msg: str = f"Error accessing the object storer: {exc_msg}"
575
+ if errors:
576
+ errors.append(err_msg)
577
+ if logger:
578
+ logger.error(msg=err_msg)
@@ -0,0 +1,145 @@
1
+ from logging import DEBUG, Logger
2
+ from pypomes_core import (
3
+ APP_PREFIX,
4
+ env_get_str, str_sanitize, str_get_positional
5
+ )
6
+
7
+ # the preferred way to specify S3 storage parameters is dynamically with 's3_setup_params'
8
+ # specifying S3 storage parameters with environment variables can be done in two ways:
9
+ # 1. specify the set
10
+ # {APP_PREFIX}_S3_ENGINE (one of 'aws', 'minio')
11
+ # {APP_PREFIX}_S3_ACCESS_KEY
12
+ # {APP_PREFIX}_S3_SECRET_KEY
13
+ # {APP_PREFIX}_S3_BUCKET_NAME
14
+ # {APP_PREFIX}_S3_TEMP_PATH
15
+ # {APP_PREFIX}_S3_REGION_NAME (for aws)
16
+ # {APP_PREFIX}_S3_ENDPOINT_URL (for minio)
17
+ # {APP_PREFIX}_S3_SECURE_ACCESS (for minio)
18
+ # 2. alternatively, specify a comma-separated list of servers in
19
+ # {APP_PREFIX}_S3_ENGINES
20
+ # and for each engine, specify the set above, replacing 'S3' with
21
+ # 'AWS' and 'MINIO', respectively for the engines listed above
22
+
23
+ _S3_ACCESS_DATA: dict = {}
24
+ _S3_ENGINES: list[str] = []
25
+ if env_get_str(f"{APP_PREFIX}_S3_ENGINE", None):
26
+ _default_setup: bool = True
27
+ _S3_ENGINES.append(env_get_str(f"{APP_PREFIX}_S3_ENGINE"))
28
+ else:
29
+ _default_setup: bool = False
30
+ _engines: str = env_get_str(f"{APP_PREFIX}_S3_ENGINES", None)
31
+ if _engines:
32
+ _S3_ENGINES.extend(_engines.split(sep=","))
33
+ for engine in _S3_ENGINES:
34
+ if _default_setup:
35
+ _tag = "S3"
36
+ _default_setup = False
37
+ else:
38
+ _tag: str = str_get_positional(source=engine,
39
+ list_origin=["aws", "minio"],
40
+ list_dest=["AWS", "MINIO"])
41
+ _s3_data = {
42
+ "access-key": env_get_str(f"{APP_PREFIX}_{_tag}_ACESS_KEY"),
43
+ "secret-key": env_get_str(f"{APP_PREFIX}_{_tag}_SECRET_KEY"),
44
+ "bucket-name": env_get_str(f"{APP_PREFIX}_{_tag}_BUCKET_NAME"),
45
+ "temp-path": env_get_str(f"{APP_PREFIX}_{_tag}_TEMP_PATH")
46
+ }
47
+ if engine == "aws":
48
+ _s3_data["client"] = env_get_str(f"{APP_PREFIX}_{_tag}_REGION_NAME")
49
+ elif engine == "minio":
50
+ _s3_data["endpoit-url"] = env_get_str(f"{APP_PREFIX}_{_tag}_ENDPOINT_URL")
51
+ _s3_data["secure-access"] = env_get_str(f"{APP_PREFIX}_{_tag}_SECURE_ACCESS")
52
+ _S3_ACCESS_DATA[engine] = _s3_data
53
+
54
+
55
+ def _assert_engine(errors: list[str],
56
+ engine: str) -> str:
57
+ """
58
+ Verify if *engine* is in the list of supported engines.
59
+
60
+ If *engine* is a supported engine, it is returned. If its value is 'None',
61
+ the first engine in the list of supported engines (the default engine) is returned.
62
+
63
+ :param errors: incidental errors
64
+ :param engine: the reference database engine
65
+ :return: the validated or default engine
66
+ """
67
+ # initialize the return valiable
68
+ result: str | None = None
69
+
70
+ if not engine and _S3_ENGINES:
71
+ result = _S3_ENGINES[0]
72
+ elif engine in _S3_ENGINES:
73
+ result = engine
74
+ else:
75
+ err_msg = f"S3 engine '{engine}' unknown or not configured"
76
+ errors.append(err_msg)
77
+
78
+ return result
79
+
80
+
81
+ def _s3_get_params(engine: str) -> tuple:
82
+ """
83
+ Return the current parameters being used for *engine*.
84
+
85
+ The parameters are returned as a *tuple*, with the elements
86
+ *access-key*, *secret-key*, *bucket-name*, *temp-path*.
87
+ For *aws* engines, the extra element *region-name* is returned.
88
+ for *minio* engines, the elements *endpoint-url* and *secure-access are returned.
89
+ The meaning of some parameters may vary between different S3 engines.
90
+
91
+ :param engine: the reference database engine
92
+ :return: the current parameters for the engine
93
+ """
94
+ access_key: str = _S3_ACCESS_DATA[engine]["access-key"]
95
+ secret_key: str = _S3_ACCESS_DATA[engine]["secret-key"]
96
+ bucket_name: str = _S3_ACCESS_DATA[engine]["bucket-name"]
97
+ temp_path = _S3_ACCESS_DATA[engine]["temp-path"]
98
+
99
+ result: tuple | None = None
100
+ if engine == "aws":
101
+ region_name: str = _S3_ACCESS_DATA[engine]["region-name"]
102
+ result = (access_key, secret_key, bucket_name, temp_path, region_name)
103
+ elif engine == "minio":
104
+ endpoint_url: str = _S3_ACCESS_DATA[engine]["endpoint-url"]
105
+ secure_access: bool = _S3_ACCESS_DATA[engine]["secure-access"]
106
+ result = (access_key, secret_key, bucket_name, temp_path, endpoint_url, secure_access)
107
+
108
+ return result
109
+
110
+
111
+ def _db_except_msg(exception: Exception,
112
+ engine: str) -> str:
113
+ """
114
+ Format and return the error message corresponding to the exception raised while accessing the database.
115
+
116
+ :param exception: the exception raised
117
+ :param engine: the reference database engine
118
+ :return: the formatted error message
119
+ """
120
+ endpoint: str = _S3_ACCESS_DATA[engine].get("region-name") if engine == "aws" else \
121
+ _S3_ACCESS_DATA[engine].get("endpoint-url")
122
+ return f"Error accessing '{engine}' at '{endpoint}': {str_sanitize(f'{exception}')}"
123
+
124
+
125
+ def _s3_log(logger: Logger,
126
+ err_msg: str = None,
127
+ level: int = DEBUG,
128
+ errors: list[str] = None,
129
+ stmt: str = None) -> None:
130
+ """
131
+ Log *err_msg* and add it to *errors*, or else log *stmt*, whichever is applicable.
132
+
133
+ :param logger: the logger object
134
+ :param err_msg: the error message to log
135
+ :param level: log level (defaults to DEBUG)
136
+ :param errors: optional incidental errors
137
+ :param stmt: optional statement
138
+ """
139
+ if err_msg:
140
+ if logger:
141
+ logger.log(level, err_msg)
142
+ if isinstance(errors, list):
143
+ errors.append(err_msg)
144
+ if logger and stmt:
145
+ logger.log(level, stmt)
@@ -0,0 +1,89 @@
1
+ from logging import Logger
2
+ from pathlib import Path
3
+ from typing import Any
4
+
5
+ from .s3_common import (
6
+ _S3_ENGINES, _S3_ACCESS_DATA, _assert_engine
7
+ )
8
+
9
+
10
+ def db_setup(engine: str,
11
+ access_key: str,
12
+ access_secret: str,
13
+ bucket_name: str,
14
+ temp_path: str | Path,
15
+ region_name: str = None,
16
+ endpoint_url: str = None,
17
+ secure_access: bool = None) -> bool:
18
+ """
19
+ Establish the provided parameters for access to *engine*.
20
+
21
+ The meaning of some parameters may vary between different database engines.
22
+ All parameters, with the exception of *db_client* and *db_driver*, are required.
23
+ *db_client* may be provided for *oracle*, but not for the other engines.
24
+ *db_driver* is required for *sqlserver*, but is not accepted for the other engines.
25
+
26
+ :param engine: the S3 engine (one of [aws, minio])
27
+ :param access_key: the access key for the service
28
+ :param access_secret: the access secret code
29
+ :param bucket_name: the name of the default bucket
30
+ :param temp_path: path for temporary files
31
+ :param region_name: the name of the region where the engine is located (AWS only)
32
+ :param endpoint_url: the access URL for the service (MinIO only)
33
+ :param secure_access: whether or not to use Transport Security Layer (MinIO only)
34
+ :return: True if the data was accepted, False otherwise
35
+ """
36
+ # initialize the return variable
37
+ result: bool = False
38
+
39
+ # are the parameters compliant ?
40
+ if (engine in ["aws", "minio"] and
41
+ access_key and access_secret and bucket_name and temp_path and
42
+ not (engine != "aws" and region_name) and
43
+ not (engine == "aws" and not region_name) and
44
+ not (engine != "minio" and endpoint_url) and
45
+ not (engine == "minio" and not endpoint_url) and
46
+ not (engine != "minio" and secure_access is not None) and
47
+ not (engine == "minio" and secure_access is None)):
48
+ _S3_ACCESS_DATA[engine] = {
49
+ "access-key": access_key,
50
+ "access-secret": access_secret,
51
+ "bucket-name": bucket_name,
52
+ "temp-path": temp_path
53
+ }
54
+ if engine == "aws":
55
+ _S3_ACCESS_DATA[engine]["region-name"] = region_name
56
+ elif engine == "minio":
57
+ _S3_ACCESS_DATA[engine]["endpoint-url"] = endpoint_url
58
+ _S3_ACCESS_DATA[engine]["secure-access"] = secure_access
59
+ if engine not in _S3_ENGINES:
60
+ _S3_ENGINES.append(engine)
61
+ result = True
62
+
63
+ return result
64
+
65
+
66
+ def s3_get_engines() -> list[str]:
67
+ """
68
+ Retrieve and return the list of configured engines.
69
+
70
+ This list may include any of the supported engines:
71
+ *aws*, *minio*.
72
+
73
+ :return: the list of configured engines
74
+ """
75
+ return _S3_ENGINES
76
+
77
+
78
+ def db_get_params(engine: str = None) -> dict:
79
+ """
80
+ Return the connection parameters a *dict*.
81
+
82
+ The returned *dict* contains the keys *name*, *user*, *pwd*, *host*, *port*.
83
+ The meaning of these parameters may vary between different database engines.
84
+
85
+ :param engine: the database engine
86
+ :return: the current connection parameters for the engine
87
+ """
88
+ curr_engine: str = _S3_ENGINES[0] if not engine and _S3_ENGINES else engine
89
+ return _S3_ACCESS_DATA.get(engine or curr_engine)