maleo-foundation 0.2.16__py3-none-any.whl → 0.2.17__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/client/services/encryption/aes.py +41 -11
- maleo_foundation/client/services/encryption/rsa.py +59 -16
- maleo_foundation/client/services/hash/bcrypt.py +24 -9
- maleo_foundation/client/services/hash/hmac.py +26 -8
- maleo_foundation/client/services/hash/sha256.py +16 -6
- maleo_foundation/client/services/key.py +34 -12
- maleo_foundation/client/services/signature.py +45 -12
- maleo_foundation/client/services/token.py +68 -16
- maleo_foundation/expanded_types/client.py +6 -3
- maleo_foundation/expanded_types/encryption/aes.py +2 -1
- maleo_foundation/expanded_types/encryption/rsa.py +2 -1
- maleo_foundation/expanded_types/general.py +2 -1
- maleo_foundation/expanded_types/hash.py +2 -1
- maleo_foundation/expanded_types/key.py +2 -1
- maleo_foundation/expanded_types/query.py +6 -3
- maleo_foundation/expanded_types/service.py +6 -3
- maleo_foundation/expanded_types/signature.py +2 -1
- maleo_foundation/expanded_types/token.py +2 -1
- maleo_foundation/managers/client/base.py +5 -1
- maleo_foundation/managers/client/google/storage.py +5 -1
- maleo_foundation/managers/client/maleo.py +20 -4
- maleo_foundation/managers/db.py +21 -6
- maleo_foundation/managers/service.py +108 -23
- maleo_foundation/middlewares/authentication.py +35 -8
- maleo_foundation/middlewares/base.py +53 -15
- maleo_foundation/models/table.py +21 -4
- maleo_foundation/models/transfers/parameters/service.py +30 -4
- maleo_foundation/utils/controller.py +23 -6
- maleo_foundation/utils/dependencies/auth.py +14 -4
- maleo_foundation/utils/exceptions.py +21 -6
- maleo_foundation/utils/loaders/credential/google.py +3 -1
- maleo_foundation/utils/query.py +38 -7
- {maleo_foundation-0.2.16.dist-info → maleo_foundation-0.2.17.dist-info}/METADATA +1 -1
- {maleo_foundation-0.2.16.dist-info → maleo_foundation-0.2.17.dist-info}/RECORD +36 -36
- {maleo_foundation-0.2.16.dist-info → maleo_foundation-0.2.17.dist-info}/WHEEL +0 -0
- {maleo_foundation-0.2.16.dist-info → maleo_foundation-0.2.17.dist-info}/top_level.txt +0 -0
@@ -7,12 +7,18 @@ from maleo_foundation.authentication import Credentials, User
|
|
7
7
|
from maleo_foundation.enums import BaseEnums
|
8
8
|
from maleo_foundation.client.manager import MaleoFoundationClientManager
|
9
9
|
from maleo_foundation.models.schemas import BaseGeneralSchemas
|
10
|
-
from maleo_foundation.models.transfers.parameters.token
|
10
|
+
from maleo_foundation.models.transfers.parameters.token \
|
11
|
+
import MaleoFoundationTokenParametersTransfers
|
11
12
|
from maleo_foundation.utils.exceptions import BaseExceptions
|
12
13
|
from maleo_foundation.utils.logging import MiddlewareLogger
|
13
14
|
|
14
15
|
class Backend(AuthenticationBackend):
|
15
|
-
def __init__(
|
16
|
+
def __init__(
|
17
|
+
self,
|
18
|
+
keys:BaseGeneralSchemas.RSAKeys,
|
19
|
+
logger:MiddlewareLogger,
|
20
|
+
maleo_foundation:MaleoFoundationClientManager
|
21
|
+
):
|
16
22
|
super().__init__()
|
17
23
|
self._keys = keys
|
18
24
|
self._logger = logger
|
@@ -29,8 +35,14 @@ class Backend(AuthenticationBackend):
|
|
29
35
|
raise AuthenticationError("Authorization scheme must be Bearer token")
|
30
36
|
|
31
37
|
#* Decode token
|
32
|
-
decode_token_parameters =
|
33
|
-
|
38
|
+
decode_token_parameters = (
|
39
|
+
MaleoFoundationTokenParametersTransfers
|
40
|
+
.Decode(key=self._keys.public, token=token)
|
41
|
+
)
|
42
|
+
decode_token_result = (
|
43
|
+
self._maleo_foundation.services.token
|
44
|
+
.decode(parameters=decode_token_parameters)
|
45
|
+
)
|
34
46
|
if decode_token_result.success:
|
35
47
|
payload = decode_token_result.data
|
36
48
|
return (
|
@@ -50,8 +62,14 @@ class Backend(AuthenticationBackend):
|
|
50
62
|
if "token" in conn.cookies:
|
51
63
|
token = conn.cookies["token"]
|
52
64
|
#* Decode token
|
53
|
-
decode_token_parameters =
|
54
|
-
|
65
|
+
decode_token_parameters = (
|
66
|
+
MaleoFoundationTokenParametersTransfers
|
67
|
+
.Decode(key=self._keys.public, token=token)
|
68
|
+
)
|
69
|
+
decode_token_result = (
|
70
|
+
self._maleo_foundation.services.token
|
71
|
+
.decode(parameters=decode_token_parameters)
|
72
|
+
)
|
55
73
|
if decode_token_result.success:
|
56
74
|
payload = decode_token_result.data
|
57
75
|
return (
|
@@ -70,7 +88,12 @@ class Backend(AuthenticationBackend):
|
|
70
88
|
|
71
89
|
return Credentials(), User(authenticated=False)
|
72
90
|
|
73
|
-
def add_authentication_middleware(
|
91
|
+
def add_authentication_middleware(
|
92
|
+
app:FastAPI,
|
93
|
+
keys:BaseGeneralSchemas.RSAKeys,
|
94
|
+
logger:MiddlewareLogger,
|
95
|
+
maleo_foundation:MaleoFoundationClientManager
|
96
|
+
) -> None:
|
74
97
|
"""
|
75
98
|
Adds Authentication middleware to the FastAPI application.
|
76
99
|
|
@@ -96,4 +119,8 @@ def add_authentication_middleware(app:FastAPI, keys:BaseGeneralSchemas.RSAKeys,
|
|
96
119
|
add_authentication_middleware(app=app, limit=10, window=1, cleanup_interval=60, ip_timeout=300)
|
97
120
|
```
|
98
121
|
"""
|
99
|
-
app.add_middleware(
|
122
|
+
app.add_middleware(
|
123
|
+
AuthenticationMiddleware,
|
124
|
+
backend=Backend(keys, logger, maleo_foundation),
|
125
|
+
on_error=BaseExceptions.authentication_error_handler
|
126
|
+
)
|
@@ -13,9 +13,12 @@ from maleo_foundation.enums import BaseEnums
|
|
13
13
|
from maleo_foundation.client.manager import MaleoFoundationClientManager
|
14
14
|
from maleo_foundation.models.schemas import BaseGeneralSchemas
|
15
15
|
from maleo_foundation.models.responses import BaseResponses
|
16
|
-
from maleo_foundation.models.transfers.general.token
|
17
|
-
|
18
|
-
from maleo_foundation.models.transfers.parameters.
|
16
|
+
from maleo_foundation.models.transfers.general.token \
|
17
|
+
import MaleoFoundationTokenGeneralTransfers
|
18
|
+
from maleo_foundation.models.transfers.parameters.token \
|
19
|
+
import MaleoFoundationTokenParametersTransfers
|
20
|
+
from maleo_foundation.models.transfers.parameters.signature \
|
21
|
+
import MaleoFoundationSignatureParametersTransfers
|
19
22
|
from maleo_foundation.utils.extractor import BaseExtractors
|
20
23
|
from maleo_foundation.utils.logging import MiddlewareLogger
|
21
24
|
|
@@ -89,14 +92,20 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
89
92
|
self._last_cleanup = now
|
90
93
|
self._logger.debug(f"Cleaned up request cache. Removed {len(inactive_ips)} inactive IPs. Current tracked IPs: {len(self._requests)}")
|
91
94
|
|
92
|
-
def _check_rate_limit(
|
95
|
+
def _check_rate_limit(
|
96
|
+
self,
|
97
|
+
client_ip:str
|
98
|
+
) -> bool:
|
93
99
|
"""Check if the client has exceeded their rate limit"""
|
94
100
|
with self._lock:
|
95
101
|
now = datetime.now() #* Define current timestamp
|
96
102
|
self._last_seen[client_ip] = now #* Update last seen timestamp for this IP
|
97
103
|
|
98
104
|
#* Filter requests within the window
|
99
|
-
self._requests[client_ip] = [
|
105
|
+
self._requests[client_ip] = [
|
106
|
+
timestamp for timestamp in self._requests[client_ip]
|
107
|
+
if now - timestamp <= self._window
|
108
|
+
]
|
100
109
|
|
101
110
|
#* Check if the request count exceeds the limit
|
102
111
|
if len(self._requests[client_ip]) >= self._limit:
|
@@ -106,7 +115,11 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
106
115
|
self._requests[client_ip].append(now)
|
107
116
|
return False
|
108
117
|
|
109
|
-
def _append_cors_headers(
|
118
|
+
def _append_cors_headers(
|
119
|
+
self,
|
120
|
+
request:Request,
|
121
|
+
response:Response
|
122
|
+
) -> Response:
|
110
123
|
origin = request.headers.get("Origin")
|
111
124
|
|
112
125
|
if origin in self._allow_origins:
|
@@ -131,18 +144,29 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
131
144
|
response.headers["X-Response-Timestamp"] = response_timestamp.isoformat() #* Define and add response timestamp header
|
132
145
|
#* Generate signature header
|
133
146
|
message = f"{request.method}|{request.url.path}|{request_timestamp.isoformat()}|{response_timestamp.isoformat()}|{str(process_time)}"
|
134
|
-
sign_parameters =
|
147
|
+
sign_parameters = (
|
148
|
+
MaleoFoundationSignatureParametersTransfers
|
149
|
+
.Sign(key=self._keys.private, password=self._keys.password, message=message)
|
150
|
+
)
|
135
151
|
sign_result = self._maleo_foundation.services.signature.sign(parameters=sign_parameters)
|
136
152
|
if sign_result.success:
|
137
153
|
response.headers["X-Signature"] = sign_result.data.signature
|
138
154
|
response = self._append_cors_headers(request=request, response=response) #* Re-append CORS headers
|
139
|
-
if authentication.user.is_authenticated
|
140
|
-
and authentication.credentials.token_type == BaseEnums.TokenType.REFRESH
|
141
|
-
and authentication.credentials.payload is not None
|
142
|
-
and (response.status_code >= 200 and response.status_code < 300)
|
155
|
+
if (authentication.user.is_authenticated
|
156
|
+
and authentication.credentials.token_type == BaseEnums.TokenType.REFRESH
|
157
|
+
and authentication.credentials.payload is not None
|
158
|
+
and (response.status_code >= 200 and response.status_code < 300)
|
159
|
+
):
|
143
160
|
#* Regenerate new authorization
|
144
|
-
payload =
|
145
|
-
|
161
|
+
payload = (
|
162
|
+
MaleoFoundationTokenGeneralTransfers
|
163
|
+
.BaseEncodePayload
|
164
|
+
.model_validate(authentication.credentials.payload.model_dump())
|
165
|
+
)
|
166
|
+
parameters = (
|
167
|
+
MaleoFoundationTokenParametersTransfers
|
168
|
+
.Encode(key=self._keys.private, password=self._keys.password, payload=payload)
|
169
|
+
)
|
146
170
|
result = self._maleo_foundation.services.token.encode(parameters=parameters)
|
147
171
|
if result.success:
|
148
172
|
response.headers["X-New-Authorization"] = result.data.token
|
@@ -160,7 +184,14 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
160
184
|
log_level:str = "info",
|
161
185
|
client_ip:str = "unknown"
|
162
186
|
) -> Response:
|
163
|
-
response = self._add_response_headers(
|
187
|
+
response = self._add_response_headers(
|
188
|
+
request,
|
189
|
+
authentication,
|
190
|
+
response,
|
191
|
+
request_timestamp,
|
192
|
+
response_timestamp,
|
193
|
+
process_time
|
194
|
+
)
|
164
195
|
log_func = getattr(self._logger, log_level)
|
165
196
|
log_func(
|
166
197
|
f"Request {authentication_info} | IP: {client_ip} | Host: {request.client.host} | Port: {request.client.port} | Method: {request.method} | URL: {request.url.path} | "
|
@@ -199,7 +230,14 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
199
230
|
f"Headers: {dict(request.headers)} - Response | Status: 500 | Exception:\n{json.dumps(error_details, indent=4)}"
|
200
231
|
)
|
201
232
|
|
202
|
-
return self._add_response_headers(
|
233
|
+
return self._add_response_headers(
|
234
|
+
request,
|
235
|
+
authentication,
|
236
|
+
response,
|
237
|
+
request_timestamp,
|
238
|
+
response_timestamp,
|
239
|
+
process_time
|
240
|
+
)
|
203
241
|
|
204
242
|
async def _request_processor(self, request:Request) -> Optional[Response]:
|
205
243
|
return None
|
maleo_foundation/models/table.py
CHANGED
@@ -18,12 +18,29 @@ class BaseTable:
|
|
18
18
|
uuid = Column(UUID, default=uuid4, unique=True, nullable=False)
|
19
19
|
|
20
20
|
#* Timestamps
|
21
|
-
created_at = Column(
|
22
|
-
|
21
|
+
created_at = Column(
|
22
|
+
TIMESTAMP(timezone=True),
|
23
|
+
server_default=func.now(),
|
24
|
+
nullable=False
|
25
|
+
)
|
26
|
+
updated_at = Column(
|
27
|
+
TIMESTAMP(timezone=True),
|
28
|
+
server_default=func.now(),
|
29
|
+
onupdate=func.now(),
|
30
|
+
nullable=False
|
31
|
+
)
|
23
32
|
deleted_at = Column(TIMESTAMP(timezone=True))
|
24
33
|
restored_at = Column(TIMESTAMP(timezone=True))
|
25
34
|
deactivated_at = Column(TIMESTAMP(timezone=True))
|
26
|
-
activated_at = Column(
|
35
|
+
activated_at = Column(
|
36
|
+
TIMESTAMP(timezone=True),
|
37
|
+
server_default=func.now(),
|
38
|
+
nullable=False
|
39
|
+
)
|
27
40
|
|
28
41
|
#* Statuses
|
29
|
-
status = Column(
|
42
|
+
status = Column(
|
43
|
+
Enum(BaseEnums.StatusType, name="statustype"),
|
44
|
+
default=BaseEnums.StatusType.ACTIVE,
|
45
|
+
nullable=False
|
46
|
+
)
|
@@ -27,7 +27,13 @@ class BaseServiceParametersTransfers:
|
|
27
27
|
parts = item.split('.')
|
28
28
|
if len(parts) == 2 and parts[1].lower() in ["asc", "desc"]:
|
29
29
|
try:
|
30
|
-
sort_columns.append(
|
30
|
+
sort_columns.append(
|
31
|
+
BaseGeneralSchemas
|
32
|
+
.SortColumn(
|
33
|
+
name=parts[0],
|
34
|
+
order=BaseEnums.SortOrder(parts[1].lower())
|
35
|
+
)
|
36
|
+
)
|
31
37
|
except ValueError:
|
32
38
|
continue
|
33
39
|
|
@@ -64,7 +70,14 @@ class BaseServiceParametersTransfers:
|
|
64
70
|
|
65
71
|
#* Only add filter if at least one date is specified
|
66
72
|
if from_date or to_date:
|
67
|
-
date_filters.append(
|
73
|
+
date_filters.append(
|
74
|
+
BaseGeneralSchemas
|
75
|
+
.DateFilter(
|
76
|
+
name=name,
|
77
|
+
from_date=from_date,
|
78
|
+
to_date=to_date
|
79
|
+
)
|
80
|
+
)
|
68
81
|
|
69
82
|
#* Update date_filters
|
70
83
|
self.date_filters = date_filters
|
@@ -88,7 +101,13 @@ class BaseServiceParametersTransfers:
|
|
88
101
|
parts = item.split('.')
|
89
102
|
if len(parts) == 2 and parts[1].lower() in ["asc", "desc"]:
|
90
103
|
try:
|
91
|
-
sort_columns.append(
|
104
|
+
sort_columns.append(
|
105
|
+
BaseGeneralSchemas
|
106
|
+
.SortColumn(
|
107
|
+
name=parts[0],
|
108
|
+
order=BaseEnums.SortOrder(parts[1].lower())
|
109
|
+
)
|
110
|
+
)
|
92
111
|
except ValueError:
|
93
112
|
continue
|
94
113
|
|
@@ -125,7 +144,14 @@ class BaseServiceParametersTransfers:
|
|
125
144
|
|
126
145
|
#* Only add filter if at least one date is specified
|
127
146
|
if from_date or to_date:
|
128
|
-
date_filters.append(
|
147
|
+
date_filters.append(
|
148
|
+
BaseGeneralSchemas
|
149
|
+
.DateFilter(
|
150
|
+
name=name,
|
151
|
+
from_date=from_date,
|
152
|
+
to_date=to_date
|
153
|
+
)
|
154
|
+
)
|
129
155
|
|
130
156
|
#* Update date_filters
|
131
157
|
self.date_filters = date_filters
|
@@ -3,8 +3,10 @@ from functools import wraps
|
|
3
3
|
from typing import Awaitable, Callable, Dict, List
|
4
4
|
from maleo_foundation.types import BaseTypes
|
5
5
|
from maleo_foundation.models.responses import BaseResponses
|
6
|
-
from maleo_foundation.models.transfers.parameters.general
|
7
|
-
|
6
|
+
from maleo_foundation.models.transfers.parameters.general \
|
7
|
+
import BaseGeneralParametersTransfers
|
8
|
+
from maleo_foundation.models.transfers.results.service.controllers.rest \
|
9
|
+
import BaseServiceRESTControllerResults
|
8
10
|
from maleo_foundation.expanded_types.general import BaseGeneralExpandedTypes
|
9
11
|
|
10
12
|
class BaseControllerUtils:
|
@@ -33,7 +35,11 @@ class BaseControllerUtils:
|
|
33
35
|
if dependent in expand:
|
34
36
|
other = f"'{dependency}' must also be expanded if '{dependent}' is expanded"
|
35
37
|
content = BaseResponses.InvalidExpand(other=other).model_dump()
|
36
|
-
return BaseServiceRESTControllerResults(
|
38
|
+
return BaseServiceRESTControllerResults(
|
39
|
+
success=False,
|
40
|
+
content=content,
|
41
|
+
status_code=status.HTTP_400_BAD_REQUEST
|
42
|
+
)
|
37
43
|
|
38
44
|
#* Call the original function
|
39
45
|
result = await func(parameters, *args, **kwargs)
|
@@ -42,18 +48,29 @@ class BaseControllerUtils:
|
|
42
48
|
return result
|
43
49
|
|
44
50
|
#* Process the fields if needed
|
45
|
-
if result.success
|
51
|
+
if (result.success
|
52
|
+
and result.content.get("data", None) is not None
|
53
|
+
and field_expansion_processors is not None
|
54
|
+
):
|
46
55
|
data = result.content["data"]
|
47
56
|
if isinstance(data, List):
|
48
57
|
for idx, dt in enumerate(data):
|
49
58
|
for processor in field_expansion_processors:
|
50
59
|
raw_parameters = {"data": dt, "expand": expand}
|
51
|
-
parameters =
|
60
|
+
parameters = (
|
61
|
+
BaseGeneralParametersTransfers
|
62
|
+
.FieldExpansionProcessor
|
63
|
+
.model_validate(raw_parameters)
|
64
|
+
)
|
52
65
|
dt = processor(parameters)
|
53
66
|
data[idx] = dt
|
54
67
|
elif isinstance(data, Dict):
|
55
68
|
raw_parameters = {"data": data, "expand": expand}
|
56
|
-
parameters =
|
69
|
+
parameters = (
|
70
|
+
BaseGeneralParametersTransfers
|
71
|
+
.FieldExpansionProcessor
|
72
|
+
.model_validate(raw_parameters)
|
73
|
+
)
|
57
74
|
for processor in field_expansion_processors:
|
58
75
|
data = processor(parameters)
|
59
76
|
result.content["data"] = data
|
@@ -6,9 +6,19 @@ from maleo_foundation.authorization import TOKEN_SCHEME, Authorization
|
|
6
6
|
|
7
7
|
class AuthDependencies:
|
8
8
|
@staticmethod
|
9
|
-
def authentication(
|
10
|
-
|
9
|
+
def authentication(
|
10
|
+
request:Request
|
11
|
+
) -> Authentication:
|
12
|
+
return Authentication(
|
13
|
+
credentials=request.auth,
|
14
|
+
user=request.user
|
15
|
+
)
|
11
16
|
|
12
17
|
@staticmethod
|
13
|
-
def authorization(
|
14
|
-
|
18
|
+
def authorization(
|
19
|
+
token:HTTPAuthorizationCredentials = Security(TOKEN_SCHEME)
|
20
|
+
) -> Authorization:
|
21
|
+
return Authorization(
|
22
|
+
scheme=token.scheme,
|
23
|
+
credentials=token.credentials
|
24
|
+
)
|
@@ -6,24 +6,39 @@ from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
6
6
|
from sqlalchemy.exc import SQLAlchemyError
|
7
7
|
from typing import Optional
|
8
8
|
from maleo_foundation.models.responses import BaseResponses
|
9
|
-
from maleo_foundation.models.transfers.results.service.general
|
10
|
-
|
9
|
+
from maleo_foundation.models.transfers.results.service.general \
|
10
|
+
import BaseServiceGeneralResultsTransfers
|
11
|
+
from maleo_foundation.models.transfers.results.service.query \
|
12
|
+
import BaseServiceQueryResultsTransfers
|
11
13
|
from maleo_foundation.utils.logging import BaseLogger
|
12
14
|
|
13
15
|
class BaseExceptions:
|
14
16
|
@staticmethod
|
15
17
|
def authentication_error_handler(request:Request, exc:Exception):
|
16
|
-
return JSONResponse(
|
18
|
+
return JSONResponse(
|
19
|
+
content=BaseResponses.Unauthorized(other=str(exc)).model_dump(mode="json"),
|
20
|
+
status_code=status.HTTP_401_UNAUTHORIZED
|
21
|
+
)
|
17
22
|
|
18
23
|
@staticmethod
|
19
24
|
async def validation_exception_handler(request:Request, exc:RequestValidationError):
|
20
|
-
return JSONResponse(
|
25
|
+
return JSONResponse(
|
26
|
+
content=BaseResponses.ValidationError(other=exc.errors()).model_dump(mode="json"),
|
27
|
+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY
|
28
|
+
)
|
21
29
|
|
22
30
|
@staticmethod
|
23
31
|
async def http_exception_handler(request:Request, exc:StarletteHTTPException):
|
24
32
|
if exc.status_code in BaseResponses.other_responses:
|
25
|
-
return JSONResponse(
|
26
|
-
|
33
|
+
return JSONResponse(
|
34
|
+
content=BaseResponses.other_responses[exc.status_code]["model"]().model_dump(mode="json"),
|
35
|
+
status_code=exc.status_code
|
36
|
+
)
|
37
|
+
|
38
|
+
return JSONResponse(
|
39
|
+
content=BaseResponses.ServerError().model_dump(mode="json"),
|
40
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
|
41
|
+
)
|
27
42
|
|
28
43
|
@staticmethod
|
29
44
|
def database_exception_handler(
|
@@ -15,7 +15,9 @@ class GoogleCredentialsLoader:
|
|
15
15
|
if credentials_path is not None:
|
16
16
|
credentials_path = Path(credentials_path)
|
17
17
|
if credentials_path.exists() or credentials_path.is_file():
|
18
|
-
credentials = Credentials.from_service_account_file(
|
18
|
+
credentials = Credentials.from_service_account_file(
|
19
|
+
filename=str(credentials_path)
|
20
|
+
)
|
19
21
|
else:
|
20
22
|
credentials, _ = default()
|
21
23
|
return credentials
|
maleo_foundation/utils/query.py
CHANGED
@@ -10,7 +10,12 @@ from maleo_foundation.extended_types import ExtendedTypes
|
|
10
10
|
|
11
11
|
class BaseQueryUtils:
|
12
12
|
@staticmethod
|
13
|
-
def filter_column(
|
13
|
+
def filter_column(
|
14
|
+
query:Query,
|
15
|
+
table:Type[DeclarativeMeta],
|
16
|
+
column:str,
|
17
|
+
value:BaseTypes.OptionalAny
|
18
|
+
) -> Query:
|
14
19
|
if not value:
|
15
20
|
return query
|
16
21
|
column_attr = getattr(table, column, None)
|
@@ -24,7 +29,12 @@ class BaseQueryUtils:
|
|
24
29
|
return query
|
25
30
|
|
26
31
|
@staticmethod
|
27
|
-
def filter_ids(
|
32
|
+
def filter_ids(
|
33
|
+
query:Query,
|
34
|
+
table:Type[DeclarativeMeta],
|
35
|
+
column:str,
|
36
|
+
ids:BaseTypes.OptionalListOfIntegers
|
37
|
+
) -> Query:
|
28
38
|
if ids is not None:
|
29
39
|
column_attr = getattr(table, column, None)
|
30
40
|
if column_attr:
|
@@ -33,7 +43,11 @@ class BaseQueryUtils:
|
|
33
43
|
return query
|
34
44
|
|
35
45
|
@staticmethod
|
36
|
-
def filter_timestamps(
|
46
|
+
def filter_timestamps(
|
47
|
+
query:Query,
|
48
|
+
table:Type[DeclarativeMeta],
|
49
|
+
date_filters:ExtendedTypes.ListOfDateFilters
|
50
|
+
) -> Query:
|
37
51
|
if date_filters and len(date_filters) > 0:
|
38
52
|
for date_filter in date_filters:
|
39
53
|
try:
|
@@ -42,7 +56,12 @@ class BaseQueryUtils:
|
|
42
56
|
column_attr:InstrumentedAttribute = getattr(table, date_filter.name)
|
43
57
|
if isinstance(column.type, (TIMESTAMP, DATE)):
|
44
58
|
if date_filter.from_date and date_filter.to_date:
|
45
|
-
query = query.filter(
|
59
|
+
query = query.filter(
|
60
|
+
column_attr.between(
|
61
|
+
date_filter.from_date,
|
62
|
+
date_filter.to_date
|
63
|
+
)
|
64
|
+
)
|
46
65
|
elif date_filter.from_date:
|
47
66
|
query = query.filter(column_attr >= date_filter.from_date)
|
48
67
|
elif date_filter.to_date:
|
@@ -52,14 +71,22 @@ class BaseQueryUtils:
|
|
52
71
|
return query
|
53
72
|
|
54
73
|
@staticmethod
|
55
|
-
def filter_statuses(
|
74
|
+
def filter_statuses(
|
75
|
+
query:Query,
|
76
|
+
table:Type[DeclarativeMeta],
|
77
|
+
statuses:BaseTypes.OptionalListOfStatuses
|
78
|
+
) -> Query:
|
56
79
|
if statuses is not None:
|
57
80
|
status_filters = [table.status == status for status in statuses]
|
58
81
|
query = query.filter(or_(*status_filters))
|
59
82
|
return query
|
60
83
|
|
61
84
|
@staticmethod
|
62
|
-
def filter_search(
|
85
|
+
def filter_search(
|
86
|
+
query:Query,
|
87
|
+
table:Type[DeclarativeMeta],
|
88
|
+
search:BaseTypes.OptionalString
|
89
|
+
) -> Query:
|
63
90
|
if search:
|
64
91
|
search_term = f"%{search}%" #* Use wildcard for partial matching
|
65
92
|
search_filters = []
|
@@ -78,7 +105,11 @@ class BaseQueryUtils:
|
|
78
105
|
return query
|
79
106
|
|
80
107
|
@staticmethod
|
81
|
-
def sort(
|
108
|
+
def sort(
|
109
|
+
query:Query,
|
110
|
+
table:Type[DeclarativeMeta],
|
111
|
+
sort_columns:ExtendedTypes.ListOfSortColumns
|
112
|
+
) -> Query:
|
82
113
|
for sort_column in sort_columns:
|
83
114
|
try:
|
84
115
|
sort_col = getattr(table, sort_column.name)
|