maleo-foundation 0.0.85__py3-none-any.whl → 0.0.87__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.
- maleo_foundation/clients/google/base.py +32 -0
- maleo_foundation/clients/google/cloud/base.py +32 -0
- maleo_foundation/clients/google/cloud/logging.py +20 -1
- maleo_foundation/clients/google/cloud/secret.py +58 -1
- maleo_foundation/clients/google/cloud/storage.py +35 -2
- maleo_foundation/clients/google/secret.py +61 -0
- maleo_foundation/clients/google/storage.py +57 -0
- maleo_foundation/db/engine.py +37 -1
- maleo_foundation/db/manager.py +111 -3
- maleo_foundation/db/session.py +48 -1
- maleo_foundation/enums.py +9 -0
- maleo_foundation/managers/__init__.py +0 -0
- maleo_foundation/managers/client/__init__.py +0 -0
- maleo_foundation/managers/client/base.py +33 -0
- maleo_foundation/managers/client/google/__init__.py +0 -0
- maleo_foundation/managers/client/google/base.py +26 -0
- maleo_foundation/managers/client/google/secret.py +65 -0
- maleo_foundation/managers/client/google/storage.py +60 -0
- maleo_foundation/managers/client/http.py +40 -0
- maleo_foundation/managers/db.py +123 -0
- maleo_foundation/managers/service.py +251 -0
- maleo_foundation/models/transfers/general/token.py +1 -1
- maleo_foundation/utils/keyloader.py +1 -1
- maleo_foundation/utils/logger.py +17 -14
- maleo_foundation/utils/logging.py +182 -0
- {maleo_foundation-0.0.85.dist-info → maleo_foundation-0.0.87.dist-info}/METADATA +1 -1
- {maleo_foundation-0.0.85.dist-info → maleo_foundation-0.0.87.dist-info}/RECORD +29 -14
- {maleo_foundation-0.0.85.dist-info → maleo_foundation-0.0.87.dist-info}/WHEEL +1 -1
- {maleo_foundation-0.0.85.dist-info → maleo_foundation-0.0.87.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from google.auth import default
|
|
3
|
+
from google.oauth2 import service_account
|
|
4
|
+
from maleo_foundation.types import BaseTypes
|
|
5
|
+
|
|
6
|
+
class GoogleClientManager:
|
|
7
|
+
def __init__(self, google_credentials_path:BaseTypes.OptionalString = None) -> None:
|
|
8
|
+
google_credentials_path = google_credentials_path or os.getenv("GOOGLE_CREDENTIALS_PATH")
|
|
9
|
+
try:
|
|
10
|
+
if google_credentials_path is not None:
|
|
11
|
+
self._credentials = service_account.Credentials.from_service_account_file(filename=google_credentials_path)
|
|
12
|
+
else:
|
|
13
|
+
self._credentials, _ = default()
|
|
14
|
+
except Exception as e:
|
|
15
|
+
raise ValueError(f"Failed to initialize credentials: {str(e)}")
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def credentials(self) -> service_account.Credentials:
|
|
19
|
+
return self._credentials
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
raise NotImplementedError()
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def client(self):
|
|
27
|
+
raise NotImplementedError()
|
|
28
|
+
|
|
29
|
+
def dispose(self) -> None:
|
|
30
|
+
"""Dispose of the client and release any resources."""
|
|
31
|
+
if self._credentials is not None:
|
|
32
|
+
self._credentials = None
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from google.auth import default
|
|
3
|
+
from google.oauth2 import service_account
|
|
4
|
+
from maleo_foundation.types import BaseTypes
|
|
5
|
+
|
|
6
|
+
class GoogleCloudClientManager:
|
|
7
|
+
def __init__(self, google_credentials_path:BaseTypes.OptionalString = None) -> None:
|
|
8
|
+
google_credentials_path = google_credentials_path or os.getenv("GOOGLE_CREDENTIALS_PATH")
|
|
9
|
+
try:
|
|
10
|
+
if google_credentials_path is not None:
|
|
11
|
+
self._credentials = service_account.Credentials.from_service_account_file(filename=google_credentials_path)
|
|
12
|
+
else:
|
|
13
|
+
self._credentials, _ = default()
|
|
14
|
+
except Exception as e:
|
|
15
|
+
raise ValueError(f"Failed to initialize credentials: {str(e)}")
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def credentials(self) -> service_account.Credentials:
|
|
19
|
+
return self._credentials
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
raise NotImplementedError()
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def client(self):
|
|
27
|
+
raise NotImplementedError()
|
|
28
|
+
|
|
29
|
+
def dispose(self) -> None:
|
|
30
|
+
"""Dispose of the client and release any resources."""
|
|
31
|
+
if self._credentials is not None:
|
|
32
|
+
self._credentials = None
|
|
@@ -4,6 +4,7 @@ from google.cloud.logging import Client
|
|
|
4
4
|
from google.cloud.logging.handlers import CloudLoggingHandler
|
|
5
5
|
from google.oauth2 import service_account
|
|
6
6
|
from typing import Optional
|
|
7
|
+
from .base import GoogleCloudClientManager
|
|
7
8
|
|
|
8
9
|
class GoogleCloudLogging:
|
|
9
10
|
_client:Optional[Client] = None
|
|
@@ -41,4 +42,22 @@ class GoogleCloudLogging:
|
|
|
41
42
|
@classmethod
|
|
42
43
|
def create_handler(cls, name:str):
|
|
43
44
|
cls.initialize()
|
|
44
|
-
return CloudLoggingHandler(client=cls._client, name=name)
|
|
45
|
+
return CloudLoggingHandler(client=cls._client, name=name)
|
|
46
|
+
|
|
47
|
+
class GoogleCloudLoggingV2(GoogleCloudClientManager):
|
|
48
|
+
def __init__(self, google_credentials_path = None) -> None:
|
|
49
|
+
super().__init__(google_credentials_path)
|
|
50
|
+
self._client = Client(credentials=self._credentials)
|
|
51
|
+
self._client.setup_logging()
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def client(self) -> Client:
|
|
55
|
+
return self._client
|
|
56
|
+
|
|
57
|
+
def dispose(self) -> None:
|
|
58
|
+
if self._client is not None:
|
|
59
|
+
self._client = None
|
|
60
|
+
return super().dispose()
|
|
61
|
+
|
|
62
|
+
def create_handler(self, name:str) -> CloudLoggingHandler:
|
|
63
|
+
return CloudLoggingHandler(client=self._client, name=name)
|
|
@@ -5,6 +5,7 @@ from google.auth import default
|
|
|
5
5
|
from google.cloud import secretmanager
|
|
6
6
|
from google.oauth2 import service_account
|
|
7
7
|
from typing import Optional
|
|
8
|
+
from .base import GoogleCloudClientManager
|
|
8
9
|
|
|
9
10
|
class GoogleSecretManager:
|
|
10
11
|
_project:Optional[str] = None
|
|
@@ -19,7 +20,7 @@ class GoogleSecretManager:
|
|
|
19
20
|
if cls._client is None:
|
|
20
21
|
#* Setup credentials with fallback chain
|
|
21
22
|
credentials = None
|
|
22
|
-
credentials_file = os.getenv("
|
|
23
|
+
credentials_file = os.getenv("GOOGLE_CREDENTIALS_PATH")
|
|
23
24
|
try:
|
|
24
25
|
if credentials_file:
|
|
25
26
|
credentials = service_account.Credentials.from_service_account_file(credentials_file)
|
|
@@ -88,5 +89,61 @@ class GoogleSecretManager:
|
|
|
88
89
|
request = secretmanager.AddSecretVersionRequest(parent=secret_path, payload=payload)
|
|
89
90
|
response = cls._client.add_secret_version(request=request)
|
|
90
91
|
return data
|
|
92
|
+
except Exception as e:
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
class GoogleSecretManagerV2(GoogleCloudClientManager):
|
|
96
|
+
def __init__(self, google_credentials_path = None) -> None:
|
|
97
|
+
super().__init__(google_credentials_path)
|
|
98
|
+
self._project_id = self.credentials.project_id
|
|
99
|
+
self._client = secretmanager.SecretManagerServiceClient(credentials=self.credentials)
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def project_id(self):
|
|
103
|
+
return self._project_id
|
|
104
|
+
|
|
105
|
+
@property
|
|
106
|
+
def client(self) -> secretmanager.SecretManagerServiceClient:
|
|
107
|
+
return self._client
|
|
108
|
+
|
|
109
|
+
def dispose(self):
|
|
110
|
+
if self._client is not None:
|
|
111
|
+
self._client = None
|
|
112
|
+
return super().dispose()
|
|
113
|
+
|
|
114
|
+
@retry.Retry(predicate=retry.if_exception_type(Exception), timeout=5)
|
|
115
|
+
def get(self, name:str, version:str = "latest") -> Optional[str]:
|
|
116
|
+
try:
|
|
117
|
+
secret_path = f"projects/{self._project_id}/secrets/{name}/versions/{version}"
|
|
118
|
+
request = secretmanager.AccessSecretVersionRequest(name=secret_path)
|
|
119
|
+
response = self._client.access_secret_version(request=request)
|
|
120
|
+
return response.payload.data.decode()
|
|
121
|
+
except Exception as e:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
@retry.Retry(predicate=retry.if_exception_type(Exception), timeout=5)
|
|
125
|
+
def create(self, name:str, data:str) -> Optional[str]:
|
|
126
|
+
parent = f"projects/{self._project_id}"
|
|
127
|
+
secret_path = f"{parent}/secrets/{name}"
|
|
128
|
+
try:
|
|
129
|
+
#* Check if the secret already exists
|
|
130
|
+
request = secretmanager.GetSecretRequest(name=secret_path)
|
|
131
|
+
self._client.get_secret(request=request)
|
|
132
|
+
|
|
133
|
+
except NotFound:
|
|
134
|
+
#* Secret does not exist, create it first
|
|
135
|
+
try:
|
|
136
|
+
secret = secretmanager.Secret(name=name, replication={"automatic": {}})
|
|
137
|
+
request = secretmanager.CreateSecretRequest(parent=parent, secret_id=name, secret=secret)
|
|
138
|
+
self._client.create_secret(request=request)
|
|
139
|
+
except Exception as e:
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
#* Add a new secret version
|
|
143
|
+
try:
|
|
144
|
+
payload = secretmanager.SecretPayload(data=data.encode()) # ✅ Fixed attribute name
|
|
145
|
+
request = secretmanager.AddSecretVersionRequest(parent=secret_path, payload=payload)
|
|
146
|
+
response = self._client.add_secret_version(request=request)
|
|
147
|
+
return data
|
|
91
148
|
except Exception as e:
|
|
92
149
|
return None
|
|
@@ -4,6 +4,8 @@ from google.auth import default
|
|
|
4
4
|
from google.cloud.storage import Bucket, Client
|
|
5
5
|
from google.oauth2 import service_account
|
|
6
6
|
from typing import Optional
|
|
7
|
+
from maleo_foundation.types import BaseTypes
|
|
8
|
+
from .base import GoogleCloudClientManager
|
|
7
9
|
|
|
8
10
|
class GoogleCloudStorage:
|
|
9
11
|
_client:Optional[Client] = None
|
|
@@ -15,7 +17,7 @@ class GoogleCloudStorage:
|
|
|
15
17
|
if cls._client is None:
|
|
16
18
|
#* Setup credentials with fallback chain
|
|
17
19
|
credentials = None
|
|
18
|
-
credentials_file = os.getenv("
|
|
20
|
+
credentials_file = os.getenv("GOOGLE_CREDENTIALS_PATH")
|
|
19
21
|
try:
|
|
20
22
|
if credentials_file:
|
|
21
23
|
credentials = service_account.Credentials.from_service_account_file(credentials_file)
|
|
@@ -74,4 +76,35 @@ class GoogleCloudStorage:
|
|
|
74
76
|
raise ValueError(f"File '{location}' did not exists.")
|
|
75
77
|
|
|
76
78
|
url = blob.generate_signed_url(version="v4", expiration=timedelta(minutes=15), method="GET")
|
|
77
|
-
return url
|
|
79
|
+
return url
|
|
80
|
+
|
|
81
|
+
class GoogleCloudStorageV2(GoogleCloudClientManager):
|
|
82
|
+
def __init__(self, google_credentials_path = None, bucket_name:BaseTypes.OptionalString = None) -> None:
|
|
83
|
+
super().__init__(google_credentials_path)
|
|
84
|
+
self._client = Client(credentials=self._credentials)
|
|
85
|
+
self._bucket_name = bucket_name or os.getenv("GCS_BUCKET_NAME")
|
|
86
|
+
if self._bucket_name is None:
|
|
87
|
+
self._client.close()
|
|
88
|
+
raise ValueError("GCS_BUCKET_NAME environment variable must be set if 'bucket_name' is set to None")
|
|
89
|
+
self._bucket = self._client.lookup_bucket(bucket_name=self._bucket_name)
|
|
90
|
+
if self._bucket is None:
|
|
91
|
+
raise ValueError(f"Bucket '{self._bucket_name}' does not exist.")
|
|
92
|
+
|
|
93
|
+
@property
|
|
94
|
+
def client(self) -> Client:
|
|
95
|
+
return self._client
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def bucket_name(self) -> str:
|
|
99
|
+
return self._bucket_name
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def bucket(self) -> Bucket:
|
|
103
|
+
return self._bucket
|
|
104
|
+
|
|
105
|
+
def dispose(self):
|
|
106
|
+
if self._bucket is not None:
|
|
107
|
+
self._bucket = None
|
|
108
|
+
if self._client is not None:
|
|
109
|
+
self._client = None
|
|
110
|
+
return super().dispose()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from google.api_core import retry
|
|
2
|
+
from google.api_core.exceptions import NotFound
|
|
3
|
+
from google.cloud import secretmanager
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from .base import GoogleClientManager
|
|
6
|
+
|
|
7
|
+
class GoogleSecretManager(GoogleClientManager):
|
|
8
|
+
def __init__(self, google_credentials_path = None) -> None:
|
|
9
|
+
super().__init__(google_credentials_path)
|
|
10
|
+
self._project_id = self.credentials.project_id
|
|
11
|
+
self._client = secretmanager.SecretManagerServiceClient(credentials=self.credentials)
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def project_id(self):
|
|
15
|
+
return self._project_id
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def client(self) -> secretmanager.SecretManagerServiceClient:
|
|
19
|
+
return self._client
|
|
20
|
+
|
|
21
|
+
def dispose(self):
|
|
22
|
+
if self._client is not None:
|
|
23
|
+
self._client = None
|
|
24
|
+
return super().dispose()
|
|
25
|
+
|
|
26
|
+
@retry.Retry(predicate=retry.if_exception_type(Exception), timeout=5)
|
|
27
|
+
def get(self, name:str, version:str = "latest") -> Optional[str]:
|
|
28
|
+
try:
|
|
29
|
+
secret_path = f"projects/{self._project_id}/secrets/{name}/versions/{version}"
|
|
30
|
+
request = secretmanager.AccessSecretVersionRequest(name=secret_path)
|
|
31
|
+
response = self._client.access_secret_version(request=request)
|
|
32
|
+
return response.payload.data.decode()
|
|
33
|
+
except Exception as e:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
@retry.Retry(predicate=retry.if_exception_type(Exception), timeout=5)
|
|
37
|
+
def create(self, name:str, data:str) -> Optional[str]:
|
|
38
|
+
parent = f"projects/{self._project_id}"
|
|
39
|
+
secret_path = f"{parent}/secrets/{name}"
|
|
40
|
+
try:
|
|
41
|
+
#* Check if the secret already exists
|
|
42
|
+
request = secretmanager.GetSecretRequest(name=secret_path)
|
|
43
|
+
self._client.get_secret(request=request)
|
|
44
|
+
|
|
45
|
+
except NotFound:
|
|
46
|
+
#* Secret does not exist, create it first
|
|
47
|
+
try:
|
|
48
|
+
secret = secretmanager.Secret(name=name, replication={"automatic": {}})
|
|
49
|
+
request = secretmanager.CreateSecretRequest(parent=parent, secret_id=name, secret=secret)
|
|
50
|
+
self._client.create_secret(request=request)
|
|
51
|
+
except Exception as e:
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
#* Add a new secret version
|
|
55
|
+
try:
|
|
56
|
+
payload = secretmanager.SecretPayload(data=data.encode())
|
|
57
|
+
request = secretmanager.AddSecretVersionRequest(parent=secret_path, payload=payload)
|
|
58
|
+
response = self._client.add_secret_version(request=request)
|
|
59
|
+
return data
|
|
60
|
+
except Exception as e:
|
|
61
|
+
return None
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from datetime import timedelta
|
|
3
|
+
from google.cloud.storage import Bucket, Client
|
|
4
|
+
from maleo_foundation.types import BaseTypes
|
|
5
|
+
from .base import GoogleClientManager
|
|
6
|
+
|
|
7
|
+
class GoogleCloudStorage(GoogleClientManager):
|
|
8
|
+
def __init__(self, google_credentials_path = None, bucket_name:BaseTypes.OptionalString = None) -> None:
|
|
9
|
+
super().__init__(google_credentials_path)
|
|
10
|
+
self._client = Client(credentials=self._credentials)
|
|
11
|
+
self._bucket_name = bucket_name or os.getenv("GCS_BUCKET_NAME")
|
|
12
|
+
if self._bucket_name is None:
|
|
13
|
+
self._client.close()
|
|
14
|
+
raise ValueError("GCS_BUCKET_NAME environment variable must be set if 'bucket_name' is set to None")
|
|
15
|
+
self._bucket = self._client.lookup_bucket(bucket_name=self._bucket_name)
|
|
16
|
+
if self._bucket is None:
|
|
17
|
+
raise ValueError(f"Bucket '{self._bucket_name}' does not exist.")
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def client(self) -> Client:
|
|
21
|
+
return self._client
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def bucket_name(self) -> str:
|
|
25
|
+
return self._bucket_name
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def bucket(self) -> Bucket:
|
|
29
|
+
return self._bucket
|
|
30
|
+
|
|
31
|
+
def dispose(self):
|
|
32
|
+
if self._bucket is not None:
|
|
33
|
+
self._bucket = None
|
|
34
|
+
if self._client is not None:
|
|
35
|
+
self._client = None
|
|
36
|
+
return super().dispose()
|
|
37
|
+
|
|
38
|
+
def generate_signed_url(self, location:str) -> str:
|
|
39
|
+
"""
|
|
40
|
+
generate signed URL of a file in the bucket based on its location.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
location: str
|
|
44
|
+
Location of the file inside the bucket
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str: File's pre-signed download url
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
ValueError: If the file does not exist
|
|
51
|
+
"""
|
|
52
|
+
blob = self._bucket.blob(blob_name=location)
|
|
53
|
+
if not blob.exists():
|
|
54
|
+
raise ValueError(f"File '{location}' did not exists.")
|
|
55
|
+
|
|
56
|
+
url = blob.generate_signed_url(version="v4", expiration=timedelta(minutes=15), method="GET")
|
|
57
|
+
return url
|
maleo_foundation/db/engine.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from sqlalchemy import create_engine, Engine
|
|
3
3
|
from typing import Optional
|
|
4
|
+
from maleo_foundation.types import BaseTypes
|
|
4
5
|
from maleo_foundation.utils.logger import BaseLogger
|
|
5
6
|
|
|
6
7
|
class EngineManager:
|
|
@@ -37,4 +38,39 @@ class EngineManager:
|
|
|
37
38
|
cls._engine = None
|
|
38
39
|
|
|
39
40
|
cls._logger.info("Engine disposed successfully.")
|
|
40
|
-
cls._logger = None
|
|
41
|
+
cls._logger = None
|
|
42
|
+
|
|
43
|
+
class EngineManagerV2:
|
|
44
|
+
_logger:Optional[BaseLogger] = None
|
|
45
|
+
_engine:Optional[Engine] = None
|
|
46
|
+
|
|
47
|
+
def __init__(self, logger:BaseLogger, url:BaseTypes.OptionalString = None):
|
|
48
|
+
"""Initialize the engine manager."""
|
|
49
|
+
self._logger = logger
|
|
50
|
+
url = url or os.getenv("DB_CONNECTION_STRING")
|
|
51
|
+
if url is None:
|
|
52
|
+
raise ValueError("DB_CONNECTION_STRING environment variable must be set if url is not provided")
|
|
53
|
+
self._engine = create_engine(url=url, echo=False, pool_pre_ping=True, pool_recycle=3600)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def logger(self) -> BaseLogger:
|
|
57
|
+
"""Retrieve the logger."""
|
|
58
|
+
if self._logger is None:
|
|
59
|
+
raise RuntimeError("Logger has not been initialized. Call initialize(db_connection_string, logger) first.")
|
|
60
|
+
return self._logger
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def engine(self) -> Engine:
|
|
64
|
+
"""Retrieve the engine."""
|
|
65
|
+
if self._engine is None:
|
|
66
|
+
raise RuntimeError("Engine has not been initialized. Call initialize(db_connection_string, logger) first.")
|
|
67
|
+
return self._engine
|
|
68
|
+
|
|
69
|
+
def dispose(self) -> None:
|
|
70
|
+
"""Dispose of the engine and release any resources."""
|
|
71
|
+
if self._engine is not None:
|
|
72
|
+
self._engine.dispose()
|
|
73
|
+
self._engine = None
|
|
74
|
+
|
|
75
|
+
self._logger.info("Engine disposed successfully.")
|
|
76
|
+
self._logger = None
|
maleo_foundation/db/manager.py
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
|
|
1
|
+
import os
|
|
2
|
+
from contextlib import contextmanager
|
|
3
|
+
from sqlalchemy import MetaData
|
|
4
|
+
from sqlalchemy.engine import Engine, create_engine
|
|
5
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
2
6
|
from sqlalchemy.ext.declarative import DeclarativeMeta
|
|
3
|
-
from sqlalchemy.orm import declarative_base
|
|
7
|
+
from sqlalchemy.orm import sessionmaker, Session, declarative_base
|
|
8
|
+
from typing import Generator, Optional, Type
|
|
9
|
+
from maleo_foundation.types import BaseTypes
|
|
10
|
+
from maleo_foundation.utils.logger import BaseLogger
|
|
11
|
+
from .table import BaseTable
|
|
4
12
|
|
|
5
13
|
class DatabaseManager:
|
|
6
14
|
Base:DeclarativeMeta = declarative_base() #* Correct way to define a declarative base
|
|
@@ -11,4 +19,104 @@ class DatabaseManager:
|
|
|
11
19
|
@classmethod
|
|
12
20
|
def initialize(cls, engine:Engine):
|
|
13
21
|
"""Creates the database tables if they do not exist."""
|
|
14
|
-
cls.metadata.create_all(engine)
|
|
22
|
+
cls.metadata.create_all(engine)
|
|
23
|
+
|
|
24
|
+
class MetadataManager:
|
|
25
|
+
_Base:DeclarativeMeta = declarative_base()
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def Base(self) -> DeclarativeMeta:
|
|
29
|
+
return self._Base
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def metadata(self) -> MetaData:
|
|
33
|
+
return self._Base.metadata
|
|
34
|
+
|
|
35
|
+
class SessionManager:
|
|
36
|
+
def __init__(self, logger:BaseLogger, engine:Engine):
|
|
37
|
+
self._logger = logger
|
|
38
|
+
self._sessionmaker:sessionmaker[Session] = sessionmaker(bind=engine, expire_on_commit=False)
|
|
39
|
+
self._logger.info("SessionManager initialized successfully.")
|
|
40
|
+
|
|
41
|
+
def _session_handler(self) -> Generator[Session, None, None]:
|
|
42
|
+
"""Reusable function for managing database sessions."""
|
|
43
|
+
if self._logger is None:
|
|
44
|
+
raise RuntimeError("Logger has not been initialized. Call initialize() first.")
|
|
45
|
+
if self._sessionmaker is None:
|
|
46
|
+
raise RuntimeError("SessionLocal has not been initialized. Call initialize() first.")
|
|
47
|
+
|
|
48
|
+
session = self._sessionmaker()
|
|
49
|
+
self._logger.debug("New database session created.")
|
|
50
|
+
try:
|
|
51
|
+
yield session #* Provide session
|
|
52
|
+
session.commit() #* Auto-commit on success
|
|
53
|
+
except SQLAlchemyError as e:
|
|
54
|
+
session.rollback() #* Rollback on error
|
|
55
|
+
self._logger.error(f"[SQLAlchemyError] Database transaction failed: {e}", exc_info=True)
|
|
56
|
+
raise
|
|
57
|
+
except Exception as e:
|
|
58
|
+
session.rollback() #* Rollback on error
|
|
59
|
+
self._logger.error(f"[Exception] Database transaction failed: {e}", exc_info=True)
|
|
60
|
+
raise
|
|
61
|
+
finally:
|
|
62
|
+
session.close() #* Ensure session closes
|
|
63
|
+
self._logger.debug("Database session closed.")
|
|
64
|
+
|
|
65
|
+
def inject(self) -> Generator[Session, None, None]:
|
|
66
|
+
"""Returns a generator that yields a SQLAlchemy session for dependency injection."""
|
|
67
|
+
return self._session_handler()
|
|
68
|
+
|
|
69
|
+
@contextmanager
|
|
70
|
+
def get(self) -> Generator[Session, None, None]:
|
|
71
|
+
"""Context manager for manual session handling. Supports `with SessionManager.get() as session:`"""
|
|
72
|
+
yield from self._session_handler()
|
|
73
|
+
|
|
74
|
+
def dispose(self) -> None:
|
|
75
|
+
"""Dispose of the sessionmaker and release any resources."""
|
|
76
|
+
if self._sessionmaker is not None:
|
|
77
|
+
self._sessionmaker.close_all()
|
|
78
|
+
self._sessionmaker = None
|
|
79
|
+
|
|
80
|
+
self._logger.info("SessionManager disposed successfully.")
|
|
81
|
+
self._logger = None
|
|
82
|
+
|
|
83
|
+
class DatabaseManagerV2:
|
|
84
|
+
# _metadata:Optional[MetaData] = None
|
|
85
|
+
# _logger:Optional[BaseLogger] = None
|
|
86
|
+
# _engine:Optional[Engine] = None
|
|
87
|
+
# _session:Optional[SessionManager] = None
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
metadata:MetaData,
|
|
92
|
+
logger:BaseLogger,
|
|
93
|
+
url:BaseTypes.OptionalString = None
|
|
94
|
+
):
|
|
95
|
+
self._metadata = metadata #* Define database metadata
|
|
96
|
+
self._logger = logger #* Define database logger
|
|
97
|
+
|
|
98
|
+
#* Create engine
|
|
99
|
+
url = url or os.getenv("DB_URL")
|
|
100
|
+
if url is None:
|
|
101
|
+
raise ValueError("DB_URL environment variable must be set if url is not provided")
|
|
102
|
+
self._engine = create_engine(url=url, echo=False, pool_pre_ping=True, pool_recycle=3600)
|
|
103
|
+
|
|
104
|
+
self._metadata.create_all(bind=self._engine) #* Create all tables
|
|
105
|
+
self._session = SessionManager(logger=self._logger, engine=self._engine) #* Define session
|
|
106
|
+
|
|
107
|
+
def dispose(self) -> None:
|
|
108
|
+
#* Dispose session
|
|
109
|
+
if self._session is not None:
|
|
110
|
+
self._session.dispose()
|
|
111
|
+
self._session = None
|
|
112
|
+
#* Dispose engine
|
|
113
|
+
if self._engine is not None:
|
|
114
|
+
self._engine.dispose()
|
|
115
|
+
self._engine = None
|
|
116
|
+
#* Dispose logger
|
|
117
|
+
if self._logger is not None:
|
|
118
|
+
self._logger.dispose()
|
|
119
|
+
self._logger = None
|
|
120
|
+
#* Dispose metadata
|
|
121
|
+
if self._metadata is not None:
|
|
122
|
+
self._metadata = None
|
maleo_foundation/db/session.py
CHANGED
|
@@ -61,4 +61,51 @@ class SessionManager:
|
|
|
61
61
|
cls._sessionmaker = None
|
|
62
62
|
|
|
63
63
|
cls._logger.info("SessionManager disposed successfully.")
|
|
64
|
-
cls._logger = None
|
|
64
|
+
cls._logger = None
|
|
65
|
+
|
|
66
|
+
class SessionManagerV2:
|
|
67
|
+
def __init__(self, logger:BaseLogger, sessionmaker:sessionmaker[Session]):
|
|
68
|
+
self._logger = logger
|
|
69
|
+
self._sessionmaker = sessionmaker
|
|
70
|
+
|
|
71
|
+
def _session_handler(self) -> Generator[Session, None, None]:
|
|
72
|
+
"""Reusable function for managing database sessions."""
|
|
73
|
+
if self._logger is None:
|
|
74
|
+
raise RuntimeError("Logger has not been initialized. Call initialize() first.")
|
|
75
|
+
if self._sessionmaker is None:
|
|
76
|
+
raise RuntimeError("SessionLocal has not been initialized. Call initialize() first.")
|
|
77
|
+
|
|
78
|
+
session = self._sessionmaker()
|
|
79
|
+
self._logger.debug("New database session created.")
|
|
80
|
+
try:
|
|
81
|
+
yield session #* Provide session
|
|
82
|
+
session.commit() #* Auto-commit on success
|
|
83
|
+
except SQLAlchemyError as e:
|
|
84
|
+
session.rollback() #* Rollback on error
|
|
85
|
+
self._logger.error(f"[SQLAlchemyError] Database transaction failed: {e}", exc_info=True)
|
|
86
|
+
raise
|
|
87
|
+
except Exception as e:
|
|
88
|
+
session.rollback() #* Rollback on error
|
|
89
|
+
self._logger.error(f"[Exception] Database transaction failed: {e}", exc_info=True)
|
|
90
|
+
raise
|
|
91
|
+
finally:
|
|
92
|
+
session.close() #* Ensure session closes
|
|
93
|
+
self._logger.debug("Database session closed.")
|
|
94
|
+
|
|
95
|
+
def inject(self) -> Generator[Session, None, None]:
|
|
96
|
+
"""Returns a generator that yields a SQLAlchemy session for dependency injection."""
|
|
97
|
+
return self._session_handler()
|
|
98
|
+
|
|
99
|
+
@contextmanager
|
|
100
|
+
def get(self) -> Generator[Session, None, None]:
|
|
101
|
+
"""Context manager for manual session handling. Supports `with SessionManager.get() as session:`"""
|
|
102
|
+
yield from self._session_handler()
|
|
103
|
+
|
|
104
|
+
def dispose(self) -> None:
|
|
105
|
+
"""Dispose of the sessionmaker and release any resources."""
|
|
106
|
+
if self._sessionmaker is not None:
|
|
107
|
+
self._sessionmaker.close_all()
|
|
108
|
+
self._sessionmaker = None
|
|
109
|
+
|
|
110
|
+
self._logger.info("SessionManager disposed successfully.")
|
|
111
|
+
self._logger = None
|
maleo_foundation/enums.py
CHANGED
|
@@ -40,6 +40,10 @@ class BaseEnums:
|
|
|
40
40
|
class ClientControllerType(StrEnum):
|
|
41
41
|
HTTP = "http"
|
|
42
42
|
|
|
43
|
+
class ClientCategory(StrEnum):
|
|
44
|
+
GOOGLE = "google"
|
|
45
|
+
MALEO = "maleo"
|
|
46
|
+
|
|
43
47
|
class KeyType(StrEnum):
|
|
44
48
|
PRIVATE = "private"
|
|
45
49
|
PUBLIC = "public"
|
|
@@ -69,6 +73,11 @@ class BaseEnums:
|
|
|
69
73
|
BaseEnums.RESTControllerResponseType.FILE: responses.FileResponse,
|
|
70
74
|
}.get(self, responses.Response)
|
|
71
75
|
|
|
76
|
+
class ServiceLoggerType(StrEnum):
|
|
77
|
+
MIDDLEWARE = "middleware"
|
|
78
|
+
DATABASE = "database"
|
|
79
|
+
APPLICATION = "application"
|
|
80
|
+
|
|
72
81
|
class LoggerType(StrEnum):
|
|
73
82
|
MIDDLEWARE = "middleware"
|
|
74
83
|
DATABASE = "database"
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
from maleo_foundation.utils.logging import GoogleCloudLogging, ClientLogger
|
|
3
|
+
|
|
4
|
+
class ClientManager:
|
|
5
|
+
def __init__(self, key:str, name:str, logs_dir:str, google_cloud_logging:Optional[GoogleCloudLogging] = None) -> None:
|
|
6
|
+
self._key = key
|
|
7
|
+
self._name = name
|
|
8
|
+
self._logs_dir = logs_dir
|
|
9
|
+
self._google_cloud_logging = google_cloud_logging
|
|
10
|
+
self._initialize_logger()
|
|
11
|
+
|
|
12
|
+
def _initialize_logger(self) -> None:
|
|
13
|
+
self._logger = ClientLogger(logs_dir=self._logs_dir, client_key=self._key, google_cloud_logging=self._google_cloud_logging)
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def key(self) -> str:
|
|
17
|
+
return self._key
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def name(self) -> str:
|
|
21
|
+
return self._name
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def logger(self):
|
|
25
|
+
return self._logger
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def credentials(self):
|
|
29
|
+
raise NotImplementedError()
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def client(self):
|
|
33
|
+
raise NotImplementedError()
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from google.auth import default
|
|
3
|
+
from google.oauth2 import service_account
|
|
4
|
+
from maleo_foundation.managers.client.base import ClientManager
|
|
5
|
+
|
|
6
|
+
class GoogleClientManager(ClientManager):
|
|
7
|
+
def __init__(self, key, name, logs_dir, google_cloud_logging=None, credentials_path=None) -> None:
|
|
8
|
+
super().__init__(key, name, logs_dir, google_cloud_logging)
|
|
9
|
+
credentials_path = credentials_path or os.getenv("GOOGLE_CREDENTIALS_PATH")
|
|
10
|
+
try:
|
|
11
|
+
if credentials_path is not None:
|
|
12
|
+
self._credentials = service_account.Credentials.from_service_account_file(filename=credentials_path)
|
|
13
|
+
else:
|
|
14
|
+
self._credentials, _ = default()
|
|
15
|
+
except Exception as e:
|
|
16
|
+
raise ValueError(f"Failed to initialize credentials: {str(e)}")
|
|
17
|
+
|
|
18
|
+
self._project_id = self._credentials.project_id
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def credentials(self) -> service_account.Credentials:
|
|
22
|
+
return self._credentials
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def project_id(self) -> str:
|
|
26
|
+
return self._project_id
|