pmdb_utils 0.3.0__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.
- pmdb_utils/__init__.py +4 -0
- pmdb_utils/api_related.py +64 -0
- pmdb_utils/dataacceslayer/__init__.py +3 -0
- pmdb_utils/dataacceslayer/storageclient.py +97 -0
- pmdb_utils/key_vault_client.py +45 -0
- pmdb_utils-0.3.0.dist-info/METADATA +9 -0
- pmdb_utils-0.3.0.dist-info/RECORD +9 -0
- pmdb_utils-0.3.0.dist-info/WHEEL +4 -0
- pmdb_utils-0.3.0.dist-info/entry_points.txt +2 -0
pmdb_utils/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# pip3 install requests
|
|
2
|
+
import requests
|
|
3
|
+
import time
|
|
4
|
+
def retry_request(
|
|
5
|
+
method="GET",
|
|
6
|
+
url=None,
|
|
7
|
+
status_forcelist=[],
|
|
8
|
+
total=3,
|
|
9
|
+
delay=10,
|
|
10
|
+
**kwargs,
|
|
11
|
+
):
|
|
12
|
+
"""
|
|
13
|
+
Sends an HTTP request with retry logic for specified status codes or connection errors.
|
|
14
|
+
|
|
15
|
+
Parameters:
|
|
16
|
+
method (str): The HTTP method to use (e.g., 'GET', 'POST'). Default is 'GET'.
|
|
17
|
+
url (str): The URL to send the request to. Default is None.
|
|
18
|
+
status_forcelist (list): A list of HTTP status codes that should trigger a retry. Default is an empty list.
|
|
19
|
+
total (int): The total number of retry attempts. Default is 3.
|
|
20
|
+
delay (int): The delay (in seconds) between retries. Default is 10.
|
|
21
|
+
**kwargs: Additional arguments to pass to the `requests.request` method.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
response (requests.Response): The final response object if successful.
|
|
25
|
+
last_response (requests.Response): The last response object after retries if the request fails.
|
|
26
|
+
|
|
27
|
+
Notes:
|
|
28
|
+
- If a `requests.exceptions.ConnectionError` occurs, the function will retry.
|
|
29
|
+
- If the response status code is in `status_forcelist`, the function will retry after waiting for `delay` seconds.
|
|
30
|
+
- After exhausting all retries, the last response object is returned.
|
|
31
|
+
- Inspired by https://www.zenrows.com/blog/python-requests-retry#code-your-retry-wrapper
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
response = retry_request(
|
|
35
|
+
method="GET",
|
|
36
|
+
url="https://httpstat.us/406",
|
|
37
|
+
total=5,
|
|
38
|
+
delay=2,
|
|
39
|
+
status_forcelist=[406]
|
|
40
|
+
)
|
|
41
|
+
"""
|
|
42
|
+
# Store the last response in an empty variable
|
|
43
|
+
last_response = None
|
|
44
|
+
|
|
45
|
+
# Implement retry logic
|
|
46
|
+
for _ in range(total):
|
|
47
|
+
try:
|
|
48
|
+
response = requests.request(method, url, **kwargs)
|
|
49
|
+
if response.status_code in status_forcelist:
|
|
50
|
+
# Track the last response
|
|
51
|
+
last_response = response
|
|
52
|
+
print(f" Got status code {response.status_code} for url {url} will retry in {delay} seconds retry number {_+1}/{total}")
|
|
53
|
+
time.sleep(delay)
|
|
54
|
+
# Retry request
|
|
55
|
+
continue
|
|
56
|
+
else:
|
|
57
|
+
return response
|
|
58
|
+
|
|
59
|
+
except requests.exceptions.ConnectionError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
# Log the response after the retry
|
|
63
|
+
return last_response
|
|
64
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from urllib.parse import urlparse
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from typing import Mapping, Optional
|
|
6
|
+
from minio import Minio
|
|
7
|
+
from minio.commonconfig import CopySource
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class StorageClient:
|
|
11
|
+
"""
|
|
12
|
+
Storage client to read/write tabular data to/from storage backends.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
file_path : str, optional
|
|
17
|
+
Default path to read/write if none is provided to .read()/.write().
|
|
18
|
+
file_type : str | None
|
|
19
|
+
Explicit type like 'delta', 'parquet', 'csv'. If None, inferred from file extension.
|
|
20
|
+
storage_options : Mapping[str, str] | None
|
|
21
|
+
Extra options for remote storage backends (e.g., S3 credentials).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
file_path: Optional[str] = None,
|
|
27
|
+
file_type: Optional[str] = None,
|
|
28
|
+
storage_options: Optional[Mapping[str, str]] = None,
|
|
29
|
+
):
|
|
30
|
+
self.file_path = file_path
|
|
31
|
+
self.file_type = file_type.lower() if file_type else None
|
|
32
|
+
self.storage_options = storage_options
|
|
33
|
+
self.client = None
|
|
34
|
+
self._set_client()
|
|
35
|
+
|
|
36
|
+
# ---------------------------
|
|
37
|
+
# client_config
|
|
38
|
+
# ---------------------------
|
|
39
|
+
def _set_client(self):
|
|
40
|
+
# TODO: Must be a better way to do this to handle different venders
|
|
41
|
+
endpoint_url = self.storage_options[
|
|
42
|
+
"endpoint_url"
|
|
43
|
+
] # e.g. "https://minio.mycorp.local:9000"
|
|
44
|
+
parsed = urlparse(endpoint_url)
|
|
45
|
+
|
|
46
|
+
self.client = Minio(
|
|
47
|
+
parsed.netloc,
|
|
48
|
+
access_key=self.storage_options["aws_access_key_id"],
|
|
49
|
+
secret_key=self.storage_options["aws_secret_access_key"],
|
|
50
|
+
secure=parsed.scheme == "https",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
def fput_object(self, bucket_name: str, object_name: str, file_path: str, **kwargs):
|
|
54
|
+
return self.client.fput_object(bucket_name, object_name, file_path, **kwargs)
|
|
55
|
+
|
|
56
|
+
def list_objects(self, bucket_name: str, prefix: str, recursive: bool):
|
|
57
|
+
return self.client.list_objects(bucket_name, prefix, recursive)
|
|
58
|
+
|
|
59
|
+
def get_object(self, bucket_name: str, object_name: str, **kwargs):
|
|
60
|
+
return self.client.get_object(
|
|
61
|
+
bucket_name=bucket_name, object_name=object_name, **kwargs
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def remove_object(self, bucket_name: str, object_name: str, **kwargs):
|
|
65
|
+
return self.client.remove_object(
|
|
66
|
+
bucket_name=bucket_name, object_name=object_name, **kwargs
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def move_object(
|
|
70
|
+
self, bucket_name: str, object_name: str, new_object_name: str, **kwargs
|
|
71
|
+
):
|
|
72
|
+
source = CopySource(bucket_name, object_name)
|
|
73
|
+
self.client.copy_object(
|
|
74
|
+
bucket_name,
|
|
75
|
+
new_object_name,
|
|
76
|
+
source,
|
|
77
|
+
)
|
|
78
|
+
self.client.remove_object(bucket_name, object_name)
|
|
79
|
+
|
|
80
|
+
def move_folder(
|
|
81
|
+
self, bucket_name: str, folder_name: str, new_folder_name: str, **kwargs
|
|
82
|
+
):
|
|
83
|
+
objects = self.client.list_objects(
|
|
84
|
+
bucket_name, prefix=folder_name, recursive=True
|
|
85
|
+
)
|
|
86
|
+
for obj in objects:
|
|
87
|
+
new_object_name = obj.object_name.replace(folder_name, new_folder_name, 1)
|
|
88
|
+
# Skip if source and destination are the same
|
|
89
|
+
if obj.object_name == new_object_name:
|
|
90
|
+
continue
|
|
91
|
+
source = CopySource(bucket_name, obj.object_name)
|
|
92
|
+
self.client.copy_object(
|
|
93
|
+
bucket_name,
|
|
94
|
+
new_object_name,
|
|
95
|
+
source,
|
|
96
|
+
)
|
|
97
|
+
self.client.remove_object(bucket_name, obj.object_name)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from azure.identity import ClientSecretCredential
|
|
2
|
+
from azure.keyvault.secrets import SecretClient
|
|
3
|
+
class KeyVaultClient:
|
|
4
|
+
def __init__(self, tenant_id: str=None, client_id: str=None, client_secret: str=None, key_vault_name: str='kv-pi-001'):
|
|
5
|
+
"""
|
|
6
|
+
Initialize the KeyVaultClient with Azure credentials and Key Vault URL.
|
|
7
|
+
|
|
8
|
+
:param tenant_id: Azure Tenant ID
|
|
9
|
+
:param client_id: Service Principal Client ID
|
|
10
|
+
:param client_secret: Service Principal Client Secret
|
|
11
|
+
:param key_vault_url: The URL of the Azure Key Vault
|
|
12
|
+
"""
|
|
13
|
+
# If user didn't pass any of the credentials, retrieve from environment
|
|
14
|
+
if not all([tenant_id, client_id, client_secret]):
|
|
15
|
+
tenant_id = tenant_id or os.getenv("AZURE_SPN_PI01_TENANT_ID")
|
|
16
|
+
client_id = client_id or os.getenv("AZURE_SPN_PI01_CLIENT_ID")
|
|
17
|
+
client_secret = client_secret or os.getenv("AZURE_SPN_PI01_CLIENT_SECRET")
|
|
18
|
+
if not all([tenant_id, client_id, client_secret]):
|
|
19
|
+
raise ValueError("Azure credentials must be provided.")
|
|
20
|
+
|
|
21
|
+
self.tenant_id = tenant_id
|
|
22
|
+
self.client_id = client_id
|
|
23
|
+
self.client_secret = client_secret
|
|
24
|
+
|
|
25
|
+
self.credential = ClientSecretCredential(
|
|
26
|
+
tenant_id=tenant_id,
|
|
27
|
+
client_id=client_id,
|
|
28
|
+
client_secret=client_secret
|
|
29
|
+
)
|
|
30
|
+
self.key_vault_url = key_vault_name
|
|
31
|
+
self.secret_client = SecretClient(vault_url=f'https://{key_vault_name}.vault.azure.net/', credential=self.credential)
|
|
32
|
+
|
|
33
|
+
def get_secret(self, secret_name: str):
|
|
34
|
+
"""
|
|
35
|
+
Fetch a secret from the Azure Key Vault.
|
|
36
|
+
|
|
37
|
+
:param secret_name: Name of the secret to fetch
|
|
38
|
+
:return: The value of the secret
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
secret = self.secret_client.get_secret(secret_name)
|
|
42
|
+
return secret.value
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"Error fetching secret '{secret_name}': {e}")
|
|
45
|
+
return None
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pmdb_utils
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Author-email: saebod <saebod@hotmail.com>
|
|
6
|
+
Requires-Python: >=3.13
|
|
7
|
+
Requires-Dist: azure-identity>=1.25.0
|
|
8
|
+
Requires-Dist: azure-keyvault>=4.2.0
|
|
9
|
+
Requires-Dist: minio>=7.2.16
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pmdb_utils/__init__.py,sha256=SV0armqoGpYok01i371MKRiYO_TQAqFeAhVkmFyH12E,188
|
|
2
|
+
pmdb_utils/api_related.py,sha256=I4VLFeVwWPnRtZazzSANMv42CvrMs4exzUsVuJecF0I,2306
|
|
3
|
+
pmdb_utils/key_vault_client.py,sha256=EmoibCEyrKbbkpceoEwKCT7EotVROkGJZXH5gtqFjJ8,1974
|
|
4
|
+
pmdb_utils/dataacceslayer/__init__.py,sha256=swMCCnFr67XvNKgeNAf4y_aGImpPXNdRByaijQpsNsI,69
|
|
5
|
+
pmdb_utils/dataacceslayer/storageclient.py,sha256=576-huGqzKq_Lqv9P3vbvg-3rownxyojjJQ70IKXOKo,3450
|
|
6
|
+
pmdb_utils-0.3.0.dist-info/METADATA,sha256=q_6GJ4REGFdQFuubjrsk0J9Ir_JsiOPuASyx9Qn07vY,259
|
|
7
|
+
pmdb_utils-0.3.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8
|
+
pmdb_utils-0.3.0.dist-info/entry_points.txt,sha256=bpYoW85JVYVja20kUmbSTBbxepoQLSQ4BSvE2fznyz4,47
|
|
9
|
+
pmdb_utils-0.3.0.dist-info/RECORD,,
|