maleo-foundation 0.2.76__py3-none-any.whl → 0.2.80__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.
@@ -90,6 +90,7 @@ class MaleoClientConfiguration(BaseModel):
90
90
  url:str = Field(..., description="Client's URL")
91
91
 
92
92
  class MaleoClientConfigurations(BaseModel):
93
+ telemetry:MaleoClientConfiguration = Field(..., description="MaleoTelemetry client's configuration")
93
94
  metadata:MaleoClientConfiguration = Field(..., description="MaleoMetadata client's configuration")
94
95
  identity:MaleoClientConfiguration = Field(..., description="MaleoIdentity client's configuration")
95
96
  access:MaleoClientConfiguration = Field(..., description="MaleoAccess client's configuration")
@@ -4,303 +4,432 @@ 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
8
+ from uuid import UUID, uuid4
9
+
7
10
  from fastapi import FastAPI, Request, Response, status
8
11
  from fastapi.responses import JSONResponse
9
12
  from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
10
- from typing import Awaitable, Callable, Optional, Sequence
13
+
11
14
  from maleo_foundation.authentication import Authentication
12
15
  from maleo_foundation.enums import BaseEnums
13
16
  from maleo_foundation.client.manager import MaleoFoundationClientManager
14
17
  from maleo_foundation.models.schemas import BaseGeneralSchemas
15
18
  from maleo_foundation.models.responses import BaseResponses
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
+ from maleo_foundation.models.transfers.general.token import MaleoFoundationTokenGeneralTransfers
20
+ from maleo_foundation.models.transfers.parameters.token import MaleoFoundationTokenParametersTransfers
21
+ from maleo_foundation.models.transfers.parameters.signature import MaleoFoundationSignatureParametersTransfers
22
22
  from maleo_foundation.utils.extractor import BaseExtractors
23
23
  from maleo_foundation.utils.logging import MiddlewareLogger
24
24
 
25
25
  RequestProcessor = Callable[[Request], Awaitable[Optional[Response]]]
26
26
  ResponseProcessor = Callable[[Response], Awaitable[Response]]
27
27
 
28
- class BaseMiddleware(BaseHTTPMiddleware):
28
+ class RateLimiter:
29
+ """Thread-safe rate limiter with automatic cleanup."""
30
+
29
31
  def __init__(
30
32
  self,
31
- app:FastAPI,
32
- keys:BaseGeneralSchemas.RSAKeys,
33
- logger:MiddlewareLogger,
34
- maleo_foundation:MaleoFoundationClientManager,
35
- allow_origins:Sequence[str] = (),
36
- allow_methods:Sequence[str] = ("GET",),
37
- allow_headers:Sequence[str] = (),
38
- allow_credentials:bool = False,
39
- limit:int = 10,
40
- window:int = 1,
41
- cleanup_interval:int = 60,
42
- ip_timeout:int = 300
33
+ limit:int,
34
+ window:timedelta,
35
+ ip_timeout:timedelta,
36
+ cleanup_interval:timedelta
43
37
  ):
44
- super().__init__(app)
45
- self._keys = keys
46
- self._logger = logger
47
- self._maleo_foundation = maleo_foundation
48
- self._allow_origins = allow_origins
49
- self._allow_methods = allow_methods
50
- self._allow_headers = allow_headers
51
- self._allow_credentials = allow_credentials
52
- self._limit = limit
53
- self._window = timedelta(seconds=window)
54
- self._cleanup_interval = timedelta(seconds=cleanup_interval)
55
- self._ip_timeout = timedelta(seconds=ip_timeout)
56
- self._requests:dict[str, list[datetime]] = defaultdict(list)
57
- self._last_seen: dict[str, datetime] = {}
38
+ self.limit = limit
39
+ self.window = window
40
+ self.ip_timeout = ip_timeout
41
+ self.cleanup_interval = cleanup_interval
42
+ self._requests:Dict[str, List[datetime]] = defaultdict(list)
43
+ self._last_seen:Dict[str, datetime] = {}
58
44
  self._last_cleanup = datetime.now()
