maleo-foundation 0.3.72__py3-none-any.whl → 0.3.74__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 +2 -48
- maleo_foundation/constants.py +2 -0
- maleo_foundation/enums.py +7 -1
- maleo_foundation/managers/middleware.py +8 -8
- maleo_foundation/managers/service.py +8 -4
- maleo_foundation/middlewares/authentication.py +3 -2
- maleo_foundation/middlewares/base.py +312 -197
- maleo_foundation/models/schemas/general.py +1 -127
- maleo_foundation/models/transfers/general/authentication.py +35 -0
- maleo_foundation/{authorization.py → models/transfers/general/authorization.py} +0 -3
- maleo_foundation/models/transfers/general/configurations/__init__.py +2 -0
- maleo_foundation/models/transfers/general/configurations/middleware.py +6 -7
- maleo_foundation/models/transfers/general/operation.py +192 -30
- maleo_foundation/models/transfers/general/request.py +13 -19
- maleo_foundation/models/transfers/general/response.py +14 -0
- maleo_foundation/models/transfers/general/service.py +9 -0
- maleo_foundation/models/transfers/general/user_agent.py +34 -0
- maleo_foundation/utils/exceptions/client.py +22 -23
- maleo_foundation/utils/exceptions/service.py +21 -22
- maleo_foundation/utils/extractor.py +49 -19
- maleo_foundation/utils/logging.py +40 -0
- maleo_foundation/utils/parser.py +7 -0
- {maleo_foundation-0.3.72.dist-info → maleo_foundation-0.3.74.dist-info}/METADATA +3 -1
- {maleo_foundation-0.3.72.dist-info → maleo_foundation-0.3.74.dist-info}/RECORD +26 -24
- maleo_foundation/utils/dependencies/__init__.py +0 -6
- maleo_foundation/utils/dependencies/auth.py +0 -17
- maleo_foundation/utils/dependencies/context.py +0 -8
- {maleo_foundation-0.3.72.dist-info → maleo_foundation-0.3.74.dist-info}/WHEEL +0 -0
- {maleo_foundation-0.3.72.dist-info → maleo_foundation-0.3.74.dist-info}/top_level.txt +0 -0
@@ -1,21 +1,35 @@
|
|
1
1
|
import json
|
2
|
+
import re
|
2
3
|
import threading
|
3
|
-
import time
|
4
4
|
import traceback
|
5
5
|
from collections import defaultdict
|
6
6
|
from datetime import datetime, timedelta, timezone
|
7
|
-
from typing import Awaitable, Callable, Optional,
|
7
|
+
from typing import Awaitable, Callable, Optional, Dict, List
|
8
|
+
from uuid import UUID, uuid4
|
8
9
|
|
9
10
|
from fastapi import FastAPI, Request, Response, status
|
10
11
|
from fastapi.responses import JSONResponse
|
11
12
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
12
13
|
from starlette.types import ASGIApp
|
13
14
|
|
14
|
-
from maleo_foundation.authentication import Authentication
|
15
15
|
from maleo_foundation.enums import BaseEnums
|
16
16
|
from maleo_foundation.client.manager import MaleoFoundationClientManager
|
17
17
|
from maleo_foundation.models.schemas import BaseGeneralSchemas
|
18
18
|
from maleo_foundation.models.responses import BaseResponses
|
19
|
+
from maleo_foundation.models.transfers.general.authentication import Authentication
|
20
|
+
from maleo_foundation.models.transfers.general.operation import (
|
21
|
+
OperationOrigin,
|
22
|
+
OperationLayer,
|
23
|
+
OperationTarget,
|
24
|
+
OperationContext,
|
25
|
+
OperationMetadata,
|
26
|
+
OperationException,
|
27
|
+
OperationTimestamps,
|
28
|
+
OperationResult,
|
29
|
+
Operation,
|
30
|
+
)
|
31
|
+
from maleo_foundation.models.transfers.general.service import ServiceContext
|
32
|
+
from maleo_foundation.models.transfers.general.settings import Settings
|
19
33
|
from maleo_foundation.models.transfers.general.token import (
|
20
34
|
MaleoFoundationTokenGeneralTransfers,
|
21
35
|
)
|
@@ -26,7 +40,11 @@ from maleo_foundation.models.transfers.parameters.signature import (
|
|
26
40
|
MaleoFoundationSignatureParametersTransfers,
|
27
41
|
)
|
28
42
|
from maleo_foundation.models.transfers.general.request import RequestContext
|
29
|
-
from maleo_foundation.
|
43
|
+
from maleo_foundation.models.transfers.general.response import ResponseContext
|
44
|
+
from maleo_foundation.utils.extractor import (
|
45
|
+
extract_authentication,
|
46
|
+
extract_request_context,
|
47
|
+
)
|
30
48
|
from maleo_foundation.utils.logging import MiddlewareLogger
|
31
49
|
|
32
50
|
RequestProcessor = Callable[[Request], Awaitable[Optional[Response]]]
|
@@ -74,7 +92,7 @@ class RateLimiter:
|
|
74
92
|
self._requests[client_ip].append(now)
|
75
93
|
return False
|
76
94
|
|
77
|
-
def cleanup_old_data(self
|
95
|
+
def cleanup_old_data(self) -> None:
|
78
96
|
"""Clean up old request data to prevent memory growth."""
|
79
97
|
now = datetime.now()
|
80
98
|
if now - self._last_cleanup <= self.cleanup_interval:
|
@@ -100,10 +118,6 @@ class RateLimiter:
|
|
100
118
|
self._last_seen.pop(ip, None)
|
101
119
|
|
102
120
|
self._last_cleanup = now
|
103
|
-
logger.debug(
|
104
|
-
f"Cleaned up request cache. Removed {len(inactive_ips)} inactive IPs. "
|
105
|
-
f"Current tracked IPs: {len(self._requests)}"
|
106
|
-
)
|
107
121
|
|
108
122
|
|
109
123
|
class ResponseBuilder:
|
@@ -117,42 +131,26 @@ class ResponseBuilder:
|
|
117
131
|
self.keys = keys
|
118
132
|
self.maleo_foundation = maleo_foundation
|
119
133
|
|
120
|
-
def add_response_headers(
|
121
|
-
self,
|
122
|
-
authentication: Authentication,
|
123
|
-
response: Response,
|
124
|
-
request_context: RequestContext,
|
125
|
-
responded_at: datetime,
|
126
|
-
process_time: float,
|
127
|
-
) -> Response:
|
128
|
-
"""Add custom headers to response."""
|
129
|
-
# Basic headers
|
130
|
-
response.headers["X-Request-ID"] = str(request_context.request_id)
|
131
|
-
response.headers["X-Process-Time"] = str(process_time)
|
132
|
-
response.headers["X-Requested-At"] = request_context.requested_at.isoformat()
|
133
|
-
response.headers["X-Responded-At"] = responded_at.isoformat()
|
134
|
-
|
135
|
-
# Add signature header
|
136
|
-
self._add_signature_header(
|
137
|
-
response, request_context, responded_at, process_time
|
138
|
-
)
|
139
|
-
|
140
|
-
# Add new authorization header if needed
|
141
|
-
self._add_new_authorization_header(request_context, authentication, response)
|
142
|
-
|
143
|
-
return response
|
144
|
-
|
145
134
|
def _add_signature_header(
|
146
135
|
self,
|
147
|
-
|
148
|
-
|
136
|
+
operation_id: UUID,
|
137
|
+
request_id: UUID,
|
138
|
+
method: str,
|
139
|
+
url: str,
|
140
|
+
requested_at: datetime,
|
149
141
|
responded_at: datetime,
|
150
142
|
process_time: float,
|
143
|
+
response: Response,
|
151
144
|
) -> None:
|
152
145
|
"""Generate and add signature header."""
|
153
146
|
message = (
|
154
|
-
f"{
|
155
|
-
f"{
|
147
|
+
f"{str(operation_id)}|"
|
148
|
+
f"{str(request_id)}"
|
149
|
+
f"{method}|"
|
150
|
+
f"{url}|"
|
151
|
+
f"{requested_at.isoformat()}|"
|
152
|
+
f"{responded_at.isoformat()}|"
|
153
|
+
f"{str(process_time)}|"
|
156
154
|
)
|
157
155
|
|
158
156
|
sign_parameters = MaleoFoundationSignatureParametersTransfers.Sign(
|
@@ -165,6 +163,22 @@ class ResponseBuilder:
|
|
165
163
|
if sign_result.success and sign_result.data is not None:
|
166
164
|
response.headers["X-Signature"] = sign_result.data.signature
|
167
165
|
|
166
|
+
def _should_regenerate_auth(
|
167
|
+
self,
|
168
|
+
request_context: RequestContext,
|
169
|
+
authentication: Authentication,
|
170
|
+
response: Response,
|
171
|
+
) -> bool:
|
172
|
+
"""Check if authorization should be regenerated."""
|
173
|
+
if authentication.credentials.token is not None:
|
174
|
+
return (
|
175
|
+
authentication.user.is_authenticated
|
176
|
+
and authentication.credentials.token.type == BaseEnums.TokenType.REFRESH
|
177
|
+
and 200 <= response.status_code < 300
|
178
|
+
and "logout" not in request_context.url
|
179
|
+
)
|
180
|
+
return False
|
181
|
+
|
168
182
|
def _add_new_authorization_header(
|
169
183
|
self,
|
170
184
|
request_context: RequestContext,
|
@@ -190,84 +204,39 @@ class ResponseBuilder:
|
|
190
204
|
if result.success and result.data is not None:
|
191
205
|
response.headers["X-New-Authorization"] = result.data.token
|
192
206
|
|
193
|
-
def
|
207
|
+
def add_response_headers(
|
194
208
|
self,
|
209
|
+
operation_id: UUID,
|
195
210
|
request_context: RequestContext,
|
196
211
|
authentication: Authentication,
|
197
212
|
response: Response,
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
class RequestLogger:
|
211
|
-
"""Handles request/response logging."""
|
212
|
-
|
213
|
-
def __init__(self, logger: MiddlewareLogger):
|
214
|
-
self.logger = logger
|
215
|
-
|
216
|
-
def log_request_response(
|
217
|
-
self,
|
218
|
-
authentication: Authentication,
|
219
|
-
response: Response,
|
220
|
-
request_context: RequestContext,
|
221
|
-
log_level: str = "info",
|
222
|
-
) -> None:
|
223
|
-
"""Log request and response details."""
|
224
|
-
authentication_info = self._get_authentication_info(authentication)
|
225
|
-
|
226
|
-
log_func = getattr(self.logger, log_level)
|
227
|
-
log_func(
|
228
|
-
f"Request | ID: {request_context.request_id} {authentication_info} | "
|
229
|
-
f"IP: {request_context.ip_address} | Host: {request_context.host} | "
|
230
|
-
f"Method: {request_context.method} | URL: {request_context.url} | "
|
231
|
-
f"Query Parameters: {request_context.query_params} - "
|
232
|
-
f"Response | Status: {response.status_code}"
|
233
|
-
)
|
213
|
+
responded_at: datetime,
|
214
|
+
process_time: float,
|
215
|
+
) -> Response:
|
216
|
+
"""Add custom headers to response."""
|
217
|
+
# Basic headers
|
218
|
+
response.headers["X-Operation-Id"] = str(operation_id)
|
219
|
+
response.headers["X-Process-Time"] = str(process_time)
|
220
|
+
response.headers["X-Request-Id"] = str(request_context.request_id)
|
221
|
+
response.headers["X-Requested-At"] = request_context.requested_at.isoformat()
|
222
|
+
response.headers["X-Responded-At"] = responded_at.isoformat()
|
234
223
|
|
235
|
-
|
236
|
-
self
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
"request_context": request_context.model_dump(mode="json"),
|
246
|
-
"error": str(error),
|
247
|
-
"traceback": traceback.format_exc().split("\n"),
|
248
|
-
}
|
249
|
-
|
250
|
-
self.logger.error(
|
251
|
-
f"Request | ID: {request_context.request_id} {authentication_info} | "
|
252
|
-
f"IP: {request_context.ip_address} | Host: {request_context.host} | "
|
253
|
-
f"Method: {request_context.method} | URL: {request_context.url} | "
|
254
|
-
f"Query Parameters: {request_context.query_params} - "
|
255
|
-
f"Response | Status: 500 | Exception:\n{json.dumps(error_details, indent=4)}"
|
224
|
+
# Add signature header
|
225
|
+
self._add_signature_header(
|
226
|
+
operation_id=operation_id,
|
227
|
+
request_id=request_context.request_id,
|
228
|
+
method=request_context.method,
|
229
|
+
url=request_context.url,
|
230
|
+
requested_at=request_context.requested_at,
|
231
|
+
responded_at=responded_at,
|
232
|
+
process_time=process_time,
|
233
|
+
response=response,
|
256
234
|
)
|
257
235
|
|
258
|
-
|
259
|
-
|
260
|
-
if not authentication.user.is_authenticated:
|
261
|
-
return "| Unauthenticated"
|
262
|
-
|
263
|
-
if authentication.credentials.token is not None:
|
264
|
-
return (
|
265
|
-
f"| Token type: {authentication.credentials.token.type} | "
|
266
|
-
f"Username: {authentication.user.display_name} | "
|
267
|
-
f"Email: {authentication.user.identity}"
|
268
|
-
)
|
236
|
+
# Add new authorization header if needed
|
237
|
+
self._add_new_authorization_header(request_context, authentication, response)
|
269
238
|
|
270
|
-
return
|
239
|
+
return response
|
271
240
|
|
272
241
|
|
273
242
|
class BaseMiddleware(BaseHTTPMiddleware):
|
@@ -276,13 +245,10 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
276
245
|
def __init__(
|
277
246
|
self,
|
278
247
|
app: ASGIApp,
|
248
|
+
settings: Settings,
|
279
249
|
keys: BaseGeneralSchemas.RSAKeys,
|
280
250
|
logger: MiddlewareLogger,
|
281
251
|
maleo_foundation: MaleoFoundationClientManager,
|
282
|
-
allow_origins: Sequence[str] = (),
|
283
|
-
allow_methods: Sequence[str] = ("GET",),
|
284
|
-
allow_headers: Sequence[str] = (),
|
285
|
-
allow_credentials: bool = False,
|
286
252
|
limit: int = 10,
|
287
253
|
window: int = 1,
|
288
254
|
cleanup_interval: int = 60,
|
@@ -290,6 +256,9 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
290
256
|
):
|
291
257
|
super().__init__(app)
|
292
258
|
|
259
|
+
self._settings = settings
|
260
|
+
self._logger = logger
|
261
|
+
|
293
262
|
# Core components
|
294
263
|
self.rate_limiter = RateLimiter(
|
295
264
|
limit=limit,
|
@@ -298,130 +267,279 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
298
267
|
cleanup_interval=timedelta(seconds=cleanup_interval),
|
299
268
|
)
|
300
269
|
self.response_builder = ResponseBuilder(keys, maleo_foundation)
|
301
|
-
self.request_logger = RequestLogger(logger)
|
302
270
|
|
303
|
-
#
|
304
|
-
self.
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
271
|
+
# define service_context
|
272
|
+
self.service_context = ServiceContext(
|
273
|
+
key=self._settings.SERVICE_KEY, environment=self._settings.ENVIRONMENT
|
274
|
+
)
|
275
|
+
|
276
|
+
# define operation context
|
277
|
+
self.operation_context = OperationContext(
|
278
|
+
origin=OperationOrigin(
|
279
|
+
type=BaseEnums.OperationOrigin.SERVICE, properties=None
|
280
|
+
),
|
281
|
+
layer=OperationLayer(
|
282
|
+
type=BaseEnums.OperationLayer.MIDDLEWARE, properties=None
|
283
|
+
),
|
284
|
+
target=OperationTarget(
|
285
|
+
type=BaseEnums.OperationTarget.INTERNAL, properties=None
|
286
|
+
),
|
287
|
+
)
|
288
|
+
|
289
|
+
def _build_operation_metadata(
|
290
|
+
self, request_context: RequestContext
|
291
|
+
) -> OperationMetadata:
|
292
|
+
operation_type = BaseEnums.OperationType.OTHER
|
293
|
+
create_type = None
|
294
|
+
update_type = None
|
295
|
+
status_update_type = None
|
296
|
+
|
297
|
+
if request_context.method == "POST":
|
298
|
+
operation_type = BaseEnums.OperationType.CREATE
|
299
|
+
if request_context.url.endswith("/restore"):
|
300
|
+
create_type = BaseEnums.CreateType.RESTORE
|
301
|
+
else:
|
302
|
+
create_type = BaseEnums.CreateType.CREATE
|
303
|
+
elif request_context.method == "GET":
|
304
|
+
operation_type = BaseEnums.OperationType.READ
|
305
|
+
elif request_context.method in ["PATCH", "PUT"]:
|
306
|
+
operation_type = BaseEnums.OperationType.UPDATE
|
307
|
+
if request_context.method == "PUT":
|
308
|
+
update_type = BaseEnums.UpdateType.DATA
|
309
|
+
elif request_context.method == "PATCH":
|
310
|
+
if request_context.url.endswith("/status"):
|
311
|
+
update_type = BaseEnums.UpdateType.STATUS
|
312
|
+
if request_context.query_params is not None:
|
313
|
+
match = re.search(
|
314
|
+
r"[?&]action=([^&]+)", request_context.query_params
|
315
|
+
)
|
316
|
+
if match:
|
317
|
+
try:
|
318
|
+
status_update_type = BaseEnums.StatusUpdateType(
|
319
|
+
match.group(1)
|
320
|
+
)
|
321
|
+
except Exception:
|
322
|
+
pass
|
323
|
+
else:
|
324
|
+
update_type = BaseEnums.UpdateType.DATA
|
325
|
+
elif request_context.method == "DELETE":
|
326
|
+
operation_type = BaseEnums.OperationType.DELETE
|
327
|
+
|
328
|
+
metadata = OperationMetadata(
|
329
|
+
type=operation_type,
|
330
|
+
create_type=create_type,
|
331
|
+
update_type=update_type,
|
332
|
+
status_update_type=status_update_type,
|
333
|
+
)
|
334
|
+
|
335
|
+
return metadata
|
336
|
+
|
337
|
+
def _log_operation(
|
338
|
+
self,
|
339
|
+
operation: Operation,
|
340
|
+
log_level: BaseEnums.LogLevel = BaseEnums.LogLevel.INFO,
|
341
|
+
) -> None:
|
342
|
+
exc_info = (
|
343
|
+
log_level is BaseEnums.LogLevel.FATAL
|
344
|
+
or log_level is BaseEnums.LogLevel.ERROR
|
345
|
+
or log_level is BaseEnums.LogLevel.CRITICAL
|
346
|
+
)
|
347
|
+
|
348
|
+
self._logger.log(int(log_level), operation.to_log_string(), exc_info=exc_info)
|
349
|
+
|
350
|
+
return None
|
310
351
|
|
311
352
|
async def dispatch(
|
312
353
|
self, request: Request, call_next: RequestResponseEndpoint
|
313
354
|
) -> Response:
|
314
355
|
"""Main middleware dispatch method."""
|
315
356
|
# Setup
|
316
|
-
self.rate_limiter.cleanup_old_data(
|
357
|
+
self.rate_limiter.cleanup_old_data()
|
358
|
+
|
359
|
+
# Assign operation id
|
360
|
+
operation_id = request.headers.get("X-Operation-Id", None)
|
361
|
+
if operation_id is None:
|
362
|
+
operation_id = uuid4()
|
363
|
+
else:
|
364
|
+
operation_id = UUID(operation_id)
|
365
|
+
request.state.operation_id = operation_id
|
366
|
+
|
367
|
+
# Assign request id and timestamp
|
368
|
+
request.state.request_id = uuid4()
|
369
|
+
request.state.requested_at = datetime.now(tz=timezone.utc)
|
370
|
+
|
371
|
+
# Extract and assign request context
|
317
372
|
request_context = extract_request_context(request)
|
318
373
|
request.state.request_context = request_context
|
319
|
-
start_time = time.perf_counter()
|
320
|
-
authentication = Authentication(credentials=request.auth, user=request.user)
|
321
374
|
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
375
|
+
# Extract authentication
|
376
|
+
authentication = extract_authentication(request=request)
|
377
|
+
|
378
|
+
started_at = datetime.now(tz=timezone.utc)
|
379
|
+
|
380
|
+
# Rate limiting check
|
381
|
+
if self.rate_limiter.is_rate_limited(request_context):
|
382
|
+
finished_at = datetime.now(tz=timezone.utc)
|
383
|
+
operation_summary = "Rate limit exceeded for the request"
|
384
|
+
# define operation timestamps
|
385
|
+
operation_timestamps = OperationTimestamps(
|
386
|
+
started_at=started_at,
|
387
|
+
finished_at=finished_at,
|
388
|
+
duration=(finished_at - started_at).total_seconds(),
|
389
|
+
)
|
390
|
+
operation_exception = OperationException(
|
391
|
+
type=BaseEnums.ExceptionType.RATE_LIMIT,
|
392
|
+
raw="Rate limit exceeded",
|
393
|
+
traceback=[],
|
394
|
+
)
|
395
|
+
content = BaseResponses.RateLimitExceeded().model_dump() # type: ignore
|
396
|
+
operation_result = OperationResult.model_validate(content)
|
397
|
+
response = JSONResponse(
|
398
|
+
content=content,
|
399
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
400
|
+
)
|
401
|
+
return self._build_final_response(
|
402
|
+
operation_id=operation_id,
|
403
|
+
operation_summary=operation_summary,
|
404
|
+
request_context=request_context,
|
405
|
+
authentication=authentication,
|
406
|
+
operation_timestamps=operation_timestamps,
|
407
|
+
operation_exception=operation_exception,
|
408
|
+
operation_result=operation_result,
|
409
|
+
response=response,
|
410
|
+
log_level=BaseEnums.LogLevel.ERROR,
|
411
|
+
)
|
335
412
|
|
413
|
+
try:
|
336
414
|
# Main request processing
|
337
415
|
response = await call_next(request)
|
416
|
+
finished_at = datetime.now(tz=timezone.utc)
|
417
|
+
operation_summary = "Successfully processed the request"
|
418
|
+
# define operation timestamps
|
419
|
+
operation_timestamps = OperationTimestamps(
|
420
|
+
started_at=started_at,
|
421
|
+
finished_at=finished_at,
|
422
|
+
duration=(finished_at - started_at).total_seconds(),
|
423
|
+
)
|
338
424
|
return self._build_final_response(
|
339
|
-
|
425
|
+
operation_id=operation_id,
|
426
|
+
operation_summary=operation_summary,
|
427
|
+
request_context=request_context,
|
428
|
+
authentication=authentication,
|
429
|
+
operation_timestamps=operation_timestamps,
|
430
|
+
operation_result=None,
|
431
|
+
operation_exception=None,
|
432
|
+
response=response,
|
340
433
|
)
|
341
434
|
|
342
435
|
except Exception as e:
|
343
|
-
|
344
|
-
|
436
|
+
finished_at = datetime.now(tz=timezone.utc)
|
437
|
+
operation_summary = "Unexpected error occured for the request"
|
438
|
+
# define operation timestamps
|
439
|
+
operation_timestamps = OperationTimestamps(
|
440
|
+
started_at=started_at,
|
441
|
+
finished_at=finished_at,
|
442
|
+
duration=(finished_at - started_at).total_seconds(),
|
443
|
+
)
|
444
|
+
operation_exception = OperationException(
|
445
|
+
type=BaseEnums.ExceptionType.INTERNAL,
|
446
|
+
raw=str(e),
|
447
|
+
traceback=traceback.format_exc().splitlines(),
|
448
|
+
)
|
449
|
+
content = (BaseResponses.ServerError(other=str(e)).model_dump(),) # type: ignore
|
450
|
+
operation_result = OperationResult.model_validate(content)
|
451
|
+
response = JSONResponse(
|
452
|
+
content=content,
|
453
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
454
|
+
)
|
455
|
+
return self._build_final_response(
|
456
|
+
operation_id=operation_id,
|
457
|
+
operation_summary=operation_summary,
|
458
|
+
request_context=request_context,
|
459
|
+
authentication=authentication,
|
460
|
+
operation_timestamps=operation_timestamps,
|
461
|
+
operation_exception=operation_exception,
|
462
|
+
operation_result=operation_result,
|
463
|
+
response=response,
|
464
|
+
log_level=BaseEnums.LogLevel.ERROR,
|
345
465
|
)
|
346
|
-
|
347
|
-
async def _request_processor(self, request: Request) -> Optional[Response]:
|
348
|
-
"""Override this method for custom request preprocessing."""
|
349
|
-
return None
|
350
|
-
|
351
|
-
def _create_rate_limit_response(
|
352
|
-
self,
|
353
|
-
authentication: Authentication,
|
354
|
-
request_context: RequestContext,
|
355
|
-
start_time: float,
|
356
|
-
) -> Response:
|
357
|
-
"""Create rate limit exceeded response."""
|
358
|
-
response = JSONResponse(
|
359
|
-
content=BaseResponses.RateLimitExceeded().model_dump(), # type: ignore
|
360
|
-
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
361
|
-
)
|
362
|
-
|
363
|
-
return self._build_final_response(
|
364
|
-
authentication, response, request_context, start_time, log_level="warning"
|
365
|
-
)
|
366
466
|
|
367
467
|
def _build_final_response(
|
368
468
|
self,
|
469
|
+
operation_id: UUID,
|
470
|
+
operation_summary: str,
|
471
|
+
request_context: RequestContext,
|
369
472
|
authentication: Authentication,
|
473
|
+
operation_timestamps: OperationTimestamps,
|
474
|
+
operation_exception: Optional[OperationException],
|
475
|
+
operation_result: Optional[OperationResult],
|
370
476
|
response: Response,
|
371
|
-
|
372
|
-
start_time: float,
|
373
|
-
log_level: str = "info",
|
477
|
+
log_level: BaseEnums.LogLevel = BaseEnums.LogLevel.INFO,
|
374
478
|
) -> Response:
|
375
479
|
"""Build final response with headers and logging."""
|
480
|
+
# define operation metadata
|
481
|
+
operation_metadata = self._build_operation_metadata(
|
482
|
+
request_context=request_context
|
483
|
+
)
|
484
|
+
|
485
|
+
# define result
|
486
|
+
if operation_result is None:
|
487
|
+
if isinstance(response, JSONResponse):
|
488
|
+
try:
|
489
|
+
data = json.loads(response.body.decode()) # type: ignore
|
490
|
+
operation_result = OperationResult.model_validate(data)
|
491
|
+
except Exception:
|
492
|
+
pass
|
493
|
+
|
494
|
+
# define response-related timestamps
|
376
495
|
responded_at = datetime.now(tz=timezone.utc)
|
377
|
-
process_time =
|
496
|
+
process_time = (responded_at - request_context.requested_at).total_seconds()
|
378
497
|
|
379
498
|
# Add headers
|
380
499
|
response = self.response_builder.add_response_headers(
|
381
|
-
|
500
|
+
operation_id,
|
501
|
+
request_context,
|
502
|
+
authentication,
|
503
|
+
response,
|
504
|
+
responded_at,
|
505
|
+
process_time,
|
382
506
|
)
|
383
507
|
|
384
|
-
#
|
385
|
-
|
386
|
-
|
508
|
+
# Define response context
|
509
|
+
response_context = ResponseContext(
|
510
|
+
responded_at=responded_at,
|
511
|
+
process_time=process_time,
|
512
|
+
headers=response.headers.items(),
|
387
513
|
)
|
388
514
|
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
515
|
+
# Define operation
|
516
|
+
operation = Operation(
|
517
|
+
operation_id=operation_id,
|
518
|
+
summary=operation_summary,
|
519
|
+
request_context=request_context,
|
520
|
+
authentication=authentication,
|
521
|
+
authorization=None,
|
522
|
+
service=self.service_context,
|
523
|
+
timestamps=operation_timestamps,
|
524
|
+
context=self.operation_context,
|
525
|
+
metadata=operation_metadata,
|
526
|
+
exception=operation_exception,
|
527
|
+
arguments=None,
|
528
|
+
result=operation_result,
|
529
|
+
response_context=response_context,
|
405
530
|
)
|
406
531
|
|
407
|
-
|
408
|
-
self.request_logger.log_exception(authentication, error, request_context)
|
532
|
+
self._log_operation(operation=operation, log_level=log_level)
|
409
533
|
|
410
|
-
|
411
|
-
return self.response_builder.add_response_headers(
|
412
|
-
authentication, response, request_context, responded_at, process_time
|
413
|
-
)
|
534
|
+
return response
|
414
535
|
|
415
536
|
|
416
537
|
def add_base_middleware(
|
417
538
|
app: FastAPI,
|
539
|
+
settings: Settings,
|
418
540
|
keys: BaseGeneralSchemas.RSAKeys,
|
419
541
|
logger: MiddlewareLogger,
|
420
542
|
maleo_foundation: MaleoFoundationClientManager,
|
421
|
-
allow_origins: Sequence[str] = (),
|
422
|
-
allow_methods: Sequence[str] = ("GET",),
|
423
|
-
allow_headers: Sequence[str] = (),
|
424
|
-
allow_credentials: bool = False,
|
425
543
|
limit: int = 10,
|
426
544
|
window: int = 1,
|
427
545
|
cleanup_interval: int = 60,
|
@@ -460,13 +578,10 @@ def add_base_middleware(
|
|
460
578
|
"""
|
461
579
|
app.add_middleware(
|
462
580
|
BaseMiddleware,
|
581
|
+
settings=settings,
|
463
582
|
keys=keys,
|
464
583
|
logger=logger,
|
465
584
|
maleo_foundation=maleo_foundation,
|
466
|
-
allow_origins=allow_origins,
|
467
|
-
allow_methods=allow_methods,
|
468
|
-
allow_headers=allow_headers,
|
469
|
-
allow_credentials=allow_credentials,
|
470
585
|
limit=limit,
|
471
586
|
window=window,
|
472
587
|
cleanup_interval=cleanup_interval,
|