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.
Files changed (29) hide show
  1. maleo_foundation/authentication.py +2 -48
  2. maleo_foundation/constants.py +2 -0
  3. maleo_foundation/enums.py +7 -1
  4. maleo_foundation/managers/middleware.py +8 -8
  5. maleo_foundation/managers/service.py +8 -4
  6. maleo_foundation/middlewares/authentication.py +3 -2
  7. maleo_foundation/middlewares/base.py +312 -197
  8. maleo_foundation/models/schemas/general.py +1 -127
  9. maleo_foundation/models/transfers/general/authentication.py +35 -0
  10. maleo_foundation/{authorization.py → models/transfers/general/authorization.py} +0 -3
  11. maleo_foundation/models/transfers/general/configurations/__init__.py +2 -0
  12. maleo_foundation/models/transfers/general/configurations/middleware.py +6 -7
  13. maleo_foundation/models/transfers/general/operation.py +192 -30
  14. maleo_foundation/models/transfers/general/request.py +13 -19
  15. maleo_foundation/models/transfers/general/response.py +14 -0
  16. maleo_foundation/models/transfers/general/service.py +9 -0
  17. maleo_foundation/models/transfers/general/user_agent.py +34 -0
  18. maleo_foundation/utils/exceptions/client.py +22 -23
  19. maleo_foundation/utils/exceptions/service.py +21 -22
  20. maleo_foundation/utils/extractor.py +49 -19
  21. maleo_foundation/utils/logging.py +40 -0
  22. maleo_foundation/utils/parser.py +7 -0
  23. {maleo_foundation-0.3.72.dist-info → maleo_foundation-0.3.74.dist-info}/METADATA +3 -1
  24. {maleo_foundation-0.3.72.dist-info → maleo_foundation-0.3.74.dist-info}/RECORD +26 -24
  25. maleo_foundation/utils/dependencies/__init__.py +0 -6
  26. maleo_foundation/utils/dependencies/auth.py +0 -17
  27. maleo_foundation/utils/dependencies/context.py +0 -8
  28. {maleo_foundation-0.3.72.dist-info → maleo_foundation-0.3.74.dist-info}/WHEEL +0 -0
  29. {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, Sequence, Dict, List
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.utils.extractor import extract_request_context
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, logger: MiddlewareLogger) -> None:
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
- response: Response,
148
- request_context: RequestContext,
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"{request_context.method}|{request_context.url}|{request_context.requested_at.isoformat()}|"
155
- f"{responded_at.isoformat()}|{str(process_time)}|{str(request_context.request_id)}"
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 _should_regenerate_auth(
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
- ) -> bool:
199
- """Check if authorization should be regenerated."""
200
- if authentication.credentials.token is not None:
201
- return (
202
- authentication.user.is_authenticated
203
- and authentication.credentials.token.type == BaseEnums.TokenType.REFRESH
204
- and 200 <= response.status_code < 300
205
- and "logout" not in request_context.url
206
- )
207
- return False
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
- def log_exception(
236
- self,
237
- authentication: Authentication,
238
- error: Exception,
239
- request_context: RequestContext,
240
- ) -> None:
241
- """Log exception details."""
242
- authentication_info = self._get_authentication_info(authentication)
243
-
244
- error_details = {
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
- def _get_authentication_info(self, authentication: Authentication) -> str:
259
- """Get authentication info string."""
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 "| Unauthenticated"
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
- # CORS settings (if needed)
304
- self.cors_config = {
305
- "allow_origins": allow_origins,
306
- "allow_methods": allow_methods,
307
- "allow_headers": allow_headers,
308
- "allow_credentials": allow_credentials,
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(self.request_logger.logger)
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
- try:
323
- # Rate limiting check
324
- if self.rate_limiter.is_rate_limited(request_context):
325
- return self._create_rate_limit_response(
326
- authentication, request_context, start_time
327
- )
328
-
329
- # Optional preprocessing
330
- pre_response = await self._request_processor(request)
331
- if pre_response is not None:
332
- return self._build_final_response(
333
- authentication, pre_response, request_context, start_time
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
- authentication, response, request_context, start_time
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
- return self._handle_exception(
344
- authentication, e, request_context, start_time
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
- request_context: RequestContext,
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 = time.perf_counter() - start_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
- authentication, response, request_context, responded_at, process_time
500
+ operation_id,
501
+ request_context,
502
+ authentication,
503
+ response,
504
+ responded_at,
505
+ process_time,
382
506
  )
383
507
 
384
- # Log request/response
385
- self.request_logger.log_request_response(
386
- authentication, response, request_context, log_level
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
- return response
390
-
391
- def _handle_exception(
392
- self,
393
- authentication: Authentication,
394
- error: Exception,
395
- request_context: RequestContext,
396
- start_time: float,
397
- ) -> Response:
398
- """Handle exceptions and create error response."""
399
- responded_at = datetime.now(tz=timezone.utc)
400
- process_time = time.perf_counter() - start_time
401
-
402
- response = JSONResponse(
403
- content=BaseResponses.ServerError().model_dump(), # type: ignore
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
- # Log exception
408
- self.request_logger.log_exception(authentication, error, request_context)
532
+ self._log_operation(operation=operation, log_level=log_level)
409
533
 
410
- # Add headers
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,