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 ADDED
@@ -0,0 +1,4 @@
1
+ from .dataacceslayer import StorageClient
2
+ from .key_vault_client import KeyVaultClient
3
+ from .api_related import retry_request
4
+ __all__ = ["StorageClient", "KeyVaultClient", "retry_request"]
@@ -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,3 @@
1
+ from .storageclient import StorageClient
2
+
3
+ __all__ = ["StorageClient"]
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pmdb_utils = pmdb_utils:main