maleo-foundation 0.2.78__tar.gz → 0.2.81__tar.gz

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 (133) hide show
  1. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/PKG-INFO +1 -1
  2. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/service.py +3 -2
  3. maleo_foundation-0.2.81/maleo_foundation/middlewares/base.py +457 -0
  4. maleo_foundation-0.2.81/maleo_foundation/models/transfers/general/__init__.py +35 -0
  5. maleo_foundation-0.2.81/maleo_foundation/utils/dependencies/context.py +9 -0
  6. maleo_foundation-0.2.81/maleo_foundation/utils/extractor.py +55 -0
  7. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation.egg-info/PKG-INFO +1 -1
  8. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation.egg-info/SOURCES.txt +1 -0
  9. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/pyproject.toml +1 -1
  10. maleo_foundation-0.2.78/maleo_foundation/middlewares/base.py +0 -366
  11. maleo_foundation-0.2.78/maleo_foundation/models/transfers/general/__init__.py +0 -11
  12. maleo_foundation-0.2.78/maleo_foundation/utils/extractor.py +0 -20
  13. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/README.md +0 -0
  14. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/__init__.py +0 -0
  15. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/authentication.py +0 -0
  16. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/authorization.py +0 -0
  17. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/__init__.py +0 -0
  18. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/manager.py +0 -0
  19. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/__init__.py +0 -0
  20. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/encryption/__init__.py +0 -0
  21. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/encryption/aes.py +0 -0
  22. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/encryption/rsa.py +0 -0
  23. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/hash/__init__.py +0 -0
  24. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/hash/bcrypt.py +0 -0
  25. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/hash/hmac.py +0 -0
  26. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/hash/sha256.py +0 -0
  27. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/key.py +0 -0
  28. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/signature.py +0 -0
  29. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/client/services/token.py +0 -0
  30. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/constants.py +0 -0
  31. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/enums.py +0 -0
  32. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/__init__.py +0 -0
  33. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/client.py +0 -0
  34. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/encryption/__init__.py +0 -0
  35. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/encryption/aes.py +0 -0
  36. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/encryption/rsa.py +0 -0
  37. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/general.py +0 -0
  38. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/hash.py +0 -0
  39. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/key.py +0 -0
  40. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/repository.py +0 -0
  41. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/service.py +0 -0
  42. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/signature.py +0 -0
  43. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/expanded_types/token.py +0 -0
  44. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/extended_types.py +0 -0
  45. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/__init__.py +0 -0
  46. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/cache/__init__.py +0 -0
  47. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/cache/base.py +0 -0
  48. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/cache/redis.py +0 -0
  49. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/__init__.py +0 -0
  50. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/base.py +0 -0
  51. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/google/__init__.py +0 -0
  52. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/google/base.py +0 -0
  53. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/google/parameter.py +0 -0
  54. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/google/secret.py +0 -0
  55. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/google/storage.py +0 -0
  56. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/client/maleo.py +0 -0
  57. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/db.py +0 -0
  58. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/managers/middleware.py +0 -0
  59. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/middlewares/authentication.py +0 -0
  60. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/middlewares/cors.py +0 -0
  61. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/__init__.py +0 -0
  62. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/responses.py +0 -0
  63. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/__init__.py +0 -0
  64. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/encryption.py +0 -0
  65. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/general.py +0 -0
  66. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/hash.py +0 -0
  67. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/key.py +0 -0
  68. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/parameter.py +0 -0
  69. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/result.py +0 -0
  70. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/signature.py +0 -0
  71. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/schemas/token.py +0 -0
  72. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/table.py +0 -0
  73. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/__init__.py +0 -0
  74. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/general/key.py +0 -0
  75. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/general/signature.py +0 -0
  76. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/general/token.py +0 -0
  77. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/__init__.py +0 -0
  78. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/client.py +0 -0
  79. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/encryption/__init__.py +0 -0
  80. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/encryption/aes.py +0 -0
  81. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/encryption/rsa.py +0 -0
  82. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/general.py +0 -0
  83. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/hash/__init__.py +0 -0
  84. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/hash/bcrypt.py +0 -0
  85. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/hash/hmac.py +0 -0
  86. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/hash/sha256.py +0 -0
  87. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/key.py +0 -0
  88. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/service.py +0 -0
  89. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/signature.py +0 -0
  90. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/parameters/token.py +0 -0
  91. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/__init__.py +0 -0
  92. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/client/__init__.py +0 -0
  93. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/client/controllers/__init__.py +0 -0
  94. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/client/controllers/http.py +0 -0
  95. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/client/service.py +0 -0
  96. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/encryption/__init__.py +0 -0
  97. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/encryption/aes.py +0 -0
  98. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/encryption/rsa.py +0 -0
  99. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/hash.py +0 -0
  100. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/key.py +0 -0
  101. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/service/__init__.py +0 -0
  102. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/service/controllers/__init__.py +0 -0
  103. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/service/controllers/rest.py +0 -0
  104. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/service/general.py +0 -0
  105. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/service/repository.py +0 -0
  106. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/signature.py +0 -0
  107. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/models/transfers/results/token.py +0 -0
  108. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/rest_controller_result.py +0 -0
  109. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/types.py +0 -0
  110. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/__init__.py +0 -0
  111. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/client.py +0 -0
  112. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/controller.py +0 -0
  113. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/dependencies/__init__.py +0 -0
  114. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/dependencies/auth.py +0 -0
  115. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/exceptions.py +0 -0
  116. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/formatter/__init__.py +0 -0
  117. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/formatter/case.py +0 -0
  118. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/__init__.py +0 -0
  119. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/credential/__init__.py +0 -0
  120. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/credential/google.py +0 -0
  121. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/json.py +0 -0
  122. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/key/__init__.py +0 -0
  123. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/key/rsa.py +0 -0
  124. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/loaders/yaml.py +0 -0
  125. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/logging.py +0 -0
  126. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/merger.py +0 -0
  127. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/query.py +0 -0
  128. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/repository.py +0 -0
  129. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation/utils/searcher.py +0 -0
  130. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation.egg-info/dependency_links.txt +0 -0
  131. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation.egg-info/requires.txt +0 -0
  132. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/maleo_foundation.egg-info/top_level.txt +0 -0
  133. {maleo_foundation-0.2.78 → maleo_foundation-0.2.81}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maleo_foundation