59
- self._lock = threading.RLock() #* Use RLock for thread safety
60
-
61
- def _cleanup_old_data(self) -> None:
62
- """
63
- Periodically clean up old request data to prevent memory growth.
64
- Removes:
65
- 1. IPs with empty request lists
66
- 2. IPs that haven't been seen in ip_timeout period
67
- """
68
- now = datetime.now()
69
- if now - self._last_cleanup > self._cleanup_interval:
70
- with self._lock:
71
- #* Remove inactive IPs (not seen recently) and empty lists
72
- inactive_ips = []
73
- for ip in list(self._requests.keys()):
74
- #* Remove IPs with empty timestamp lists
75
- if not self._requests[ip]:
76
- inactive_ips.append(ip)
77
- continue
78
-
79
- #* Remove IPs that haven't been active recently
80
- last_active = self._last_seen.get(ip, datetime.min)
81
- if now - last_active > self._ip_timeout:
82
- inactive_ips.append(ip)
83
-
84
- #* Remove the inactive IPs
85
- for ip in inactive_ips:
86
- if ip in self._requests:
87
- del self._requests[ip]
88
- if ip in self._last_seen:
89
- del self._last_seen[ip]
90
-
91
- # Update last cleanup time
92
- self._last_cleanup = now
93
- self._logger.debug(f"Cleaned up request cache. Removed {len(inactive_ips)} inactive IPs. Current tracked IPs: {len(self._requests)}")
94
-
95
- def _check_rate_limit(
45
+ self._lock = threading.RLock()
46
+
47
+ def is_rate_limited(
96
48
  self,
97
49
  client_ip:str
98
50
  ) -> bool:
99
- """Check if the client has exceeded their rate limit"""
51
+ """Check if client IP is rate limited and record the request."""
100
52
  with self._lock:
101
- now = datetime.now() #* Define current timestamp
102
- self._last_seen[client_ip] = now #* Update last seen timestamp for this IP
53
+ now = datetime.now()
54
+ self._last_seen[client_ip] = now
103
55
 
104
- #* Filter requests within the window
56
+ # Remove old requests outside the window
105
57
  self._requests[client_ip] = [
106
58
  timestamp for timestamp in self._requests[client_ip]
107
- if now - timestamp <= self._window
59
+ if now - timestamp <= self.window
108
60
  ]
109
61
 
110
- #* Check if the request count exceeds the limit
111
- if len(self._requests[client_ip]) >= self._limit:
62
+ # Check rate limit
63
+ if len(self._requests[client_ip]) >= self.limit:
112
64
  return True
113
65
 
114
- #* Add the current request timestamp
66
+ # Record this request
115
67
  self._requests[client_ip].append(now)
116
68
  return False
117
69
 
118
- def _add_response_headers(
70
+ def cleanup_old_data(
71
+ self,
72
+ logger:MiddlewareLogger
73
+ ) -> None:
74
+ """Clean up old request data to prevent memory growth."""
75
+ now = datetime.now()
76
+ if now - self._last_cleanup <= self.cleanup_interval:
77
+ return
78
+
79
+ with self._lock:
80
+ inactive_ips = []
81
+
82
+ for ip in list(self._requests.keys()):
83
+ # Remove IPs with empty request lists
84
+ if not self._requests[ip]:
85
+ inactive_ips.append(ip)
86
+ continue
87
+
88
+ # Remove IPs that haven't been active recently
89
+ last_active = self._last_seen.get(ip, datetime.min)
90
+ if now - last_active > self.ip_timeout:
91
+ inactive_ips.append(ip)
92
+
93
+ # Clean up inactive IPs
94
+ for ip in inactive_ips:
95
+ self._requests.pop(ip, None)
96
+ self._last_seen.pop(ip, None)
97
+
98
+ self._last_cleanup = now
99
+ logger.debug(
100
+ f"Cleaned up request cache. Removed {len(inactive_ips)} inactive IPs. "
101
+ f"Current tracked IPs: {len(self._requests)}"
102
+ )
103
+
104
+
105
+ class ResponseBuilder:
106
+ """Handles response building and header management."""
107
+
108
+ def __init__(
109
+ self,
110
+ keys:BaseGeneralSchemas.RSAKeys,
111
+ maleo_foundation:MaleoFoundationClientManager
112
+ ):
113
+ self.keys = keys
114
+ self.maleo_foundation = maleo_foundation
115
+
116
+ def add_response_headers(
119
117
  self,
120
118
  request:Request,
121
119
  authentication:Authentication,
122
120
  response:Response,
123
121
  request_timestamp:datetime,
124
122
  response_timestamp:datetime,
125
- process_time:int
123
+ process_time:float,
124
+ request_id:UUID
126
125
  ) -> Response:
127
- response.headers["X-Process-Time"] = str(process_time) #* Add Process Time Header
128
- response.headers["X-Request-Timestamp"] = request_timestamp.isoformat() #* Add request timestamp header
129
- response.headers["X-Response-Timestamp"] = response_timestamp.isoformat() #* Define and add response timestamp header
130
- #* Generate signature header
131
- message = f"{request.method}|{request.url.path}|{request_timestamp.isoformat()}|{response_timestamp.isoformat()}|{str(process_time)}"
132
- sign_parameters = (
133
- MaleoFoundationSignatureParametersTransfers
134
- .Sign(key=self._keys.private, password=self._keys.password, message=message)
126
+ """Add custom headers to response."""
127
+ # Basic headers
128
+ response.headers["X-Request-ID"] = str(request_id)
129
+ response.headers["X-Process-Time"] = str(process_time)
130
+ response.headers["X-Request-Timestamp"] = request_timestamp.isoformat()
131
+ response.headers["X-Response-Timestamp"] = response_timestamp.isoformat()
132
+
133
+ # Add signature header
134
+ self._add_signature_header(request, response, request_timestamp, response_timestamp, process_time, request_id)
135
+
136
+ # Add new authorization header if needed
137
+ self._add_new_authorization_header(request, authentication, response)
138
+
139
+ return response
140
+
141
+ def _add_signature_header(
142
+ self,
143
+ request:Request,
144
+ response:Response,
145
+ request_timestamp:datetime,
146
+ response_timestamp:datetime,
147
+ process_time:float,
148
+ request_id:UUID
149
+ ) -> None:
150
+ """Generate and add signature header."""
151
+ message = (
152
+ f"{request.method}|{request.url.path}|{request_timestamp.isoformat()}|"
153
+ f"{response_timestamp.isoformat()}|{str(process_time)}|{str(request_id)}"
135
154
  )
136
- sign_result = self._maleo_foundation.services.signature.sign(parameters=sign_parameters)
155
+
156
+ sign_parameters = MaleoFoundationSignatureParametersTransfers.Sign(
157
+ key=self.keys.private,
158
+ password=self.keys.password,
159
+ message=message
160
+ )
161
+
162
+ sign_result = self.maleo_foundation.services.signature.sign(parameters=sign_parameters)
137
163
  if sign_result.success:
138
164
  response.headers["X-Signature"] = sign_result.data.signature
139
- if (authentication.user.is_authenticated
165
+
166
+ def _add_new_authorization_header(
167
+ self,
168
+ request:Request,
169
+ authentication:Authentication,
170
+ response:Response
171
+ ) -> None:
172
+ """Add new authorization header for refresh tokens."""
173
+ if not self._should_regenerate_auth(request, authentication, response):
174
+ return
175
+
176
+ payload = MaleoFoundationTokenGeneralTransfers.BaseEncodePayload.model_validate(
177
+ authentication.credentials.token.payload.model_dump()
178
+ )
179
+
180
+ parameters = MaleoFoundationTokenParametersTransfers.Encode(
181
+ key=self.keys.private,
182
+ password=self.keys.password,
183
+ payload=payload
184
+ )
185
+
186
+ result = self.maleo_foundation.services.token.encode(parameters=parameters)
187
+ if result.success:
188
+ response.headers["X-New-Authorization"] = result.data.token
189
+
190
+ def _should_regenerate_auth(
191
+ self,
192
+ request:Request,
193
+ authentication:Authentication,
194
+ response:Response
195
+ ) -> bool:
196
+ """Check if authorization should be regenerated."""
197
+ return (
198
+ authentication.user.is_authenticated
140
199
  and authentication.credentials.token.type == BaseEnums.TokenType.REFRESH
141
- and (response.status_code >= 200 and response.status_code < 300)
200
+ and 200 <= response.status_code < 300
142
201
  and "logout" not in request.url.path
143
- ):
144
- #* Regenerate new authorization
145
- payload = (
146
- MaleoFoundationTokenGeneralTransfers
147
- .BaseEncodePayload
148
- .model_validate(authentication.credentials.token.payload.model_dump())
149
- )
150
- parameters = (
151
- MaleoFoundationTokenParametersTransfers
152
- .Encode(key=self._keys.private, password=self._keys.password, payload=payload)
153
- )
154
- result = self._maleo_foundation.services.token.encode(parameters=parameters)
155
- if result.success:
156
- response.headers["X-New-Authorization"] = result.data.token
157
- return response
202
+ )
203
+
204
+
205
+ class RequestLogger:
206
+ """Handles request/response logging."""
207
+
208
+ def __init__(
209
+ self,
210
+ logger:MiddlewareLogger
211
+ ):
212
+ self.logger = logger
158
213
 
159
- def _build_response(
214
+ def log_request_response(
160
215
  self,
161
216
  request:Request,
162
217
  authentication:Authentication,
163
- authentication_info:str,
164
218
  response:Response,
165
- request_timestamp:datetime,
166
- response_timestamp:datetime,
167
- process_time:int,
168
- log_level:str = "info",
169
- client_ip:str = "unknown"
170
- ) -> Response:
171
- response = self._add_response_headers(
172
- request,
173
- authentication,
174
- response,
175
- request_timestamp,
176
- response_timestamp,
177
- process_time
178
- )
179
- log_func = getattr(self._logger, log_level)
219
+ client_ip:str,
220
+ request_id:UUID,
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)
180
227
  log_func(
181
- f"Request {authentication_info} | IP: {client_ip} | Host: {request.client.host} | Port: {request.client.port} | Method: {request.method} | URL: {request.url.path} | "
228
+ f"Request | ID: {request_id} {authentication_info} | IP: {client_ip} | "
229
+ f"Host: {request.client.host} | Port: {request.client.port} | "
230
+ f"Method: {request.method} | URL: {request.url.path} | "
182
231
  f"Headers: {dict(request.headers)} - Response | Status: {response.status_code}"
183
232
  )
184
- return response
185
-
186
- def _handle_exception(
233
+
234
+ def log_exception(
187
235
  self,
188
236
  request:Request,
189
237
  authentication:Authentication,
190
- authentication_info:str,
191
- error,
192
- request_timestamp:datetime,
193
- response_timestamp:datetime,
194
- process_time:int,
195
- client_ip:str = "unknown"
196
- ):
197
- traceback_str = traceback.format_exc().split("\n")
238
+ error:Exception,
239
+ client_ip:str,
240
+ request_id:UUID
241
+ ) -> None:
242
+ """Log exception details."""
243
+ authentication_info = self._get_authentication_info(authentication)
244
+
198
245
  error_details = {
246
+ "request_id": str(request_id),
199
247
  "error": str(error),
200
- "traceback": traceback_str,
248
+ "traceback": traceback.format_exc().split("\n"),
201
249
  "client_ip": client_ip,
202
250
  "method": request.method,
203
251
  "url": request.url.path,
204
252
  "headers": dict(request.headers),
205
253
  }
206
254
 
207
- response = JSONResponse(
208
- content=BaseResponses.ServerError().model_dump(),
209
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
255
+ self.logger.error(
256
+ f"Request | ID: {request_id} {authentication_info} | IP: {client_ip} | "
257
+ f"Host: {request.client.host} | Port: {request.client.port} | "
258
+ f"Method: {request.method} | URL: {request.url.path} | "
259
+ f"Headers: {dict(request.headers)} - Response | Status: 500 | "
260
+ f"Exception:\n{json.dumps(error_details, indent=4)}"
210
261
  )
211
262
 
212
- self._logger.error(
213
- f"Request {authentication_info} | IP: {client_ip} | Host: {request.client.host} | Port: {request.client.port} | Method: {request.method} | URL: {request.url.path} | "
214
- f"Headers: {dict(request.headers)} - Response | Status: 500 | Exception:\n{json.dumps(error_details, indent=4)}"
215
- )
263
+ def _get_authentication_info(self, authentication:Authentication) -> str:
264
+ """Get authentication info string."""
265
+ if not authentication.user.is_authenticated:
266
+ return "| Unauthenticated"
216
267
 
217
- return self._add_response_headers(
218
- request,
219
- authentication,
220
- response,
221
- request_timestamp,
222
- response_timestamp,
223
- process_time
268
+ return (
269
+ f"| Token type: {authentication.credentials.token.type} | "
270
+ f"Username: {authentication.user.display_name} | "
271
+ f"Email: {authentication.user.identity}"
224
272
  )
225
273
 
226
- async def _request_processor(self, request:Request) -> Optional[Response]:
227
- return None
228
274
 
229
- async def dispatch(self, request:Request, call_next:RequestResponseEndpoint):
230
- self._cleanup_old_data() #* Run periodic cleanup
231
- request_timestamp = datetime.now(tz=timezone.utc) #* Record the request timestamp
232
- start_time = time.perf_counter() #* Record the start time
233
- client_ip = BaseExtractors.extract_client_ip(request) #* Get request IP with improved extraction
234
- authentication = Authentication(
235
- credentials=request.auth,
236
- user=request.user
275
+ class BaseMiddleware(BaseHTTPMiddleware):
276
+ """Base middleware with rate limiting, logging, and response enhancement."""
277
+
278
+ def __init__(
279
+ self,
280
+ app:FastAPI,
281
+ keys:BaseGeneralSchemas.RSAKeys,
282
+ logger:MiddlewareLogger,
283
+ maleo_foundation:MaleoFoundationClientManager,
284
+ allow_origins:Sequence[str] = (),
285
+ allow_methods:Sequence[str] = ("GET",),
286
+ allow_headers:Sequence[str] = (),
287
+ allow_credentials:bool = False,
288
+ limit:int = 10,
289
+ window:int = 1,
290
+ cleanup_interval:int = 60,
291
+ ip_timeout:int = 300
292
+ ):
293
+ super().__init__(app)
294
+
295
+ # Core components
296
+ self.rate_limiter = RateLimiter(
297
+ limit=limit,
298
+ window=timedelta(seconds=window),
299
+ ip_timeout=timedelta(seconds=ip_timeout),
300
+ cleanup_interval=timedelta(seconds=cleanup_interval)
237
301
  )
238
- if not authentication.user.is_authenticated:
239
- authentication_info = "| Unauthenticated"
240
- else:
241
- authentication_info = f"| Token type: {authentication.credentials.token.type} | Username: {authentication.user.display_name} | Email:{authentication.user.identity}"
302
+ self.response_builder = ResponseBuilder(keys, maleo_foundation)
303
+ self.request_logger = RequestLogger(logger)
304
+
305
+ # CORS settings (if needed)
306
+ self.cors_config = {
307
+ 'allow_origins': allow_origins,
308
+ 'allow_methods': allow_methods,
309
+ 'allow_headers': allow_headers,
310
+ 'allow_credentials': allow_credentials,
311
+ }
312
+
313
+ async def dispatch(self, request:Request, call_next:RequestResponseEndpoint) -> Response:
314
+ """Main middleware dispatch method."""
315
+ # Generate unique request ID
316
+ request_id = uuid4()
317
+ request.state.request_id = request_id
318
+
319
+ # Setup
320
+ self.rate_limiter.cleanup_old_data(self.request_logger.logger)
321
+ request_timestamp = datetime.now(tz=timezone.utc)
322
+ request.state.request_timestamp = request_timestamp
323
+ start_time = time.perf_counter()
324
+ client_ip = BaseExtractors.extract_client_ip(request)
325
+ authentication = Authentication(credentials=request.auth, user=request.user)
242
326
 
243
327
  try:
244
- #* 1. Rate limit check
245
- if self._check_rate_limit(client_ip):
246
- return self._build_response(
247
- request=request,
248
- authentication=authentication,
249
- authentication_info=authentication_info,
250
- response=JSONResponse(
251
- content=BaseResponses.RateLimitExceeded().model_dump(),
252
- status_code=status.HTTP_429_TOO_MANY_REQUESTS,
253
- ),
254
- request_timestamp=request_timestamp,
255
- response_timestamp=datetime.now(tz=timezone.utc),
256
- process_time=time.perf_counter() - start_time,
257
- log_level="warning",
258
- client_ip=client_ip,
328
+ # Rate limiting check
329
+ if self.rate_limiter.is_rate_limited(client_ip):
330
+ return self._create_rate_limit_response(
331
+ request, authentication, client_ip, request_timestamp, start_time, request_id
259
332
  )
260
333
 
261
- #* 2. Optional preprocessing
334
+ # Optional preprocessing
262
335
  pre_response = await self._request_processor(request)
263
336
  if pre_response is not None:
264
- return self._build_response(
265
- request=request,
266
- authentication=authentication,
267
- authentication_info=authentication_info,
268
- response=pre_response,
269
- request_timestamp=request_timestamp,
270
- response_timestamp=datetime.now(tz=timezone.utc),
271
- process_time=time.perf_counter() - start_time,
272
- log_level="info",
273
- client_ip=client_ip,
337
+ return self._build_final_response(
338
+ request, authentication, pre_response, client_ip,
339
+ request_timestamp, start_time, request_id
274
340
  )
275
341
 
276
- #* 3. Main handler
342
+ # Main request processing
277
343
  response = await call_next(request)
278
- response = self._build_response(
279
- request=request,
280
- authentication=authentication,
281
- authentication_info=authentication_info,
282
- response=response,
283
- request_timestamp=request_timestamp,
284
- response_timestamp=datetime.now(tz=timezone.utc),
285
- process_time=time.perf_counter() - start_time,
286
- log_level="info",
287
- client_ip=client_ip,
344
+ return self._build_final_response(
345
+ request, authentication, response, client_ip,
346
+ request_timestamp, start_time, request_id
288
347
  )
289
348
 
290
- return response
291
-
292
349
  except Exception as e:
293
350
  return self._handle_exception(
294
- request=request,
295
- authentication=authentication,
296
- authentication_info=authentication_info,
297
- error=e,
298
- request_timestamp=request_timestamp,
299
- response_timestamp=datetime.now(tz=timezone.utc),
300
- process_time=time.perf_counter() - start_time,
301
- client_ip=client_ip
351
+ request, authentication, e, client_ip, request_timestamp, start_time, request_id
302
352
  )
303
353
 
354
+ async def _request_processor(self, request:Request) -> Optional[Response]:
355
+ """Override this method for custom request preprocessing."""
356
+ return None
357
+
358
+ def _create_rate_limit_response(
359
+ self,
360
+ request:Request,
361
+ authentication:Authentication,
362
+ client_ip:str,
363
+ request_timestamp:datetime,
364
+ start_time:float,
365
+ request_id:UUID
366
+ ) -> Response:
367
+ """Create rate limit exceeded response."""
368
+ response = JSONResponse(
369
+ content=BaseResponses.RateLimitExceeded().model_dump(),
370
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
371
+ )
372
+
373
+ return self._build_final_response(
374
+ request, authentication, response, client_ip,
375
+ request_timestamp, start_time, request_id, log_level="warning"
376
+ )
377
+
378
+ def _build_final_response(
379
+ self,
380
+ request:Request,
381
+ authentication:Authentication,
382
+ response:Response,
383
+ client_ip:str,
384
+ request_timestamp:datetime,
385
+ start_time:float,
386
+ request_id:UUID,
387
+ log_level:str = "info"
388
+ ) -> Response:
389
+ """Build final response with headers and logging."""
390
+ response_timestamp = datetime.now(tz=timezone.utc)
391
+ process_time = time.perf_counter() - start_time
392
+
393
+ # Add headers
394
+ response = self.response_builder.add_response_headers(
395
+ request, authentication, response, request_timestamp, response_timestamp, process_time, request_id
396
+ )
397
+
398
+ # Log request/response
399
+ self.request_logger.log_request_response(
400
+ request, authentication, response, client_ip, request_id, log_level
401
+ )
402
+
403
+ return response
404
+
405
+ def _handle_exception(
406
+ self,
407
+ request:Request,
408
+ authentication:Authentication,
409
+ error:Exception,
410
+ client_ip:str,
411
+ request_timestamp:datetime,
412
+ start_time:float,
413
+ request_id:UUID
414
+ ) -> Response:
415
+ """Handle exceptions and create error response."""
416
+ response_timestamp = datetime.now(tz=timezone.utc)
417
+ process_time = time.perf_counter() - start_time
418
+
419
+ response = JSONResponse(
420
+ content=BaseResponses.ServerError().model_dump(),
421
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
422
+ )
423
+
424
+ # Log exception
425
+ self.request_logger.log_exception(request, authentication, error, client_ip, request_id)
426
+
427
+ # Add headers
428
+ return self.response_builder.add_response_headers(
429
+ request, authentication, response, request_timestamp, response_timestamp, process_time, request_id
430
+ )
431
+
432
+
304
433
  def add_base_middleware(
305
434
  app:FastAPI,
306
435
  keys:BaseGeneralSchemas.RSAKeys,
@@ -316,39 +445,35 @@ def add_base_middleware(
316
445
  ip_timeout:int = 300
317
446
  ) -> None:
318
447
  """
319
- Adds Base middleware to the FastAPI application.
448
+ Add Base middleware to the FastAPI application.
320
449
 
321
450
  Args:
322
- app: FastAPI
323
- The FastAPI application instance to which the middleware will be added.
324
-
325
- logger: Logger
326
- The middleware logger to be used.
327
-
328
- limit: int
329
- Request count limit in a specific window of time
330
-
331
- window: int
332
- Time window for rate limiting (in seconds).
333
-
334
- cleanup_interval: int
335
- How often to clean up old IP data (in seconds).
336
-
337
- ip_timeout: int
338
- How long to keep an IP in memory after its last activity (in seconds).
339
- Default is 300 seconds (5 minutes).
340
-
341
- Returns:
342
- None: The function modifies the FastAPI app by adding Base middleware.
343
-
344
- Note:
345
- FastAPI applies middleware in reverse order of registration, so this middleware
346
- will execute after any middleware added subsequently.
451
+ app:FastAPI application instance
452
+ keys:RSA keys for signing and token generation
453
+ logger:Middleware logger instance
454
+ maleo_foundation:Client manager for foundation services
455
+ allow_origins:CORS allowed origins
456
+ allow_methods:CORS allowed methods
457
+ allow_headers:CORS allowed headers
458
+ allow_credentials:CORS allow credentials flag
459
+ limit:Request count limit per window
460
+ window:Time window for rate limiting (seconds)
461
+ cleanup_interval:Cleanup interval for old IP data (seconds)
462
+ ip_timeout:IP timeout after last activity (seconds)
347
463
 
348
464
  Example:
349
- ```python
350
- add_base_middleware(app=app, limit=10, window=1, cleanup_interval=60, ip_timeout=300)
351
- ```
465
+ ```python
466
+ add_base_middleware(
467
+ app=app,
468
+ keys=rsa_keys,
469
+ logger=middleware_logger,
470
+ maleo_foundation=client_manager,
471
+ limit=10,
472
+ window=1,
473
+ cleanup_interval=60,
474
+ ip_timeout=300
475
+ )
476
+ ```
352
477
  """
353
478
  app.add_middleware(
354
479
  BaseMiddleware,
@@ -1,5 +1,5 @@
1
1
  from __future__ import annotations
2
- from datetime import datetime
2
+ from datetime import datetime, timezone
3
3
  from pydantic import BaseModel, Field
4
4
  from uuid import UUID
5
5
  from maleo_foundation.enums import BaseEnums
@@ -77,4 +77,10 @@ class BaseGeneralSchemas:
77
77
  class RSAKeys(BaseModel):
78
78
  password:str = Field(..., description="Key's password")
79
79
  private:str = Field(..., description="Private key")
80
- public:str = Field(..., description="Public key")
80
+ public:str = Field(..., description="Public key")
81
+
82
+ class AccessedAt(BaseModel):
83
+ accessed_at:datetime = Field(datetime.now(tz=timezone.utc), description="Accessed at")
84
+
85
+ class AccessedBy(BaseModel):
86
+ accessed_by:int = Field(0, ge=0, description="Accessed by")
@@ -1,5 +1,11 @@
1
1
  from __future__ import annotations
2
+ from maleo_foundation.models.schemas.general import BaseGeneralSchemas
2
3
  from .token import MaleoFoundationTokenGeneralTransfers
3
4
 
4
5
  class BaseGeneralTransfers:
5
- Token = MaleoFoundationTokenGeneralTransfers
6
+ Token = MaleoFoundationTokenGeneralTransfers
7
+
8
+ class AccessTransfers(
9
+ BaseGeneralSchemas.AccessedBy,
10
+ BaseGeneralSchemas.AccessedAt
11
+ ): pass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maleo_foundation
3
- Version: 0.2.76
3
+ Version: 0.2.80
4
4
  Summary: Foundation package for Maleo
5
5
  Author-email: Agra Bima Yuda <agra@nexmedis.com>
6
6
  License: MIT
@@ -34,7 +34,7 @@ maleo_foundation/expanded_types/encryption/rsa.py,sha256=Esf_H8nMz2kOLAWa3M7dlD-
34
34
  maleo_foundation/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
35
35
  maleo_foundation/managers/db.py,sha256=cpY1IOiUytT9XXYtzS0E9OSYOuB7jBKo0XHe__uI1Jg,5340
36
36
  maleo_foundation/managers/middleware.py,sha256=77wRCC_CWc22nSDL-UJanO3rXmSB7wLzaAIXEFjXq6M,4249
37
- maleo_foundation/managers/service.py,sha256=_0eme0NQMsf_8UlLmrLkD9OzHrLbrKjMYUNGYcA0EPI,19179
37
+ maleo_foundation/managers/service.py,sha256=n2ubW5NoeBBju0t0fLSWa0FxeZMLzmoRcimuCIobgzk,19284
38
38
  maleo_foundation/managers/cache/__init__.py,sha256=CeY0oof2bVl_v5WS-FKXNwn2gf3xrEMfUsHK9cHo59s,471
39
39
  maleo_foundation/managers/cache/base.py,sha256=YyPjde4KTsp2IHV6NdFMysa0ev-1GX1rtX-0jQPuIBU,837
40
40
  maleo_foundation/managers/cache/redis.py,sha256=xLa8QfXdNtghs0eBxIqc04H3XTYmxLEzrqJZAFCigvM,1150
@@ -47,14 +47,14 @@ maleo_foundation/managers/client/google/parameter.py,sha256=Lnj7mQgxWQpsQwbmDRK5
47
47
  maleo_foundation/managers/client/google/secret.py,sha256=Ski1CHYeA8vjSk2Oc2Pf4CfFrzT_RcA6NEZwza7gM7Y,4464
48
48
  maleo_foundation/managers/client/google/storage.py,sha256=Hq79cVreWR2AGwUzpD57NzziZjqxj__TqYIBRy-A9A0,5340
49
49
  maleo_foundation/middlewares/authentication.py,sha256=UL6kL65SvqrzemlIDopoO9N1C05eWlYMHVR2tiRsVEA,4821
50
- maleo_foundation/middlewares/base.py,sha256=fLctfwzfWlGSPFnSiqbV-9wDhlG2SLA1DAs-hXk0diU,14600
50
+ maleo_foundation/middlewares/base.py,sha256=R-N8rxUaPt7_mvK6DI8y8Jal8GfzuD7Yyx3VLp9_tQA,17097
51
51
  maleo_foundation/middlewares/cors.py,sha256=9uvBvY2N6Vxa9RP_YtESxcWo6Doi6uS0lzAG9iLY7Uc,2288
52
52
  maleo_foundation/models/__init__.py,sha256=AaKehO7c1HyKhoTGRmNHDddSeBXkW-_YNrpOGBu8Ms8,246
53
53
  maleo_foundation/models/responses.py,sha256=nE5qThK-WgcYB-9J4wHzJltMA3PLmWbMI-dkxAxAdII,5631
54
54
  maleo_foundation/models/table.py,sha256=yFv9KAr0kp-QqfIFEPXqRn61pQKCCIory1zEL7Y7rVc,1873
55
55
  maleo_foundation/models/schemas/__init__.py,sha256=Xj8Ahsqyra-fmEaVcGPok5GOOsPQlKcknHYMvbjvENA,277
56
56
  maleo_foundation/models/schemas/encryption.py,sha256=KYs2P57AqWpEROuqTuSuyt1Zk-jsIUKFeRWIfSwem74,658
57
- maleo_foundation/models/schemas/general.py,sha256=W6bncs4z7tMiRL06xs4Q3mh9xHv7zXOXaDPC2VKr4vk,3430
57
+ maleo_foundation/models/schemas/general.py,sha256=1X0aQohTGN_EbYk1aTn7H4vhUB17A5I711fROy2Exio,3671
58
58
  maleo_foundation/models/schemas/hash.py,sha256=db2uyCeUzvF2zDCcbiZMh1MxIOGOGezOMOx-M1ta4zI,441
59
59
  maleo_foundation/models/schemas/key.py,sha256=7FZxVqTL5qRK48AXL1odrMNhAwhwtCwSkBUPsJwuBII,594
60
60
  maleo_foundation/models/schemas/parameter.py,sha256=6jVHw538cnCK-SN8SlPVKWakeeJGn3yRNzHegR231j8,3497
@@ -62,7 +62,7 @@ maleo_foundation/models/schemas/result.py,sha256=MaWDyKGvZu0IcvKMj30lrMXojGV9Mhi
62
62
  maleo_foundation/models/schemas/signature.py,sha256=-5ldTnJsjwqPRbHw7PFcLKITqEXJ_qKDdRHShK75NVA,620
63
63
  maleo_foundation/models/schemas/token.py,sha256=RYq8v1T_WZIu8lcjwyV6Pp7ZjFkr_lW9x6QyDmXBsfw,563
64
64
  maleo_foundation/models/transfers/__init__.py,sha256=oJLJ3Geeme6vBw7R2Dhvdvg4ziVvzEYAGJaP-tm_90w,299
65
- maleo_foundation/models/transfers/general/__init__.py,sha256=UPIE9l9XXCb6nWzaV3atMgbbCeBeRzsvFyROJuH2d2w,168
65
+ maleo_foundation/models/transfers/general/__init__.py,sha256=7yQNGSihJt1db6i_zIrhLdY2Tgn5lrTxuDlAVlDh2vo,340
66
66
  maleo_foundation/models/transfers/general/key.py,sha256=tLKkXbwNu7Oc1MdKa8-Y7TlBAIk54h_02AmL5Yg2PrQ,751
67
67
  maleo_foundation/models/transfers/general/signature.py,sha256=J9xQy2HjpCQOnES7RJqsUnDgjFPuakQ1mxyfdTdstSE,297
68
68
  maleo_foundation/models/transfers/general/token.py,sha256=O_U6dQS7oMScJzqufl6Pe21pTxMsYhOzKH8aFLxjblQ,2895
@@ -118,7 +118,7 @@ maleo_foundation/utils/loaders/credential/__init__.py,sha256=qopTKvcMVoTFwyRijeg
118
118
  maleo_foundation/utils/loaders/credential/google.py,sha256=HUcuHD4tXHPt0eHInlFYxA_MDrGSOtbenpd0PX156OM,1255
119
119
  maleo_foundation/utils/loaders/key/__init__.py,sha256=hVygcC2ImHc_aVrSrOmyedR8tMUZokWUKCKOSh5ctbo,106
120
120
  maleo_foundation/utils/loaders/key/rsa.py,sha256=gDhyX6iTFtHiluuhFCozaZ3pOLKU2Y9TlrNMK_GVyGU,3796
121
- maleo_foundation-0.2.76.dist-info/METADATA,sha256=xcB30u5nhK0ok6NTydC63dormNoi0ObMokMjqOTxyu0,3598
122
- maleo_foundation-0.2.76.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
123
- maleo_foundation-0.2.76.dist-info/top_level.txt,sha256=_iBos3F_bhEOdjOnzeiEYSrCucasc810xXtLBXI8cQc,17
124
- maleo_foundation-0.2.76.dist-info/RECORD,,
121
+ maleo_foundation-0.2.80.dist-info/METADATA,sha256=dRNIGvejuDcdeeazla9MoD2p0H1LGUATumhyh2XTB00,3598
122
+ maleo_foundation-0.2.80.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
123
+ maleo_foundation-0.2.80.dist-info/top_level.txt,sha256=_iBos3F_bhEOdjOnzeiEYSrCucasc810xXtLBXI8cQc,17
124
+ maleo_foundation-0.2.80.dist-info/RECORD,,