maleo-foundation 0.0.1__py3-none-any.whl → 0.0.2__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/__init__.py +0 -0
- maleo_foundation/clients/__init__.py +7 -0
- maleo_foundation/clients/general/__init__.py +4 -0
- maleo_foundation/clients/general/http.py +41 -0
- maleo_foundation/clients/google/__init__.py +4 -0
- maleo_foundation/clients/google/cloud/__init__.py +8 -0
- maleo_foundation/clients/google/cloud/logging.py +45 -0
- maleo_foundation/clients/google/cloud/secret.py +92 -0
- maleo_foundation/clients/google/cloud/storage.py +77 -0
- maleo_foundation/constants.py +8 -0
- maleo_foundation/controller.py +50 -0
- maleo_foundation/db/__init__.py +3 -0
- maleo_foundation/db/database.py +41 -0
- maleo_foundation/db/engine.py +40 -0
- maleo_foundation/db/session.py +64 -0
- maleo_foundation/models/__init__.py +13 -0
- maleo_foundation/models/enums.py +66 -0
- maleo_foundation/models/responses.py +99 -0
- maleo_foundation/models/schemas/__init__.py +9 -0
- maleo_foundation/models/schemas/general.py +105 -0
- maleo_foundation/models/schemas/parameter.py +16 -0
- maleo_foundation/models/schemas/result.py +89 -0
- maleo_foundation/models/transfers/__init__.py +7 -0
- maleo_foundation/models/transfers/parameters/__init__.py +9 -0
- maleo_foundation/models/transfers/parameters/client.py +65 -0
- maleo_foundation/models/transfers/parameters/general.py +13 -0
- maleo_foundation/models/transfers/parameters/service.py +121 -0
- maleo_foundation/models/transfers/results/__init__.py +7 -0
- maleo_foundation/models/transfers/results/client/__init__.py +7 -0
- maleo_foundation/models/transfers/results/client/controllers/__init__.py +5 -0
- maleo_foundation/models/transfers/results/client/controllers/http.py +35 -0
- maleo_foundation/models/transfers/results/client/service.py +27 -0
- maleo_foundation/models/transfers/results/service/__init__.py +9 -0
- maleo_foundation/models/transfers/results/service/controllers/__init__.py +5 -0
- maleo_foundation/models/transfers/results/service/controllers/rest.py +22 -0
- maleo_foundation/models/transfers/results/service/general.py +38 -0
- maleo_foundation/models/transfers/results/service/query.py +42 -0
- maleo_foundation/models/types/__init__.py +9 -0
- maleo_foundation/models/types/client.py +40 -0
- maleo_foundation/models/types/query.py +40 -0
- maleo_foundation/models/types/service.py +40 -0
- maleo_foundation/utils/__init__.py +9 -0
- maleo_foundation/utils/exceptions.py +74 -0
- maleo_foundation/utils/formatter/__init__.py +4 -0
- maleo_foundation/utils/formatter/case.py +37 -0
- maleo_foundation/utils/logger.py +68 -0
- {maleo_foundation-0.0.1.dist-info → maleo_foundation-0.0.2.dist-info}/METADATA +1 -1
- maleo_foundation-0.0.2.dist-info/RECORD +50 -0
- maleo_foundation-0.0.2.dist-info/top_level.txt +1 -0
- maleo_foundation-0.0.1.dist-info/RECORD +0 -4
- maleo_foundation-0.0.1.dist-info/top_level.txt +0 -1
- {maleo_foundation-0.0.1.dist-info → maleo_foundation-0.0.2.dist-info}/WHEEL +0 -0
File without changes
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import httpx
|
2
|
+
from contextlib import asynccontextmanager
|
3
|
+
from typing import AsyncGenerator, Optional
|
4
|
+
|
5
|
+
class HTTPClientManager:
|
6
|
+
_client:Optional[httpx.AsyncClient] = None
|
7
|
+
|
8
|
+
@classmethod
|
9
|
+
def initialize(cls) -> None:
|
10
|
+
"""Initialize the HTTP client if not already initialized."""
|
11
|
+
if cls._client is None:
|
12
|
+
cls._client = httpx.AsyncClient()
|
13
|
+
|
14
|
+
@classmethod
|
15
|
+
async def _client_handler(cls) -> AsyncGenerator[httpx.AsyncClient, None]:
|
16
|
+
"""Reusable generator for client handling."""
|
17
|
+
if cls._client is None:
|
18
|
+
raise RuntimeError("Client has not been initialized. Call initialize first.")
|
19
|
+
|
20
|
+
yield cls._client
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
async def inject(cls) -> AsyncGenerator[httpx.AsyncClient, None]:
|
24
|
+
return cls._client_handler()
|
25
|
+
|
26
|
+
@classmethod
|
27
|
+
@asynccontextmanager
|
28
|
+
async def get(cls) -> AsyncGenerator[httpx.AsyncClient, None]:
|
29
|
+
"""
|
30
|
+
Async context manager for manual HTTP client handling.
|
31
|
+
Supports `async with HTTPClientManager.get() as client:`
|
32
|
+
"""
|
33
|
+
async for client in cls._client_handler():
|
34
|
+
yield client
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
async def dispose(cls) -> None:
|
38
|
+
"""Dispose of the HTTP client and release any resources."""
|
39
|
+
if cls._client is not None:
|
40
|
+
await cls._client.aclose()
|
41
|
+
cls._client = None
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import os
|
2
|
+
from google.auth import default
|
3
|
+
from google.cloud.logging import Client
|
4
|
+
from google.cloud.logging.handlers import CloudLoggingHandler
|
5
|
+
from google.oauth2 import service_account
|
6
|
+
from typing import Optional
|
7
|
+
|
8
|
+
class GoogleCloudLogging:
|
9
|
+
_client:Optional[Client] = None
|
10
|
+
|
11
|
+
@classmethod
|
12
|
+
def initialize(cls) -> Client:
|
13
|
+
"""Initialize the cloud logging if not already initialized."""
|
14
|
+
if cls._client is None:
|
15
|
+
#* Setup credentials with fallback chain
|
16
|
+
credentials = None
|
17
|
+
credentials_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
|
18
|
+
try:
|
19
|
+
if credentials_file:
|
20
|
+
credentials = service_account.Credentials.from_service_account_file(credentials_file)
|
21
|
+
else:
|
22
|
+
credentials, _ = default()
|
23
|
+
except Exception as e:
|
24
|
+
raise ValueError(f"Failed to initialize credentials: {str(e)}")
|
25
|
+
|
26
|
+
cls._client = Client(credentials=credentials)
|
27
|
+
cls._client.setup_logging()
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def dispose(cls) -> None:
|
31
|
+
"""Dispose of the cloud logging and release any resources."""
|
32
|
+
if cls._client is not None:
|
33
|
+
cls._client.close()
|
34
|
+
cls._client = None
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
def _get_client(cls) -> Client:
|
38
|
+
"""Retrieve the cloud logging client, initializing it if necessary."""
|
39
|
+
cls.initialize()
|
40
|
+
return cls._client
|
41
|
+
|
42
|
+
@classmethod
|
43
|
+
def create_handler(cls, name:str):
|
44
|
+
cls.initialize()
|
45
|
+
return CloudLoggingHandler(client=cls._client, name=name)
|
@@ -0,0 +1,92 @@
|
|
1
|
+
import os
|
2
|
+
from google.api_core import retry
|
3
|
+
from google.api_core.exceptions import NotFound
|
4
|
+
from google.auth import default
|
5
|
+
from google.cloud import secretmanager
|
6
|
+
from google.oauth2 import service_account
|
7
|
+
from typing import Optional
|
8
|
+
|
9
|
+
class GoogleSecretManager:
|
10
|
+
_project:Optional[str] = None
|
11
|
+
_client:Optional[secretmanager.SecretManagerServiceClient] = None
|
12
|
+
|
13
|
+
@classmethod
|
14
|
+
def initialize(cls, project_id:Optional[str] = None) -> None:
|
15
|
+
"""Initialize the cloud storage if not already initialized."""
|
16
|
+
cls._project = project_id or os.getenv("GCP_PROJECT_ID")
|
17
|
+
if cls._project is None:
|
18
|
+
raise ValueError("GCP_PROJECT_ID environment variable must be set if no project_id is provided")
|
19
|
+
if cls._client is None:
|
20
|
+
#* Setup credentials with fallback chain
|
21
|
+
credentials = None
|
22
|
+
credentials_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
|
23
|
+
try:
|
24
|
+
if credentials_file:
|
25
|
+
credentials = service_account.Credentials.from_service_account_file(credentials_file)
|
26
|
+
else:
|
27
|
+
credentials, _ = default()
|
28
|
+
except Exception as e:
|
29
|
+
raise ValueError(f"Failed to initialize credentials: {str(e)}")
|
30
|
+
|
31
|
+
cls._client = secretmanager.SecretManagerServiceClient(credentials=credentials)
|
32
|
+
|
33
|
+
@classmethod
|
34
|
+
def dispose(cls):
|
35
|
+
"""Disposes of the Google Secret Manager client"""
|
36
|
+
if cls._client is not None:
|
37
|
+
cls._client = None
|
38
|
+
if cls._project is not None:
|
39
|
+
cls._project = None
|
40
|
+
|
41
|
+
@classmethod
|
42
|
+
@retry.Retry(
|
43
|
+
predicate=retry.if_exception_type(Exception),
|
44
|
+
timeout=5
|
45
|
+
)
|
46
|
+
def get(
|
47
|
+
cls,
|
48
|
+
name:str,
|
49
|
+
version:str = "latest",
|
50
|
+
) -> Optional[str]:
|
51
|
+
try:
|
52
|
+
secret_path = f"projects/{cls._project}/secrets/{name}/versions/{version}"
|
53
|
+
request = secretmanager.AccessSecretVersionRequest(name=secret_path)
|
54
|
+
response = cls._client.access_secret_version(request=request)
|
55
|
+
return response.payload.data.decode()
|
56
|
+
except Exception as e:
|
57
|
+
return None
|
58
|
+
|
59
|
+
@classmethod
|
60
|
+
@retry.Retry(
|
61
|
+
predicate=retry.if_exception_type(Exception),
|
62
|
+
timeout=5
|
63
|
+
)
|
64
|
+
def create(
|
65
|
+
cls,
|
66
|
+
name:str,
|
67
|
+
data:str
|
68
|
+
) -> Optional[str]:
|
69
|
+
parent = f"projects/{cls._project}"
|
70
|
+
secret_path = f"{parent}/secrets/{name}"
|
71
|
+
try:
|
72
|
+
#* Check if the secret already exists
|
73
|
+
request = secretmanager.GetSecretRequest(name=secret_path)
|
74
|
+
cls._client.get_secret(request=request)
|
75
|
+
|
76
|
+
except NotFound:
|
77
|
+
#* Secret does not exist, create it first
|
78
|
+
try:
|
79
|
+
secret = secretmanager.Secret(name=name, replication={"automatic": {}})
|
80
|
+
request = secretmanager.CreateSecretRequest(parent=parent, secret_id=name, secret=secret)
|
81
|
+
cls._client.create_secret(request=request)
|
82
|
+
except Exception as e:
|
83
|
+
return None
|
84
|
+
|
85
|
+
#* Add a new secret version
|
86
|
+
try:
|
87
|
+
payload = secretmanager.SecretPayload(data=data.encode()) # ✅ Fixed attribute name
|
88
|
+
request = secretmanager.AddSecretVersionRequest(parent=secret_path, payload=payload)
|
89
|
+
response = cls._client.add_secret_version(request=request)
|
90
|
+
return data
|
91
|
+
except Exception as e:
|
92
|
+
return None
|
@@ -0,0 +1,77 @@
|
|
1
|
+
import os
|
2
|
+
from datetime import timedelta
|
3
|
+
from google.auth import default
|
4
|
+
from google.cloud.storage import Bucket, Client
|
5
|
+
from google.oauth2 import service_account
|
6
|
+
from typing import Optional
|
7
|
+
|
8
|
+
class GoogleCloudStorage:
|
9
|
+
_client:Optional[Client] = None
|
10
|
+
_bucket:Optional[Bucket] = None
|
11
|
+
|
12
|
+
@classmethod
|
13
|
+
def initialize(cls) -> None:
|
14
|
+
"""Initialize the cloud storage if not already initialized."""
|
15
|
+
if cls._client is None:
|
16
|
+
#* Setup credentials with fallback chain
|
17
|
+
credentials = None
|
18
|
+
credentials_file = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
|
19
|
+
try:
|
20
|
+
if credentials_file:
|
21
|
+
credentials = service_account.Credentials.from_service_account_file(credentials_file)
|
22
|
+
else:
|
23
|
+
credentials, _ = default()
|
24
|
+
except Exception as e:
|
25
|
+
raise ValueError(f"Failed to initialize credentials: {str(e)}")
|
26
|
+
|
27
|
+
cls._client = Client(credentials=credentials)
|
28
|
+
|
29
|
+
#* Preload bucket
|
30
|
+
bucket_name = os.getenv("GCS_BUCKET_NAME")
|
31
|
+
if not bucket_name:
|
32
|
+
cls._client.close()
|
33
|
+
raise ValueError("GCS_BUCKET_NAME environment variable must be set")
|
34
|
+
|
35
|
+
#* Validate bucket existence
|
36
|
+
bucket = cls._client.lookup_bucket(bucket_name)
|
37
|
+
if bucket is None:
|
38
|
+
raise ValueError(f"Bucket '{bucket_name}' does not exist.")
|
39
|
+
|
40
|
+
cls._bucket = bucket
|
41
|
+
|
42
|
+
@classmethod
|
43
|
+
def dispose(cls) -> None:
|
44
|
+
"""Dispose of the cloud storage and release any resources."""
|
45
|
+
if cls._client is not None:
|
46
|
+
cls._client.close()
|
47
|
+
cls._client = None
|
48
|
+
cls._bucket = None
|
49
|
+
|
50
|
+
@classmethod
|
51
|
+
def _get_client(cls) -> Client:
|
52
|
+
"""Retrieve the cloud storage client, initializing it if necessary."""
|
53
|
+
cls.initialize()
|
54
|
+
return cls._client
|
55
|
+
|
56
|
+
@classmethod
|
57
|
+
def generate_signed_url(cls, location:str) -> str:
|
58
|
+
"""
|
59
|
+
generate signed URL of a file in the bucket based on its location.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
location: str
|
63
|
+
Location of the file inside the bucket
|
64
|
+
|
65
|
+
Returns:
|
66
|
+
str: File's pre-signed download url
|
67
|
+
|
68
|
+
Raises:
|
69
|
+
ValueError: If the file does not exist
|
70
|
+
"""
|
71
|
+
cls.initialize()
|
72
|
+
blob = cls._bucket.blob(blob_name=location)
|
73
|
+
if not blob.exists():
|
74
|
+
raise ValueError(f"File '{location}' did not exists.")
|
75
|
+
|
76
|
+
url = blob.generate_signed_url(version="v4", expiration=timedelta(minutes=15), method="GET")
|
77
|
+
return url
|
@@ -0,0 +1,8 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
EMAIL_REGEX:str = r"^[^\s@]+@[^\s@]+\.[^\s@]+$"
|
4
|
+
TOKEN_COOKIE_KEY_NAME="token"
|
5
|
+
REFRESH_TOKEN_DURATION_DAYS:int = 7
|
6
|
+
ACCESS_TOKEN_DURATION_MINUTES:int = 5
|
7
|
+
SORT_COLUMN_PATTERN = re.compile(r'^[a-z_]+\.(asc|desc)$')
|
8
|
+
DATE_FILTER_PATTERN = re.compile(r'^[a-z_]+(?:\|from::\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2}))?(?:\|to::\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2}))?$')
|
@@ -0,0 +1,50 @@
|
|
1
|
+
from fastapi import status
|
2
|
+
from typing import Type, Any
|
3
|
+
from maleo_foundation.models.enums import BaseEnums
|
4
|
+
from maleo_foundation.models.responses import BaseResponses
|
5
|
+
from maleo_foundation.models.transfers.results.service.controllers.rest import BaseServiceRESTControllerResults
|
6
|
+
from maleo_foundation.models.transfers.parameters.service import BaseServiceParametersTransfers
|
7
|
+
from maleo_foundation.models.types.service import BaseServiceTypes
|
8
|
+
|
9
|
+
class BaseController:
|
10
|
+
@staticmethod
|
11
|
+
def check_unique_existence(
|
12
|
+
check:BaseServiceParametersTransfers.UniqueFieldCheck,
|
13
|
+
get_single_parameters_class:Type[BaseServiceTypes.GetSingleParameter],
|
14
|
+
get_single_service_function:BaseServiceTypes.SyncGetSingleFunction,
|
15
|
+
create_failed_response_class:Type[BaseResponses.Fail],
|
16
|
+
update_failed_response_class:Type[BaseResponses.Fail],
|
17
|
+
**additional_get_parameters:Any
|
18
|
+
) -> BaseServiceRESTControllerResults:
|
19
|
+
"""Generic helper function to check if a unique value exists in the database."""
|
20
|
+
|
21
|
+
#* Return early if nullable and no new value
|
22
|
+
if check.nullable and check.new_value is None:
|
23
|
+
return BaseServiceRESTControllerResults(success=True, content=None)
|
24
|
+
|
25
|
+
#* Return early if values are unchanged on update
|
26
|
+
if check.operation == BaseEnums.OperationType.UPDATE and check.old_value == check.new_value:
|
27
|
+
return BaseServiceRESTControllerResults(success=True, content=None)
|
28
|
+
|
29
|
+
#* Prepare parameters to query for existing data
|
30
|
+
get_single_parameters = get_single_parameters_class(identifier=check.field, value=check.new_value)
|
31
|
+
|
32
|
+
#* Query the existing data using provided function
|
33
|
+
service_result:BaseServiceTypes.GetSingleResult = get_single_service_function(parameters=get_single_parameters, **additional_get_parameters)
|
34
|
+
if not service_result.success:
|
35
|
+
content = BaseResponses.ServerError.model_validate(service_result.model_dump(exclude_unset=True)).model_dump()
|
36
|
+
return BaseServiceRESTControllerResults(success=False, content=content, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
37
|
+
|
38
|
+
#* Handle case if duplicate is found
|
39
|
+
if service_result.data:
|
40
|
+
description = f"External error: {check.field} of '{check.new_value}' already exists in the database"
|
41
|
+
other = check.suggestion or f"Select another {check.field} value"
|
42
|
+
if check.operation == BaseEnums.OperationType.CREATE:
|
43
|
+
content = create_failed_response_class(description=description, other=other).model_dump()
|
44
|
+
elif check.operation == BaseEnums.OperationType.UPDATE:
|
45
|
+
content = update_failed_response_class(description=description, other=other).model_dump()
|
46
|
+
|
47
|
+
return BaseServiceRESTControllerResults(success=False, content=content, status_code=status.HTTP_400_BAD_REQUEST)
|
48
|
+
|
49
|
+
#* No duplicates found
|
50
|
+
return BaseServiceRESTControllerResults(success=True, content=None)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
from sqlalchemy import Engine, MetaData, Column, Integer, UUID, TIMESTAMP, Enum, func
|
2
|
+
from sqlalchemy.orm import declarative_base, declared_attr
|
3
|
+
from uuid import uuid4
|
4
|
+
from maleo_foundation.models.enums import BaseEnums
|
5
|
+
from maleo_foundation.utils.formatter.case import CaseFormatter
|
6
|
+
|
7
|
+
Base = declarative_base() #* Correct way to define a declarative base
|
8
|
+
|
9
|
+
class DatabaseManager:
|
10
|
+
class Base(Base): #* Inheriting from declarative_base
|
11
|
+
__abstract__ = True #* Ensures this class is not treated as a table itself
|
12
|
+
|
13
|
+
@declared_attr
|
14
|
+
def __tablename__(cls) -> str:
|
15
|
+
"""Automatically generates table name (in snake_case) based on class name."""
|
16
|
+
return CaseFormatter.to_snake_case(cls.__name__)
|
17
|
+
|
18
|
+
#* ----- ----- Common columns definition ----- ----- *#
|
19
|
+
|
20
|
+
#* Identifiers
|
21
|
+
id = Column(name="id", type_=Integer, primary_key=True)
|
22
|
+
uuid = Column(name="uuid", type_=UUID, default=uuid4, unique=True, nullable=False)
|
23
|
+
|
24
|
+
#* Timestamps
|
25
|
+
created_at = Column(name="created_at", type_=TIMESTAMP(timezone=True), server_default=func.now(), nullable=False)
|
26
|
+
updated_at = Column(name="updated_at", type_=TIMESTAMP(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
27
|
+
deleted_at = Column(name="deleted_at", type_=TIMESTAMP(timezone=True))
|
28
|
+
restored_at = Column(name="restored_at", type_=TIMESTAMP(timezone=True))
|
29
|
+
deactivated_at = Column(name="deactivated_at", type_=TIMESTAMP(timezone=True))
|
30
|
+
activated_at = Column(name="activated_at", type_=TIMESTAMP(timezone=True), server_default=func.now(), nullable=False)
|
31
|
+
|
32
|
+
#* Statuses
|
33
|
+
status = Column(name="status", type_=Enum(BaseEnums.StatusType, name="statustype"), default=BaseEnums.StatusType.ACTIVE, nullable=False)
|
34
|
+
|
35
|
+
#* Explicitly define the type of metadata
|
36
|
+
metadata:MetaData = Base.metadata
|
37
|
+
|
38
|
+
@staticmethod
|
39
|
+
def initialize(engine: Engine):
|
40
|
+
"""Creates the database tables if they do not exist."""
|
41
|
+
DatabaseManager.metadata.create_all(engine)
|
@@ -0,0 +1,40 @@
|
|
1
|
+
import os
|
2
|
+
from sqlalchemy import create_engine, Engine
|
3
|
+
from typing import Optional
|
4
|
+
from maleo_foundation.utils.logger import BaseLogger
|
5
|
+
|
6
|
+
class EngineManager:
|
7
|
+
_logger:Optional[BaseLogger] = None
|
8
|
+
_engine:Optional[Engine] = None
|
9
|
+
|
10
|
+
@classmethod
|
11
|
+
def initialize(cls, logger:BaseLogger, url:Optional[str] = None) -> Engine:
|
12
|
+
"""Initialize the engine if not already initialized."""
|
13
|
+
if cls._engine is None:
|
14
|
+
cls._logger = logger
|
15
|
+
url = url or os.getenv("DB_CONNECTION_STRING")
|
16
|
+
if url is None:
|
17
|
+
raise ValueError("DB_CONNECTION_STRING environment variable must be set if url is not provided")
|
18
|
+
cls._engine = create_engine(url=url, echo=False, pool_pre_ping=True, pool_recycle=3600)
|
19
|
+
cls._logger.info("EngineManager initialized successfully.")
|
20
|
+
return cls._engine
|
21
|
+
|
22
|
+
@classmethod
|
23
|
+
def get(cls) -> Engine:
|
24
|
+
"""Retrieve the engine, initializing it if necessary."""
|
25
|
+
if cls._logger is None:
|
26
|
+
raise RuntimeError("Logger has not been initialized. Call initialize(db_connection_string, logger) first.")
|
27
|
+
if cls._engine is None:
|
28
|
+
raise RuntimeError("Engine has not been initialized. Call initialize(db_connection_string, logger) first.")
|
29
|
+
|
30
|
+
return cls._engine
|
31
|
+
|
32
|
+
@classmethod
|
33
|
+
def dispose(cls) -> None:
|
34
|
+
"""Dispose of the engine and release any resources."""
|
35
|
+
if cls._engine is not None:
|
36
|
+
cls._engine.dispose()
|
37
|
+
cls._engine = None
|
38
|
+
|
39
|
+
cls._logger.info("Engine disposed successfully.")
|
40
|
+
cls._logger = None
|
@@ -0,0 +1,64 @@
|
|
1
|
+
from contextlib import contextmanager
|
2
|
+
from sqlalchemy import Engine
|
3
|
+
from sqlalchemy.orm import sessionmaker, Session
|
4
|
+
from sqlalchemy.exc import SQLAlchemyError
|
5
|
+
from typing import Generator, Optional
|
6
|
+
from maleo_foundation.utils.logger import BaseLogger
|
7
|
+
|
8
|
+
class SessionManager:
|
9
|
+
_logger:Optional[BaseLogger] = None
|
10
|
+
_sessionmaker:Optional[sessionmaker[Session]] = None
|
11
|
+
|
12
|
+
@classmethod
|
13
|
+
def initialize(cls, logger:BaseLogger, engine:Engine) -> None:
|
14
|
+
"""Initialize the sessionmaker if not already initialized."""
|
15
|
+
if cls._sessionmaker is None:
|
16
|
+
cls._logger = logger
|
17
|
+
cls._sessionmaker = sessionmaker(bind=engine, expire_on_commit=False)
|
18
|
+
cls._logger.info("SessionManager initialized successfully.")
|
19
|
+
|
20
|
+
@classmethod
|
21
|
+
def _session_handler(cls) -> Generator[Session, None, None]:
|
22
|
+
"""Reusable function for managing database sessions."""
|
23
|
+
if cls._logger is None:
|
24
|
+
raise RuntimeError("Logger has not been initialized. Call initialize() first.")
|
25
|
+
if cls._sessionmaker is None:
|
26
|
+
raise RuntimeError("SessionLocal has not been initialized. Call initialize() first.")
|
27
|
+
|
28
|
+
session = cls._sessionmaker()
|
29
|
+
cls._logger.debug("New database session created.")
|
30
|
+
try:
|
31
|
+
yield session #* Provide session
|
32
|
+
session.commit() #* Auto-commit on success
|
33
|
+
except SQLAlchemyError as e:
|
34
|
+
session.rollback() #* Rollback on error
|
35
|
+
cls._logger.error(f"[SQLAlchemyError] Database transaction failed: {e}", exc_info=True)
|
36
|
+
raise
|
37
|
+
except Exception as e:
|
38
|
+
session.rollback() #* Rollback on error
|
39
|
+
cls._logger.error(f"[Exception] Database transaction failed: {e}", exc_info=True)
|
40
|
+
raise
|
41
|
+
finally:
|
42
|
+
session.close() #* Ensure session closes
|
43
|
+
cls._logger.debug("Database session closed.")
|
44
|
+
|
45
|
+
@classmethod
|
46
|
+
def inject(cls) -> Generator[Session, None, None]:
|
47
|
+
"""Returns a generator that yields a SQLAlchemy session for dependency injection."""
|
48
|
+
return cls._session_handler()
|
49
|
+
|
50
|
+
@classmethod
|
51
|
+
@contextmanager
|
52
|
+
def get(cls) -> Generator[Session, None, None]:
|
53
|
+
"""Context manager for manual session handling. Supports `with SessionManager.get() as session:`"""
|
54
|
+
yield from cls._session_handler()
|
55
|
+
|
56
|
+
@classmethod
|
57
|
+
def dispose(cls) -> None:
|
58
|
+
"""Dispose of the sessionmaker and release any resources."""
|
59
|
+
if cls._sessionmaker is not None:
|
60
|
+
cls._sessionmaker.close_all()
|
61
|
+
cls._sessionmaker = None
|
62
|
+
|
63
|
+
cls._logger.info("SessionManager disposed successfully.")
|
64
|
+
cls._logger = None
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from .enums import BaseEnums
|
3
|
+
from .schemas import BaseSchemas
|
4
|
+
from .transfers import BaseTransfers
|
5
|
+
from .responses import BaseResponses
|
6
|
+
from .types import BaseTypes
|
7
|
+
|
8
|
+
class BaseModels:
|
9
|
+
Enums = BaseEnums
|
10
|
+
Schemas = BaseSchemas
|
11
|
+
Transfers = BaseTransfers
|
12
|
+
Responses = BaseResponses
|
13
|
+
Types = BaseTypes
|
@@ -0,0 +1,66 @@
|
|
1
|
+
import logging
|
2
|
+
from enum import IntEnum, StrEnum
|
3
|
+
from fastapi import responses
|
4
|
+
|
5
|
+
class BaseEnums:
|
6
|
+
class StatusType(StrEnum):
|
7
|
+
DELETED = "deleted"
|
8
|
+
INACTIVE = "inactive"
|
9
|
+
ACTIVE = "active"
|
10
|
+
|
11
|
+
class UserType(StrEnum):
|
12
|
+
REGULAR = "regular"
|
13
|
+
PROXY = "proxy"
|
14
|
+
|
15
|
+
class SortOrder(StrEnum):
|
16
|
+
ASC = "asc"
|
17
|
+
DESC = "desc"
|
18
|
+
|
19
|
+
class StatusUpdateAction(StrEnum):
|
20
|
+
ACTIVATE = "activate"
|
21
|
+
DEACTIVATE = "deactivate"
|
22
|
+
RESTORE = "restore"
|
23
|
+
DELETE = "delete"
|
24
|
+
|
25
|
+
class TokenType(StrEnum):
|
26
|
+
REFRESH = "refresh"
|
27
|
+
ACCESS = "access"
|
28
|
+
|
29
|
+
class OperationType(StrEnum):
|
30
|
+
CREATE = "create"
|
31
|
+
UPDATE = "update"
|
32
|
+
|
33
|
+
class UniqueIdentifiers(StrEnum):
|
34
|
+
ID = "id"
|
35
|
+
UUID = "uuid"
|
36
|
+
|
37
|
+
class RESTControllerResponseType(StrEnum):
|
38
|
+
NONE = "none"
|
39
|
+
HTML = "html"
|
40
|
+
TEXT = "text"
|
41
|
+
JSON = "json"
|
42
|
+
REDIRECT = "redirect"
|
43
|
+
STREAMING = "streaming"
|
44
|
+
FILE = "file"
|
45
|
+
|
46
|
+
def get_response_type(self) -> type[responses.Response]:
|
47
|
+
"""Returns the corresponding FastAPI Response type."""
|
48
|
+
return {
|
49
|
+
BaseEnums.RESTControllerResponseType.NONE: responses.Response,
|
50
|
+
BaseEnums.RESTControllerResponseType.HTML: responses.HTMLResponse,
|
51
|
+
BaseEnums.RESTControllerResponseType.TEXT: responses.PlainTextResponse,
|
52
|
+
BaseEnums.RESTControllerResponseType.JSON: responses.JSONResponse,
|
53
|
+
BaseEnums.RESTControllerResponseType.REDIRECT: responses.RedirectResponse,
|
54
|
+
BaseEnums.RESTControllerResponseType.STREAMING: responses.StreamingResponse,
|
55
|
+
BaseEnums.RESTControllerResponseType.FILE: responses.FileResponse,
|
56
|
+
}.get(self, responses.Response)
|
57
|
+
|
58
|
+
class LoggerLevel(IntEnum):
|
59
|
+
CRITICAL = logging.CRITICAL
|
60
|
+
FATAL = logging.FATAL
|
61
|
+
ERROR = logging.ERROR
|
62
|
+
WARNING = logging.WARNING
|
63
|
+
WARN = logging.WARN
|
64
|
+
INFO = logging.INFO
|
65
|
+
DEBUG = logging.DEBUG
|
66
|
+
NOTSET = logging.NOTSET
|