maleo-foundation 0.0.86__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.
Files changed (28) hide show
  1. maleo_foundation/clients/google/base.py +32 -0
  2. maleo_foundation/clients/google/cloud/base.py +32 -0
  3. maleo_foundation/clients/google/cloud/logging.py +20 -1
  4. maleo_foundation/clients/google/cloud/secret.py +58 -1
  5. maleo_foundation/clients/google/cloud/storage.py +35 -2
  6. maleo_foundation/clients/google/secret.py +61 -0
  7. maleo_foundation/clients/google/storage.py +57 -0
  8. maleo_foundation/db/engine.py +37 -1
  9. maleo_foundation/db/manager.py +111 -3
  10. maleo_foundation/db/session.py +48 -1
  11. maleo_foundation/enums.py +9 -0
  12. maleo_foundation/managers/__init__.py +0 -0
  13. maleo_foundation/managers/client/__init__.py +0 -0
  14. maleo_foundation/managers/client/base.py +33 -0
  15. maleo_foundation/managers/client/google/__init__.py +0 -0
  16. maleo_foundation/managers/client/google/base.py +26 -0
  17. maleo_foundation/managers/client/google/secret.py +65 -0
  18. maleo_foundation/managers/client/google/storage.py +60 -0
  19. maleo_foundation/managers/client/http.py +40 -0
  20. maleo_foundation/managers/db.py +123 -0
  21. maleo_foundation/managers/service.py +251 -0
  22. maleo_foundation/models/transfers/general/token.py +1 -1
  23. maleo_foundation/utils/logger.py +17 -14
  24. maleo_foundation/utils/logging.py +182 -0
  25. {maleo_foundation-0.0.86.dist-info → maleo_foundation-0.0.87.dist-info}/METADATA +1 -1
  26. {maleo_foundation-0.0.86.dist-info → maleo_foundation-0.0.87.dist-info}/RECORD +28 -13
  27. {maleo_foundation-0.0.86.dist-info → maleo_foundation-0.0.87.dist-info}/WHEEL +1 -1
  28. {maleo_foundation-0.0.86.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("GOOGLE_APPLICATION_CREDENTIALS")
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("GOOGLE_APPLICATION_CREDENTIALS")
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
@@ -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
@@ -1,6 +1,14 @@
1
- from sqlalchemy import Engine, MetaData
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
@@ -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