3
- Version: 0.2.78
3
+ Version: 0.2.81
4
4
  Summary: Foundation package for Maleo
5
5
  Author-email: Agra Bima Yuda <agra@nexmedis.com>
6
6
  License: MIT
@@ -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")
@@ -415,7 +416,7 @@ class ServiceManager:
415
416
  maleo_foundation=self._foundation
416
417
  )
417
418
  self._middleware.add_all()
418
- self._loggers.application.info("Middlewares addedd successfully")
419
+ self._loggers.application.info("Middlewares added successfully")
419
420
 
420
421
  #* Add exception handler(s)
421
422
  self._loggers.application.info("Adding exception handlers")
@@ -427,7 +428,7 @@ class ServiceManager:
427
428
  exc_class_or_status_code=HTTPException,
428
429
  handler=BaseExceptions.http_exception_handler
429
430
  )
430
- self._loggers.application.info("Exception handlers addedd successfully")
431
+ self._loggers.application.info("Exception handlers added successfully")
431
432
 
432
433
  #* Include router
433
434
  self._loggers.application.info("Including routers")
@@ -0,0 +1,457 @@
1
+ import json
2
+ import threading
3
+ import time
4
+ import traceback
5
+ from collections import defaultdict
6
+ from datetime import datetime, timedelta, timezone
7
+ from typing import Awaitable, Callable, Optional, Sequence, Dict, List
8
+
9
+ from fastapi import FastAPI, Request, Response, status
10
+ from fastapi.responses import JSONResponse
11
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
12
+
13
+ from maleo_foundation.authentication import Authentication
14
+ from maleo_foundation.enums import BaseEnums
15
+ from maleo_foundation.client.manager import MaleoFoundationClientManager
16
+ from maleo_foundation.models.schemas import BaseGeneralSchemas
17
+ from maleo_foundation.models.responses import BaseResponses
18
+ from maleo_foundation.models.transfers.general.token import MaleoFoundationTokenGeneralTransfers
19
+ from maleo_foundation.models.transfers.parameters.token import MaleoFoundationTokenParametersTransfers
20
+ from maleo_foundation.models.transfers.parameters.signature import MaleoFoundationSignatureParametersTransfers
21
+ from maleo_foundation.models.transfers.general import RequestContextTransfers
22
+ from maleo_foundation.utils.extractor import extract_request_context
23
+ from maleo_foundation.utils.logging import MiddlewareLogger
24
+
25
+ RequestProcessor = Callable[[Request], Awaitable[Optional[Response]]]
26
+ ResponseProcessor = Callable[[Response], Awaitable[Response]]
27
+
28
+ class RateLimiter:
29
+ """Thread-safe rate limiter with automatic cleanup."""
30
+
31
+ def __init__(
32
+ self,
33
+ limit:int,
34
+ window:timedelta,
35
+ ip_timeout:timedelta,
36
+ cleanup_interval:timedelta
37
+ ):
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] = {}
44
+ self._last_cleanup = datetime.now()
45
+ self._lock = threading.RLock()
46
+
47
+ def is_rate_limited(
48
+ self,
49
+ request_context:RequestContextTransfers
50
+ ) -> bool:
51
+ """Check if client IP is rate limited and record the request."""
52
+ with self._lock:
53
+ now = datetime.now()
54
+ client_ip = request_context.ip_address
55
+ self._last_seen[client_ip] = now
56
+
57
+ # Remove old requests outside the window
58
+ self._requests[client_ip] = [
59
+ timestamp for timestamp in self._requests[client_ip]
60
+ if now - timestamp <= self.window
61
+ ]
62
+
63
+ # Check rate limit
64
+ if len(self._requests[client_ip]) >= self.limit:
65
+ return True
66
+
67
+ # Record this request
68
+ self._requests[client_ip].append(now)
69
+ return False
70
+
71
+ def cleanup_old_data(
72
+ self,
73
+ logger:MiddlewareLogger
74
+ ) -> None:
75
+ """Clean up old request data to prevent memory growth."""
76
+ now = datetime.now()
77
+ if now - self._last_cleanup <= self.cleanup_interval:
78
+ return
79
+
80
+ with self._lock:
81
+ inactive_ips = []
82
+
83
+ for ip in list(self._requests.keys()):
84
+ # Remove IPs with empty request lists
85
+ if not self._requests[ip]:
86
+ inactive_ips.append(ip)
87
+ continue
88
+
89
+ # Remove IPs that haven't been active recently
90
+ last_active = self._last_seen.get(ip, datetime.min)
91
+ if now - last_active > self.ip_timeout:
92
+ inactive_ips.append(ip)
93
+
94
+ # Clean up inactive IPs
95
+ for ip in inactive_ips:
96
+ self._requests.pop(ip, None)
97
+ self._last_seen.pop(ip, None)
98
+
99
+ self._last_cleanup = now
100
+ logger.debug(
101
+ f"Cleaned up request cache. Removed {len(inactive_ips)} inactive IPs. "
102
+ f"Current tracked IPs: {len(self._requests)}"
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(
117
+ self,
118
+ authentication:Authentication,
119
+ response:Response,
120
+ request_context:RequestContextTransfers,
121
+ responded_at:datetime,
122
+ process_time:float
123
+ ) -> Response:
124
+ """Add custom headers to response."""
125
+ # Basic headers
126
+ response.headers["X-Request-ID"] = str(request_context.request_id)
127
+ response.headers["X-Process-Time"] = str(process_time)
128
+ response.headers["X-Requested-At"] = request_context.requested_at.isoformat()
129
+ response.headers["X-Responded-At"] = responded_at.isoformat()
130
+
131
+ # Add signature header
132
+ self._add_signature_header(response, request_context, responded_at, process_time)
133
+
134
+ # Add new authorization header if needed
135
+ self._add_new_authorization_header(request_context, authentication, response)
136
+
137
+ return response
138
+
139
+ def _add_signature_header(
140
+ self,
141
+ response:Response,
142
+ request_context:RequestContextTransfers,
143
+ responded_at:datetime,
144
+ process_time:float
145
+ ) -> None:
146
+ """Generate and add signature header."""
147
+ message = (
148
+ f"{request_context.method}|{request_context.url}|{request_context.requested_at.isoformat()}|"
149
+ f"{responded_at.isoformat()}|{str(process_time)}|{str(request_context.request_id)}"
150
+ )
151
+
152
+ sign_parameters = MaleoFoundationSignatureParametersTransfers.Sign(
153
+ key=self.keys.private,
154
+ password=self.keys.password,
155
+ message=message
156
+ )
157
+
158
+ sign_result = self.maleo_foundation.services.signature.sign(parameters=sign_parameters)
159
+ if sign_result.success:
160
+ response.headers["X-Signature"] = sign_result.data.signature
161
+
162
+ def _add_new_authorization_header(
163
+ self,
164
+ request_context:RequestContextTransfers,
165
+ authentication:Authentication,
166
+ response:Response
167
+ ) -> None:
168
+ """Add new authorization header for refresh tokens."""
169
+ if not self._should_regenerate_auth(request_context, authentication, response):
170
+ return
171
+
172
+ payload = MaleoFoundationTokenGeneralTransfers.BaseEncodePayload.model_validate(
173
+ authentication.credentials.token.payload.model_dump()
174
+ )
175
+
176
+ parameters = MaleoFoundationTokenParametersTransfers.Encode(
177
+ key=self.keys.private,
178
+ password=self.keys.password,
179
+ payload=payload
180
+ )
181
+
182
+ result = self.maleo_foundation.services.token.encode(parameters=parameters)
183
+ if result.success:
184
+ response.headers["X-New-Authorization"] = result.data.token
185
+
186
+ def _should_regenerate_auth(
187
+ self,
188
+ request_context:RequestContextTransfers,
189
+ authentication:Authentication,
190
+ response:Response
191
+ ) -> bool:
192
+ """Check if authorization should be regenerated."""
193
+ return (
194
+ authentication.user.is_authenticated
195
+ and authentication.credentials.token.type == BaseEnums.TokenType.REFRESH
196
+ and 200 <= response.status_code < 300
197
+ and "logout" not in request_context.url
198
+ )
199
+
200
+ class RequestLogger:
201
+ """Handles request/response logging."""
202
+
203
+ def __init__(self, logger:MiddlewareLogger):
204
+ self.logger = logger
205
+
206
+ def log_request_response(
207
+ self,
208
+ authentication:Authentication,
209
+ response:Response,
210
+ request_context:RequestContextTransfers,
211
+ log_level:str = "info"
212
+ ) -> None:
213
+ """Log request and response details."""
214
+ authentication_info = self._get_authentication_info(authentication)
215
+
216
+ log_func = getattr(self.logger, log_level)
217
+ log_func(
218
+ f"Request | ID: {request_context.request_id} {authentication_info} | "
219
+ f"IP: {request_context.ip_address} | Host: {request_context.host} | "
220
+ f"Method: {request_context.method} | URL: {request_context.url} - "
221
+ f"Response | Status: {response.status_code}"
222
+ )
223
+
224
+ def log_exception(
225
+ self,
226
+ authentication:Authentication,
227
+ error:Exception,
228
+ request_context:RequestContextTransfers
229
+ ) -> None:
230
+ """Log exception details."""
231
+ authentication_info = self._get_authentication_info(authentication)
232
+
233
+ error_details = {
234
+ "request_context": request_context.model_dump(mode="json"),
235
+ "error": str(error),
236
+ "traceback": traceback.format_exc().split("\n")
237
+ }
238
+
239
+ self.logger.error(
240
+ f"Request | ID: {request_context.request_id} {authentication_info} | "
241
+ f"IP: {request_context.ip_address} | Host: {request_context.host} | "
242
+ f"Method: {request_context.method} | URL: {request_context.url} - "
243
+ f"Response | Status: 500 | Exception:\n{json.dumps(error_details, indent=4)}"
244
+ )
245
+
246
+ def _get_authentication_info(self, authentication:Authentication) -> str:
247
+ """Get authentication info string."""
248
+ if not authentication.user.is_authenticated:
249
+ return "| Unauthenticated"
250
+
251
+ return (
252
+ f"| Token type: {authentication.credentials.token.type} | "
253
+ f"Username: {authentication.user.display_name} | "
254
+ f"Email: {authentication.user.identity}"
255
+ )
256
+
257
+
258
+ class BaseMiddleware(BaseHTTPMiddleware):
259
+ """Base middleware with rate limiting, logging, and response enhancement."""
260
+
261
+ def __init__(
262
+ self,
263
+ app:FastAPI,
264
+ keys:BaseGeneralSchemas.RSAKeys,
265
+ logger:MiddlewareLogger,
266
+ maleo_foundation:MaleoFoundationClientManager,
267
+ allow_origins:Sequence[str] = (),
268
+ allow_methods:Sequence[str] = ("GET",),
269
+ allow_headers:Sequence[str] = (),
270
+ allow_credentials:bool = False,
271
+ limit:int = 10,
272
+ window:int = 1,
273
+ cleanup_interval:int = 60,
274
+ ip_timeout:int = 300
275
+ ):
276
+ super().__init__(app)
277
+
278
+ # Core components
279
+ self.rate_limiter = RateLimiter(
280
+ limit=limit,
281
+ window=timedelta(seconds=window),
282
+ ip_timeout=timedelta(seconds=ip_timeout),
283
+ cleanup_interval=timedelta(seconds=cleanup_interval)
284
+ )
285
+ self.response_builder = ResponseBuilder(keys, maleo_foundation)
286
+ self.request_logger = RequestLogger(logger)
287
+
288
+ # CORS settings (if needed)
289
+ self.cors_config = {
290
+ 'allow_origins': allow_origins,
291
+ 'allow_methods': allow_methods,
292
+ 'allow_headers': allow_headers,
293
+ 'allow_credentials': allow_credentials,
294
+ }
295
+
296
+ async def dispatch(self, request:Request, call_next:RequestResponseEndpoint) -> Response:
297
+ """Main middleware dispatch method."""
298
+ # Setup
299
+ self.rate_limiter.cleanup_old_data(self.request_logger.logger)
300
+ request_context = extract_request_context(request)
301
+ request.state.request_context = request_context
302
+ start_time = time.perf_counter()
303
+ authentication = Authentication(credentials=request.auth, user=request.user)
304
+
305
+ try:
306
+ # Rate limiting check
307
+ if self.rate_limiter.is_rate_limited(request_context):
308
+ return self._create_rate_limit_response(
309
+ authentication, request_context, start_time
310
+ )
311
+
312
+ # Optional preprocessing
313
+ pre_response = await self._request_processor(request)
314
+ if pre_response is not None:
315
+ return self._build_final_response(
316
+ authentication, pre_response, request_context, start_time
317
+ )
318
+
319
+ # Main request processing
320
+ response = await call_next(request)
321
+ return self._build_final_response(
322
+ authentication, response, request_context, start_time
323
+ )
324
+
325
+ except Exception as e:
326
+ return self._handle_exception(
327
+ request, authentication, e, request_context, start_time
328
+ )
329
+
330
+ async def _request_processor(self, request:Request) -> Optional[Response]:
331
+ """Override this method for custom request preprocessing."""
332
+ return None
333
+
334
+ def _create_rate_limit_response(
335
+ self,
336
+ authentication:Authentication,
337
+ request_context:RequestContextTransfers,
338
+ start_time:float
339
+ ) -> Response:
340
+ """Create rate limit exceeded response."""
341
+ response = JSONResponse(
342
+ content=BaseResponses.RateLimitExceeded().model_dump(),
343
+ status_code=status.HTTP_429_TOO_MANY_REQUESTS,
344
+ )
345
+
346
+ return self._build_final_response(
347
+ authentication, response, request_context, start_time, log_level="warning"
348
+ )
349
+
350
+ def _build_final_response(
351
+ self,
352
+ authentication:Authentication,
353
+ response:Response,
354
+ request_context:RequestContextTransfers,
355
+ start_time:float,
356
+ log_level:str = "info"
357
+ ) -> Response:
358
+ """Build final response with headers and logging."""
359
+ responded_at = datetime.now(tz=timezone.utc)
360
+ process_time = time.perf_counter() - start_time
361
+
362
+ # Add headers
363
+ response = self.response_builder.add_response_headers(
364
+ authentication, response, request_context, responded_at, process_time
365
+ )
366
+
367
+ # Log request/response
368
+ self.request_logger.log_request_response(
369
+ authentication, response, request_context, log_level
370
+ )
371
+
372
+ return response
373
+
374
+ def _handle_exception(
375
+ self,
376
+ authentication:Authentication,
377
+ error:Exception,
378
+ request_context:RequestContextTransfers,
379
+ start_time:float
380
+ ) -> Response:
381
+ """Handle exceptions and create error response."""
382
+ responded_at = datetime.now(tz=timezone.utc)
383
+ process_time = time.perf_counter() - start_time
384
+
385
+ response = JSONResponse(
386
+ content=BaseResponses.ServerError().model_dump(),
387
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
388
+ )
389
+
390
+ # Log exception
391
+ self.request_logger.log_exception(authentication, error, request_context)
392
+
393
+ # Add headers
394
+ return self.response_builder.add_response_headers(
395
+ authentication, response, request_context, responded_at, process_time
396
+ )
397
+
398
+
399
+ def add_base_middleware(
400
+ app:FastAPI,
401
+ keys:BaseGeneralSchemas.RSAKeys,
402
+ logger:MiddlewareLogger,
403
+ maleo_foundation:MaleoFoundationClientManager,
404
+ allow_origins:Sequence[str] = (),
405
+ allow_methods:Sequence[str] = ("GET",),
406
+ allow_headers:Sequence[str] = (),
407
+ allow_credentials:bool = False,
408
+ limit:int = 10,
409
+ window:int = 1,
410
+ cleanup_interval:int = 60,
411
+ ip_timeout:int = 300
412
+ ) -> None:
413
+ """
414
+ Add Base middleware to the FastAPI application.
415
+
416
+ Args:
417
+ app:FastAPI application instance
418
+ keys:RSA keys for signing and token generation
419
+ logger:Middleware logger instance
420
+ maleo_foundation:Client manager for foundation services
421
+ allow_origins:CORS allowed origins
422
+ allow_methods:CORS allowed methods
423
+ allow_headers:CORS allowed headers
424
+ allow_credentials:CORS allow credentials flag
425
+ limit:Request count limit per window
426
+ window:Time window for rate limiting (seconds)
427
+ cleanup_interval:Cleanup interval for old IP data (seconds)
428
+ ip_timeout:IP timeout after last activity (seconds)
429
+
430
+ Example:
431
+ ```python
432
+ add_base_middleware(
433
+ app=app,
434
+ keys=rsa_keys,
435
+ logger=middleware_logger,
436
+ maleo_foundation=client_manager,
437
+ limit=10,
438
+ window=1,
439
+ cleanup_interval=60,
440
+ ip_timeout=300
441
+ )
442
+ ```
443
+ """
444
+ app.add_middleware(
445
+ BaseMiddleware,
446
+ keys=keys,
447
+ logger=logger,
448
+ maleo_foundation=maleo_foundation,
449
+ allow_origins=allow_origins,
450
+ allow_methods=allow_methods,
451
+ allow_headers=allow_headers,
452
+ allow_credentials=allow_credentials,
453
+ limit=limit,
454
+ window=window,
455
+ cleanup_interval=cleanup_interval,
456
+ ip_timeout=ip_timeout
457
+ )
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+ from datetime import datetime, timezone
3
+ from pydantic import BaseModel, Field
4
+ from typing import Dict
5
+ from uuid import UUID
6
+ from maleo_foundation.models.schemas.general import BaseGeneralSchemas
7
+ from maleo_foundation.types import BaseTypes
8
+ from .token import MaleoFoundationTokenGeneralTransfers
9
+
10
+ class BaseGeneralTransfers:
11
+ Token = MaleoFoundationTokenGeneralTransfers
12
+
13
+ class AccessTransfers(
14
+ BaseGeneralSchemas.AccessedBy,
15
+ BaseGeneralSchemas.AccessedAt
16
+ ): pass
17
+
18
+ class RequestContextTransfers(BaseModel):
19
+ request_id:UUID = Field(..., description="Unique identifier for tracing the request")
20
+ requested_at:datetime = Field(datetime.now(tz=timezone.utc), description="Request timestamp")
21
+ method:str = Field(..., description="Request's method")
22
+ url:str = Field(..., description="Request's URL")
23
+ path_params:Dict = Field(..., description="Request's path parameters")
24
+ query_params:Dict = Field(..., description="Request's query parameters")
25
+ ip_address:str = Field("unknown", description="Client's IP address")
26
+ is_internal:BaseTypes.OptionalBoolean = Field(None, description="True if IP is internal")
27
+ user_agent:BaseTypes.OptionalString = Field(None, description="User-Agent string")
28
+ ua_browser:BaseTypes.OptionalString = Field(None, description="Browser info from sec-ch-ua")
29
+ ua_mobile:BaseTypes.OptionalString = Field(None, description="Is mobile device?")
30
+ platform:BaseTypes.OptionalString = Field(None, description="Client platform or OS")
31
+ referer:BaseTypes.OptionalString = Field(None, description="Referrer URL")
32
+ origin:BaseTypes.OptionalString = Field(None, description="Origin of the request")
33
+ host:BaseTypes.OptionalString = Field(None, description="Host header from request")
34
+ forwarded_proto:BaseTypes.OptionalString = Field(None, description="Forwarded protocol (http/https)")
35
+ language:BaseTypes.OptionalString = Field(None, description="Accepted languages from client")
@@ -0,0 +1,9 @@
1
+ from fastapi.requests import Request
2
+ from maleo_foundation.models.transfers.general import RequestContextTransfers
3
+
4
+ class ContextDependencies:
5
+ @staticmethod
6
+ def get_request_context(
7
+ request:Request
8
+ ) -> RequestContextTransfers:
9
+ return request.state.request_context
@@ -0,0 +1,55 @@
1
+ from datetime import datetime, timezone
2
+ from fastapi import Request
3
+ from starlette.requests import HTTPConnection
4
+ from uuid import uuid4
5
+ from maleo_foundation.models.transfers.general import RequestContextTransfers
6
+
7
+ def extract_client_ip(conn:HTTPConnection) -> str:
8
+ """Extract client IP with more robust handling of proxies"""
9
+ #* Check for X-Forwarded-For header (common when behind proxy/load balancer)
10
+ x_forwarded_for = conn.headers.get("X-Forwarded-For")
11
+ if x_forwarded_for:
12
+ #* The client's IP is the first one in the list
13
+ ips = [ip.strip() for ip in x_forwarded_for.split(",")]
14
+ return ips[0]
15
+
16
+ #* Check for X-Real-IP header (used by some proxies)
17
+ x_real_ip = conn.headers.get("X-Real-IP")
18
+ if x_real_ip:
19
+ return x_real_ip
20
+
21
+ #* Fall back to direct client connection
22
+ return conn.client.host if conn.client else "unknown"
23
+
24
+ def extract_request_context(request:Request) -> RequestContextTransfers:
25
+ headers = request.headers
26
+
27
+ request_id = headers.get("x-request-id")
28
+ if request_id is None:
29
+ request_id = uuid4()
30
+
31
+ ip_address = extract_client_ip(request)
32
+
33
+ ua_browser = headers.get("sec-ch-ua", "")
34
+ if ua_browser:
35
+ ua_browser = ua_browser.replace('"', "").split(",")[0].strip()
36
+
37
+ return RequestContextTransfers(
38
+ request_id=request_id,
39
+ requested_at=datetime.now(tz=timezone.utc),
40
+ method=request.method,
41
+ url=request.url.path,
42
+ path_params=dict(request.path_params),
43
+ query_params=dict(request.query_params),
44
+ ip_address=ip_address,
45
+ is_internal=ip_address.startswith("10.") or ip_address.startswith("192.168.") or ip_address.startswith("172."),
46
+ user_agent=headers.get("user-agent"),
47
+ ua_browser=ua_browser,
48
+ ua_mobile=headers.get("sec-ch-ua-mobile"),
49
+ platform=headers.get("sec-ch-ua-platform"),
50
+ referer=headers.get("referer"),
51
+ origin=headers.get("origin"),
52
+ host=headers.get("host"),
53
+ forwarded_proto=headers.get("x-forwarded-proto"),
54
+ language=headers.get("accept-language"),
55
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maleo_foundation
3
- Version: 0.2.78
3
+ Version: 0.2.81
4
4
  Summary: Foundation package for Maleo
5
5
  Author-email: Agra Bima Yuda <agra@nexmedis.com>
6
6
  License: MIT
@@ -116,6 +116,7 @@ maleo_foundation/utils/repository.py
116
116
  maleo_foundation/utils/searcher.py
117
117
  maleo_foundation/utils/dependencies/__init__.py
118
118
  maleo_foundation/utils/dependencies/auth.py
119
+ maleo_foundation/utils/dependencies/context.py
119
120
  maleo_foundation/utils/formatter/__init__.py
120
121
  maleo_foundation/utils/formatter/case.py
121
122
  maleo_foundation/utils/loaders/__init__.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maleo_foundation"
7
- version = "0.2.78"
7
+ version = "0.2.81"
8
8
  description = "Foundation package for Maleo"
9
9
  authors = [
10
10
  { name = "Agra Bima Yuda", email = "agra@nexmedis.com" }