maleo-foundation 0.3.46__py3-none-any.whl → 0.3.48__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/authentication.py +24 -13
- maleo_foundation/authorization.py +2 -1
- maleo_foundation/client/manager.py +22 -21
- maleo_foundation/client/services/__init__.py +16 -7
- maleo_foundation/client/services/encryption/__init__.py +13 -4
- maleo_foundation/client/services/encryption/aes.py +41 -36
- maleo_foundation/client/services/encryption/rsa.py +50 -50
- maleo_foundation/client/services/hash/__init__.py +19 -6
- maleo_foundation/client/services/hash/bcrypt.py +20 -18
- maleo_foundation/client/services/hash/hmac.py +20 -17
- maleo_foundation/client/services/hash/sha256.py +18 -15
- maleo_foundation/client/services/key.py +50 -42
- maleo_foundation/client/services/signature.py +46 -42
- maleo_foundation/client/services/token.py +49 -58
- maleo_foundation/constants.py +12 -19
- maleo_foundation/enums.py +14 -13
- maleo_foundation/expanded_types/__init__.py +2 -3
- maleo_foundation/expanded_types/client.py +30 -34
- maleo_foundation/expanded_types/encryption/__init__.py +2 -1
- maleo_foundation/expanded_types/encryption/aes.py +7 -5
- maleo_foundation/expanded_types/encryption/rsa.py +7 -5
- maleo_foundation/expanded_types/general.py +13 -11
- maleo_foundation/expanded_types/hash.py +7 -5
- maleo_foundation/expanded_types/key.py +8 -6
- maleo_foundation/expanded_types/service.py +30 -34
- maleo_foundation/expanded_types/signature.py +7 -5
- maleo_foundation/expanded_types/token.py +7 -5
- maleo_foundation/extended_types.py +4 -3
- maleo_foundation/managers/cache.py +2 -1
- maleo_foundation/managers/client/base.py +25 -12
- maleo_foundation/managers/client/google/base.py +11 -4
- maleo_foundation/managers/client/google/parameter.py +9 -11
- maleo_foundation/managers/client/google/secret.py +53 -35
- maleo_foundation/managers/client/google/storage.py +52 -22
- maleo_foundation/managers/client/google/subscription.py +37 -39
- maleo_foundation/managers/client/maleo.py +18 -23
- maleo_foundation/managers/configuration.py +5 -9
- maleo_foundation/managers/credential.py +14 -17
- maleo_foundation/managers/db.py +51 -40
- maleo_foundation/managers/middleware.py +9 -9
- maleo_foundation/managers/service.py +47 -54
- maleo_foundation/middlewares/authentication.py +29 -54
- maleo_foundation/middlewares/base.py +83 -72
- maleo_foundation/middlewares/cors.py +8 -7
- maleo_foundation/models/__init__.py +2 -1
- maleo_foundation/models/responses.py +57 -29
- maleo_foundation/models/schemas/__init__.py +2 -1
- maleo_foundation/models/schemas/encryption.py +5 -2
- maleo_foundation/models/schemas/general.py +38 -18
- maleo_foundation/models/schemas/hash.py +2 -1
- maleo_foundation/models/schemas/key.py +5 -2
- maleo_foundation/models/schemas/parameter.py +45 -15
- maleo_foundation/models/schemas/result.py +35 -20
- maleo_foundation/models/schemas/signature.py +5 -2
- maleo_foundation/models/schemas/token.py +5 -2
- maleo_foundation/models/table.py +33 -27
- maleo_foundation/models/transfers/__init__.py +2 -1
- maleo_foundation/models/transfers/general/__init__.py +2 -1
- maleo_foundation/models/transfers/general/configurations/__init__.py +10 -4
- maleo_foundation/models/transfers/general/configurations/cache/__init__.py +3 -2
- maleo_foundation/models/transfers/general/configurations/cache/redis.py +13 -5
- maleo_foundation/models/transfers/general/configurations/client/__init__.py +5 -1
- maleo_foundation/models/transfers/general/configurations/client/maleo.py +38 -12
- maleo_foundation/models/transfers/general/configurations/database.py +5 -2
- maleo_foundation/models/transfers/general/configurations/middleware.py +22 -15
- maleo_foundation/models/transfers/general/configurations/service.py +2 -1
- maleo_foundation/models/transfers/general/credentials.py +2 -1
- maleo_foundation/models/transfers/general/database.py +11 -4
- maleo_foundation/models/transfers/general/key.py +13 -4
- maleo_foundation/models/transfers/general/request.py +28 -9
- maleo_foundation/models/transfers/general/settings.py +12 -22
- maleo_foundation/models/transfers/general/signature.py +4 -2
- maleo_foundation/models/transfers/general/token.py +34 -27
- maleo_foundation/models/transfers/parameters/__init__.py +2 -1
- maleo_foundation/models/transfers/parameters/client.py +15 -19
- maleo_foundation/models/transfers/parameters/encryption/__init__.py +2 -1
- maleo_foundation/models/transfers/parameters/encryption/aes.py +7 -5
- maleo_foundation/models/transfers/parameters/encryption/rsa.py +7 -5
- maleo_foundation/models/transfers/parameters/general.py +15 -13
- maleo_foundation/models/transfers/parameters/hash/__init__.py +2 -1
- maleo_foundation/models/transfers/parameters/hash/bcrypt.py +5 -5
- maleo_foundation/models/transfers/parameters/hash/hmac.py +6 -6
- maleo_foundation/models/transfers/parameters/hash/sha256.py +5 -5
- maleo_foundation/models/transfers/parameters/key.py +9 -8
- maleo_foundation/models/transfers/parameters/service.py +42 -48
- maleo_foundation/models/transfers/parameters/signature.py +7 -4
- maleo_foundation/models/transfers/parameters/token.py +10 -10
- maleo_foundation/models/transfers/results/__init__.py +2 -1
- maleo_foundation/models/transfers/results/client/__init__.py +2 -1
- maleo_foundation/models/transfers/results/client/controllers/__init__.py +2 -1
- maleo_foundation/models/transfers/results/client/controllers/http.py +10 -7
- maleo_foundation/models/transfers/results/client/service.py +12 -6
- maleo_foundation/models/transfers/results/encryption/__init__.py +2 -1
- maleo_foundation/models/transfers/results/encryption/aes.py +13 -5
- maleo_foundation/models/transfers/results/encryption/rsa.py +12 -4
- maleo_foundation/models/transfers/results/hash.py +7 -3
- maleo_foundation/models/transfers/results/key.py +18 -6
- maleo_foundation/models/transfers/results/service/__init__.py +2 -3
- maleo_foundation/models/transfers/results/service/controllers/__init__.py +2 -1
- maleo_foundation/models/transfers/results/service/controllers/rest.py +14 -11
- maleo_foundation/models/transfers/results/service/general.py +16 -10
- maleo_foundation/models/transfers/results/signature.py +12 -4
- maleo_foundation/models/transfers/results/token.py +10 -4
- maleo_foundation/rest_controller_result.py +23 -21
- maleo_foundation/types.py +15 -14
- maleo_foundation/utils/__init__.py +2 -1
- maleo_foundation/utils/cache.py +10 -13
- maleo_foundation/utils/client.py +25 -12
- maleo_foundation/utils/controller.py +59 -37
- maleo_foundation/utils/dependencies/__init__.py +2 -1
- maleo_foundation/utils/dependencies/auth.py +5 -12
- maleo_foundation/utils/dependencies/context.py +3 -4
- maleo_foundation/utils/exceptions.py +50 -28
- maleo_foundation/utils/extractor.py +18 -6
- maleo_foundation/utils/formatter/__init__.py +2 -1
- maleo_foundation/utils/formatter/case.py +5 -4
- maleo_foundation/utils/loaders/__init__.py +2 -1
- maleo_foundation/utils/loaders/credential/__init__.py +2 -1
- maleo_foundation/utils/loaders/credential/google.py +29 -15
- maleo_foundation/utils/loaders/json.py +3 -2
- maleo_foundation/utils/loaders/key/__init__.py +2 -1
- maleo_foundation/utils/loaders/key/rsa.py +26 -13
- maleo_foundation/utils/loaders/yaml.py +2 -1
- maleo_foundation/utils/logging.py +70 -46
- maleo_foundation/utils/merger.py +7 -9
- maleo_foundation/utils/query.py +41 -34
- maleo_foundation/utils/repository.py +29 -16
- maleo_foundation/utils/searcher.py +4 -6
- {maleo_foundation-0.3.46.dist-info → maleo_foundation-0.3.48.dist-info}/METADATA +14 -1
- maleo_foundation-0.3.48.dist-info/RECORD +137 -0
- maleo_foundation/expanded_types/repository.py +0 -68
- maleo_foundation/models/transfers/results/service/repository.py +0 -39
- maleo_foundation-0.3.46.dist-info/RECORD +0 -139
- {maleo_foundation-0.3.46.dist-info → maleo_foundation-0.3.48.dist-info}/WHEEL +0 -0
- {maleo_foundation-0.3.46.dist-info → maleo_foundation-0.3.48.dist-info}/top_level.txt +0 -0
maleo_foundation/utils/client.py
CHANGED
@@ -3,8 +3,10 @@ from functools import wraps
|
|
3
3
|
from pydantic import ValidationError
|
4
4
|
from typing import Optional, Type, Union
|
5
5
|
from maleo_foundation.types import BaseTypes
|
6
|
-
from maleo_foundation.models.transfers.results.client.service import
|
7
|
-
BaseClientServiceResultsTransfers
|
6
|
+
from maleo_foundation.models.transfers.results.client.service import (
|
7
|
+
BaseClientServiceResultsTransfers,
|
8
|
+
)
|
9
|
+
|
8
10
|
|
9
11
|
class BaseClientUtils:
|
10
12
|
@staticmethod
|
@@ -13,49 +15,60 @@ class BaseClientUtils:
|
|
13
15
|
data_found_class: Union[
|
14
16
|
Type[BaseClientServiceResultsTransfers.SingleData],
|
15
17
|
Type[BaseClientServiceResultsTransfers.UnpaginatedMultipleData],
|
16
|
-
Type[BaseClientServiceResultsTransfers.PaginatedMultipleData]
|
18
|
+
Type[BaseClientServiceResultsTransfers.PaginatedMultipleData],
|
17
19
|
],
|
18
20
|
no_data_class: Optional[Type[BaseClientServiceResultsTransfers.NoData]] = None,
|
19
21
|
):
|
20
22
|
"""Decorator to handle repository-related exceptions consistently."""
|
23
|
+
|
21
24
|
def decorator(func):
|
22
25
|
def _processor(result: BaseTypes.StringToAnyDict):
|
23
26
|
if "success" not in result and "data" not in result:
|
24
|
-
raise ValueError(
|
25
|
-
|
26
|
-
|
27
|
+
raise ValueError(
|
28
|
+
"Result did not have both 'success' and 'data' field"
|
29
|
+
)
|
30
|
+
success: bool = result.get("success", False)
|
31
|
+
data: BaseTypes.StringToAnyDict = result.get("data", {})
|
27
32
|
if not success:
|
28
33
|
validated_result = fail_class.model_validate(result)
|
29
34
|
return validated_result
|
30
35
|
else:
|
31
36
|
if data is None:
|
32
37
|
if no_data_class is None:
|
33
|
-
raise ValueError(
|
38
|
+
raise ValueError(
|
39
|
+
"'no_data_class' must be given to validate No Data"
|
40
|
+
)
|
34
41
|
validated_result = no_data_class.model_validate(result)
|
35
42
|
return validated_result
|
36
43
|
else:
|
37
44
|
validated_result = data_found_class.model_validate(result)
|
38
45
|
return validated_result
|
46
|
+
|
39
47
|
if asyncio.iscoroutinefunction(func):
|
48
|
+
|
40
49
|
@wraps(func)
|
41
50
|
async def async_wrapper(*args, **kwargs):
|
42
51
|
try:
|
43
52
|
result: BaseTypes.StringToAnyDict = await func(*args, **kwargs)
|
44
53
|
return _processor(result=result)
|
45
|
-
except ValidationError
|
54
|
+
except ValidationError:
|
46
55
|
raise
|
47
|
-
except Exception
|
56
|
+
except Exception:
|
48
57
|
raise
|
58
|
+
|
49
59
|
return async_wrapper
|
50
60
|
else:
|
61
|
+
|
51
62
|
@wraps(func)
|
52
63
|
def sync_wrapper(*args, **kwargs):
|
53
64
|
try:
|
54
65
|
result: BaseTypes.StringToAnyDict = func(*args, **kwargs)
|
55
66
|
return _processor(result=result)
|
56
|
-
except ValidationError
|
67
|
+
except ValidationError:
|
57
68
|
raise
|
58
|
-
except Exception
|
69
|
+
except Exception:
|
59
70
|
raise
|
71
|
+
|
60
72
|
return sync_wrapper
|
61
|
-
|
73
|
+
|
74
|
+
return decorator
|
@@ -1,28 +1,32 @@
|
|
1
1
|
import inspect
|
2
2
|
from fastapi import status
|
3
3
|
from functools import wraps
|
4
|
-
from typing import Awaitable, Callable, Dict, List,
|
4
|
+
from typing import Awaitable, Callable, Dict, List, cast
|
5
5
|
from maleo_foundation.types import BaseTypes
|
6
6
|
from maleo_foundation.models.responses import BaseResponses
|
7
|
-
from maleo_foundation.models.transfers.parameters.general
|
8
|
-
|
9
|
-
|
10
|
-
|
7
|
+
from maleo_foundation.models.transfers.parameters.general import (
|
8
|
+
BaseGeneralParametersTransfers,
|
9
|
+
)
|
10
|
+
from maleo_foundation.models.transfers.results.service.controllers.rest import (
|
11
|
+
BaseServiceRESTControllerResults,
|
12
|
+
)
|
11
13
|
from maleo_foundation.expanded_types.general import BaseGeneralExpandedTypes
|
12
14
|
|
15
|
+
|
13
16
|
class BaseControllerUtils:
|
14
17
|
@staticmethod
|
15
18
|
def field_expansion_handler(
|
16
19
|
expandable_fields_dependencies_map: BaseTypes.OptionalStringToListOfStringDict = None,
|
17
|
-
field_expansion_processors: BaseGeneralExpandedTypes.OptionalListOfFieldExpansionProcessor = None
|
20
|
+
field_expansion_processors: BaseGeneralExpandedTypes.OptionalListOfFieldExpansionProcessor = None,
|
18
21
|
):
|
19
22
|
"""
|
20
23
|
Decorator to handle expandable fields validation and processing.
|
21
|
-
|
24
|
+
|
22
25
|
Args:
|
23
26
|
expandable_fields_dependencies_map: Dictionary where keys are dependency fields and values are lists of dependent fields
|
24
27
|
field_expansion_processors: List of processor functions that handle that field's data
|
25
28
|
"""
|
29
|
+
|
26
30
|
def decorator(func: Callable[..., Awaitable[BaseServiceRESTControllerResults]]):
|
27
31
|
@wraps(func)
|
28
32
|
async def wrapper(*args, **kwargs):
|
@@ -31,53 +35,64 @@ class BaseControllerUtils:
|
|
31
35
|
bound.apply_defaults()
|
32
36
|
|
33
37
|
parameters = bound.arguments.get("parameters")
|
34
|
-
expand: BaseTypes.OptionalListOfStrings = getattr(
|
38
|
+
expand: BaseTypes.OptionalListOfStrings = getattr(
|
39
|
+
parameters, "expand", None
|
40
|
+
)
|
35
41
|
|
36
|
-
|
37
|
-
if
|
38
|
-
|
42
|
+
# * Validate expandable fields dependencies
|
43
|
+
if (
|
44
|
+
expand is not None
|
45
|
+
and expandable_fields_dependencies_map is not None
|
46
|
+
):
|
47
|
+
for (
|
48
|
+
dependency,
|
49
|
+
dependents,
|
50
|
+
) in expandable_fields_dependencies_map.items():
|
39
51
|
if dependency not in expand:
|
40
52
|
for dependent in dependents:
|
41
53
|
if dependent in expand:
|
42
54
|
other = f"'{dependency}' must also be expanded if '{dependent}' is expanded"
|
43
|
-
content = BaseResponses.InvalidExpand(other=other).model_dump()
|
55
|
+
content = BaseResponses.InvalidExpand(other=other).model_dump() # type: ignore
|
44
56
|
return BaseServiceRESTControllerResults(
|
45
57
|
success=False,
|
46
58
|
content=content,
|
47
|
-
status_code=status.HTTP_400_BAD_REQUEST
|
48
|
-
)
|
59
|
+
status_code=status.HTTP_400_BAD_REQUEST,
|
60
|
+
) # type: ignore
|
49
61
|
|
50
|
-
|
62
|
+
# * Call the original function
|
51
63
|
result = await func(*args, **kwargs)
|
52
64
|
|
53
65
|
if not isinstance(result.content, Dict):
|
54
66
|
return result
|
55
67
|
|
56
|
-
|
57
|
-
def recursive_expand(
|
68
|
+
# * Recursive function to apply expansion processors
|
69
|
+
def recursive_expand(
|
70
|
+
data: BaseTypes.ListOrDictOfAny,
|
71
|
+
expand: BaseTypes.OptionalListOfStrings,
|
72
|
+
) -> BaseTypes.ListOrDictOfAny:
|
58
73
|
if isinstance(data, list):
|
59
74
|
for idx, item in enumerate(data):
|
60
75
|
data[idx] = recursive_expand(item, expand)
|
61
76
|
return data
|
62
|
-
elif isinstance(data,
|
63
|
-
|
77
|
+
elif isinstance(data, dict):
|
78
|
+
# * If no expand is provided
|
79
|
+
# * Apply each processor to current dict
|
64
80
|
parameters = (
|
65
|
-
BaseGeneralParametersTransfers
|
66
|
-
|
67
|
-
data=data,
|
68
|
-
expand=expand
|
81
|
+
BaseGeneralParametersTransfers.FieldExpansionProcessor(
|
82
|
+
data=data, expand=expand
|
69
83
|
)
|
70
84
|
)
|
71
85
|
for processor in field_expansion_processors or []:
|
72
86
|
data = processor(parameters)
|
87
|
+
data = cast(BaseTypes.StringToAnyDict, data)
|
73
88
|
for key in data.keys():
|
74
|
-
if isinstance(data[key], (
|
89
|
+
if isinstance(data[key], (dict, list)):
|
75
90
|
data[key] = recursive_expand(data[key], expand)
|
76
91
|
return data
|
77
92
|
else:
|
78
93
|
return data
|
79
94
|
|
80
|
-
|
95
|
+
# * Process expansions recursively if needed
|
81
96
|
if (
|
82
97
|
result.success
|
83
98
|
and result.content.get("data", None) is not None
|
@@ -85,46 +100,51 @@ class BaseControllerUtils:
|
|
85
100
|
):
|
86
101
|
data = result.content["data"]
|
87
102
|
result.content["data"] = recursive_expand(data, expand)
|
88
|
-
result.
|
103
|
+
result.response
|
89
104
|
|
90
105
|
return result
|
106
|
+
|
91
107
|
return wrapper
|
108
|
+
|
92
109
|
return decorator
|
93
110
|
|
94
111
|
@staticmethod
|
95
112
|
def field_modification_handler(
|
96
|
-
field_modification_processors: BaseGeneralExpandedTypes.OptionalListOfFieldModificationProcessor = None
|
113
|
+
field_modification_processors: BaseGeneralExpandedTypes.OptionalListOfFieldModificationProcessor = None,
|
97
114
|
):
|
98
115
|
"""
|
99
116
|
Decorator to handle expandable fields validation and processing.
|
100
|
-
|
117
|
+
|
101
118
|
Args:
|
102
119
|
expandable_fields_dependencies_map: Dictionary where keys are dependency fields and values are lists of dependent fields
|
103
120
|
field_modification_processors: List of processor functions that handle that field's data
|
104
121
|
"""
|
122
|
+
|
105
123
|
def decorator(func: Callable[..., Awaitable[BaseServiceRESTControllerResults]]):
|
106
124
|
@wraps(func)
|
107
125
|
async def wrapper(*args, **kwargs):
|
108
|
-
|
126
|
+
# * Call the original function
|
109
127
|
result = await func(*args, **kwargs)
|
110
128
|
|
111
129
|
if not isinstance(result.content, Dict):
|
112
130
|
return result
|
113
131
|
|
114
|
-
|
115
|
-
def recursive_modify(data:
|
132
|
+
# * Recursive function to apply modification processors
|
133
|
+
def recursive_modify(data: BaseTypes.ListOrDictOfAny):
|
116
134
|
if isinstance(data, list):
|
117
135
|
for idx, item in enumerate(data):
|
118
136
|
data[idx] = recursive_modify(item)
|
119
137
|
return data
|
120
138
|
elif isinstance(data, Dict):
|
121
|
-
|
139
|
+
# * Apply each processor to current dict
|
122
140
|
parameters = (
|
123
|
-
BaseGeneralParametersTransfers
|
124
|
-
|
141
|
+
BaseGeneralParametersTransfers.FieldModificationProcessor(
|
142
|
+
data=data
|
143
|
+
)
|
125
144
|
)
|
126
145
|
for processor in field_modification_processors or []:
|
127
146
|
data = processor(parameters)
|
147
|
+
data = cast(BaseTypes.StringToAnyDict, data)
|
128
148
|
for key in data.keys():
|
129
149
|
if isinstance(data[key], (Dict, List)):
|
130
150
|
data[key] = recursive_modify(data[key])
|
@@ -132,7 +152,7 @@ class BaseControllerUtils:
|
|
132
152
|
else:
|
133
153
|
return data
|
134
154
|
|
135
|
-
|
155
|
+
# * Process modifications recursively if needed
|
136
156
|
if (
|
137
157
|
result.success
|
138
158
|
and result.content.get("data", None) is not None
|
@@ -140,8 +160,10 @@ class BaseControllerUtils:
|
|
140
160
|
):
|
141
161
|
data = result.content["data"]
|
142
162
|
result.content["data"] = recursive_modify(data)
|
143
|
-
result.
|
163
|
+
result.response
|
144
164
|
|
145
165
|
return result
|
166
|
+
|
146
167
|
return wrapper
|
147
|
-
|
168
|
+
|
169
|
+
return decorator
|
@@ -4,21 +4,14 @@ from fastapi.security import HTTPAuthorizationCredentials
|
|
4
4
|
from maleo_foundation.authentication import Authentication
|
5
5
|
from maleo_foundation.authorization import TOKEN_SCHEME, Authorization
|
6
6
|
|
7
|
+
|
7
8
|
class AuthDependencies:
|
8
9
|
@staticmethod
|
9
|
-
def authentication(
|
10
|
-
request
|
11
|
-
) -> Authentication:
|
12
|
-
return Authentication(
|
13
|
-
credentials=request.auth,
|
14
|
-
user=request.user
|
15
|
-
)
|
10
|
+
def authentication(request: Request) -> Authentication:
|
11
|
+
return Authentication(credentials=request.auth, user=request.user)
|
16
12
|
|
17
13
|
@staticmethod
|
18
14
|
def authorization(
|
19
|
-
token: HTTPAuthorizationCredentials = Security(TOKEN_SCHEME)
|
15
|
+
token: HTTPAuthorizationCredentials = Security(TOKEN_SCHEME),
|
20
16
|
) -> Authorization:
|
21
|
-
return Authorization(
|
22
|
-
scheme=token.scheme,
|
23
|
-
credentials=token.credentials
|
24
|
-
)
|
17
|
+
return Authorization(scheme=token.scheme, credentials=token.credentials)
|
@@ -1,9 +1,8 @@
|
|
1
1
|
from fastapi.requests import Request
|
2
2
|
from maleo_foundation.models.transfers.general.request import RequestContext
|
3
3
|
|
4
|
+
|
4
5
|
class ContextDependencies:
|
5
6
|
@staticmethod
|
6
|
-
def get_request_context(
|
7
|
-
request
|
8
|
-
) -> RequestContext:
|
9
|
-
return request.state.request_context
|
7
|
+
def get_request_context(request: Request) -> RequestContext:
|
8
|
+
return request.state.request_context
|
@@ -9,61 +9,68 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
9
9
|
from sqlalchemy.exc import SQLAlchemyError
|
10
10
|
from typing import Optional
|
11
11
|
from maleo_foundation.models.responses import BaseResponses
|
12
|
-
from maleo_foundation.models.transfers.results.service.general
|
13
|
-
|
14
|
-
|
15
|
-
import BaseServiceRepositoryResultsTransfers
|
12
|
+
from maleo_foundation.models.transfers.results.service.general import (
|
13
|
+
BaseServiceGeneralResultsTransfers,
|
14
|
+
)
|
16
15
|
from maleo_foundation.utils.logging import BaseLogger
|
17
16
|
|
17
|
+
|
18
18
|
class BaseExceptions:
|
19
19
|
@staticmethod
|
20
20
|
def authentication_error_handler(request: Request, exc: Exception):
|
21
21
|
return JSONResponse(
|
22
|
-
content=BaseResponses.Unauthorized(other=str(exc)).model_dump(mode="json"),
|
23
|
-
status_code=status.HTTP_401_UNAUTHORIZED
|
22
|
+
content=BaseResponses.Unauthorized(other=str(exc)).model_dump(mode="json"), # type: ignore
|
23
|
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
24
24
|
)
|
25
25
|
|
26
26
|
@staticmethod
|
27
|
-
async def validation_exception_handler(
|
27
|
+
async def validation_exception_handler(
|
28
|
+
request: Request, exc: RequestValidationError
|
29
|
+
):
|
28
30
|
serialized_error = jsonable_encoder(exc.errors())
|
29
31
|
return JSONResponse(
|
30
|
-
content=BaseResponses.ValidationError(other=serialized_error).model_dump(mode="json"),
|
31
|
-
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
|
32
|
+
content=BaseResponses.ValidationError(other=serialized_error).model_dump(mode="json"), # type: ignore
|
33
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
32
34
|
)
|
33
35
|
|
34
36
|
@staticmethod
|
35
37
|
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
|
36
38
|
if exc.status_code in BaseResponses.other_responses:
|
37
39
|
return JSONResponse(
|
38
|
-
content=BaseResponses.other_responses[exc.status_code]["model"]().model_dump(mode="json"),
|
39
|
-
status_code=exc.status_code
|
40
|
+
content=BaseResponses.other_responses[exc.status_code]["model"]().model_dump(mode="json"), # type: ignore
|
41
|
+
status_code=exc.status_code,
|
40
42
|
)
|
41
43
|
|
42
44
|
return JSONResponse(
|
43
|
-
content=BaseResponses.ServerError().model_dump(mode="json"),
|
44
|
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
45
|
+
content=BaseResponses.ServerError().model_dump(mode="json"), # type: ignore
|
46
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
45
47
|
)
|
46
48
|
|
47
49
|
@staticmethod
|
48
50
|
def repository_exception_handler(
|
49
51
|
operation: str,
|
50
52
|
logger: Optional[BaseLogger] = None,
|
51
|
-
fail_result_class: type[
|
53
|
+
fail_result_class: type[
|
54
|
+
BaseServiceGeneralResultsTransfers.Fail
|
55
|
+
] = BaseServiceGeneralResultsTransfers.Fail,
|
52
56
|
):
|
53
57
|
"""Decorator to handle repository-related exceptions consistently for sync and async functions."""
|
58
|
+
|
54
59
|
def decorator(func):
|
55
60
|
def _handler(e: Exception, category: str, description: str):
|
56
61
|
if logger:
|
57
62
|
logger.error(
|
58
63
|
f"{category} occurred while {operation}: '{str(e)}'",
|
59
|
-
exc_info=True
|
64
|
+
exc_info=True,
|
60
65
|
)
|
61
66
|
return fail_result_class(
|
62
67
|
message=f"Failed {operation}",
|
63
68
|
description=description,
|
64
|
-
other=category
|
65
|
-
)
|
69
|
+
other=category,
|
70
|
+
) # type: ignore
|
71
|
+
|
66
72
|
if asyncio.iscoroutinefunction(func):
|
73
|
+
|
67
74
|
@wraps(func)
|
68
75
|
async def async_wrapper(*args, **kwargs):
|
69
76
|
try:
|
@@ -72,22 +79,24 @@ class BaseExceptions:
|
|
72
79
|
return _handler(
|
73
80
|
e,
|
74
81
|
category="Validation error",
|
75
|
-
description=f"A validation error occurred while {operation}. Please try again later or contact administrator."
|
82
|
+
description=f"A validation error occurred while {operation}. Please try again later or contact administrator.",
|
76
83
|
)
|
77
84
|
except SQLAlchemyError as e:
|
78
85
|
return _handler(
|
79
86
|
e,
|
80
87
|
category="Database operation failed",
|
81
|
-
description=f"A database error occurred while {operation}. Please try again later or contact administrator."
|
88
|
+
description=f"A database error occurred while {operation}. Please try again later or contact administrator.",
|
82
89
|
)
|
83
90
|
except Exception as e:
|
84
91
|
return _handler(
|
85
92
|
e,
|
86
93
|
category="Internal processing error",
|
87
|
-
description=f"An unexpected error occurred while {operation}. Please try again later or contact administrator."
|
94
|
+
description=f"An unexpected error occurred while {operation}. Please try again later or contact administrator.",
|
88
95
|
)
|
96
|
+
|
89
97
|
return async_wrapper
|
90
98
|
else:
|
99
|
+
|
91
100
|
@wraps(func)
|
92
101
|
def sync_wrapper(*args, **kwargs):
|
93
102
|
try:
|
@@ -96,41 +105,54 @@ class BaseExceptions:
|
|
96
105
|
return _handler(
|
97
106
|
e,
|
98
107
|
category="Validation error",
|
99
|
-
description=f"A validation error occurred while {operation}. Please try again later or contact administrator."
|
108
|
+
description=f"A validation error occurred while {operation}. Please try again later or contact administrator.",
|
100
109
|
)
|
101
110
|
except SQLAlchemyError as e:
|
102
111
|
return _handler(
|
103
112
|
e,
|
104
113
|
category="Database operation failed",
|
105
|
-
description=f"A database error occurred while {operation}. Please try again later or contact administrator."
|
114
|
+
description=f"A database error occurred while {operation}. Please try again later or contact administrator.",
|
106
115
|
)
|
107
116
|
except Exception as e:
|
108
117
|
return _handler(
|
109
118
|
e,
|
110
119
|
category="Internal processing error",
|
111
|
-
description=f"An unexpected error occurred while {operation}. Please try again later or contact administrator."
|
120
|
+
description=f"An unexpected error occurred while {operation}. Please try again later or contact administrator.",
|
112
121
|
)
|
122
|
+
|
113
123
|
return sync_wrapper
|
124
|
+
|
114
125
|
return decorator
|
115
126
|
|
116
127
|
@staticmethod
|
117
128
|
def service_exception_handler(
|
118
129
|
operation: str,
|
119
130
|
logger: Optional[BaseLogger] = None,
|
120
|
-
fail_result_class: type[
|
131
|
+
fail_result_class: type[
|
132
|
+
BaseServiceGeneralResultsTransfers.Fail
|
133
|
+
] = BaseServiceGeneralResultsTransfers.Fail,
|
121
134
|
):
|
122
135
|
"""Decorator to handle service-related exceptions consistently."""
|
136
|
+
|
123
137
|
def decorator(func):
|
124
138
|
@wraps(func)
|
125
139
|
def wrapper(*args, **kwargs):
|
126
140
|
try:
|
127
141
|
return func(*args, **kwargs)
|
128
142
|
except Exception as e:
|
129
|
-
logger
|
143
|
+
if logger:
|
144
|
+
logger.error(
|
145
|
+
"Unexpected error occurred while %s: '%s'",
|
146
|
+
operation,
|
147
|
+
str(e),
|
148
|
+
exc_info=True,
|
149
|
+
)
|
130
150
|
return fail_result_class(
|
131
151
|
message=f"Failed {operation}",
|
132
152
|
description=f"An unexpected error occurred while {operation}. Please try again later or contact administrator.",
|
133
|
-
other="Internal processing error"
|
134
|
-
)
|
153
|
+
other="Internal processing error",
|
154
|
+
) # type: ignore
|
155
|
+
|
135
156
|
return wrapper
|
136
|
-
|
157
|
+
|
158
|
+
return decorator
|
@@ -1,32 +1,36 @@
|
|
1
1
|
from datetime import datetime, timezone
|
2
2
|
from fastapi import Request
|
3
3
|
from starlette.requests import HTTPConnection
|
4
|
-
from uuid import uuid4
|
4
|
+
from uuid import UUID, uuid4
|
5
5
|
from maleo_foundation.models.transfers.general.request import RequestContext
|
6
6
|
|
7
|
+
|
7
8
|
def extract_client_ip(conn: HTTPConnection) -> str:
|
8
9
|
"""Extract client IP with more robust handling of proxies"""
|
9
|
-
|
10
|
+
# * Check for X-Forwarded-For header (common when behind proxy/load balancer)
|
10
11
|
x_forwarded_for = conn.headers.get("X-Forwarded-For")
|
11
12
|
if x_forwarded_for:
|
12
|
-
|
13
|
+
# * The client's IP is the first one in the list
|
13
14
|
ips = [ip.strip() for ip in x_forwarded_for.split(",")]
|
14
15
|
return ips[0]
|
15
16
|
|
16
|
-
|
17
|
+
# * Check for X-Real-IP header (used by some proxies)
|
17
18
|
x_real_ip = conn.headers.get("X-Real-IP")
|
18
19
|
if x_real_ip:
|
19
20
|
return x_real_ip
|
20
21
|
|
21
|
-
|
22
|
+
# * Fall back to direct client connection
|
22
23
|
return conn.client.host if conn.client else "unknown"
|
23
24
|
|
25
|
+
|
24
26
|
def extract_request_context(request: Request) -> RequestContext:
|
25
27
|
headers = request.headers
|
26
28
|
|
27
29
|
request_id = headers.get("x-request-id")
|
28
30
|
if request_id is None:
|
29
31
|
request_id = uuid4()
|
32
|
+
else:
|
33
|
+
request_id = UUID(request_id)
|
30
34
|
|
31
35
|
ip_address = extract_client_ip(request)
|
32
36
|
|
@@ -42,7 +46,15 @@ def extract_request_context(request: Request) -> RequestContext:
|
|
42
46
|
path_params=None if not request.path_params else request.path_params,
|
43
47
|
query_params=None if not request.query_params else str(request.query_params),
|
44
48
|
ip_address=ip_address,
|
45
|
-
is_internal=
|
49
|
+
is_internal=(
|
50
|
+
None
|
51
|
+
if ip_address == "unknown"
|
52
|
+
else (
|
53
|
+
ip_address.startswith("10.")
|
54
|
+
or ip_address.startswith("192.168.")
|
55
|
+
or ip_address.startswith("172.")
|
56
|
+
)
|
57
|
+
),
|
46
58
|
user_agent=headers.get("user-agent"),
|
47
59
|
ua_browser=ua_browser,
|
48
60
|
ua_mobile=headers.get("sec-ch-ua-mobile"),
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import re
|
2
2
|
from enum import StrEnum
|
3
3
|
|
4
|
+
|
4
5
|
class CaseFormatter:
|
5
6
|
class Case(StrEnum):
|
6
7
|
CAMEL = "camel"
|
@@ -10,19 +11,19 @@ class CaseFormatter:
|
|
10
11
|
@staticmethod
|
11
12
|
def to_camel_case(text: str) -> str:
|
12
13
|
"""Converts snake_case or PascalCase to camelCase."""
|
13
|
-
words = re.split(r
|
14
|
+
words = re.split(r"[_\s]", text) # Handle snake_case and spaces
|
14
15
|
return words[0].lower() + "".join(word.capitalize() for word in words[1:])
|
15
16
|
|
16
17
|
@staticmethod
|
17
18
|
def to_pascal_case(text: str) -> str:
|
18
19
|
"""Converts snake_case or camelCase to PascalCase."""
|
19
|
-
words = re.split(r
|
20
|
+
words = re.split(r"[_\s]", text)
|
20
21
|
return "".join(word.capitalize() for word in words)
|
21
22
|
|
22
23
|
@staticmethod
|
23
24
|
def to_snake_case(text: str) -> str:
|
24
25
|
"""Converts camelCase or PascalCase to snake_case."""
|
25
|
-
return re.sub(r
|
26
|
+
return re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", text).lower()
|
26
27
|
|
27
28
|
@staticmethod
|
28
29
|
def convert(text: str, target: Case) -> str:
|
@@ -34,4 +35,4 @@ class CaseFormatter:
|
|
34
35
|
elif target == CaseFormatter.Case.SNAKE:
|
35
36
|
return CaseFormatter.to_snake_case(text)
|
36
37
|
else:
|
37
|
-
raise ValueError(f"Invalid target case: {target}.")
|
38
|
+
raise ValueError(f"Invalid target case: {target}.")
|