maleo-foundation 0.2.76__tar.gz → 0.2.80__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 (131) hide show
  1. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/PKG-INFO +1 -1
  2. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/service.py +1 -0
  3. maleo_foundation-0.2.80/maleo_foundation/middlewares/base.py +491 -0
  4. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/schemas/general.py +8 -2
  5. maleo_foundation-0.2.80/maleo_foundation/models/transfers/general/__init__.py +11 -0
  6. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation.egg-info/PKG-INFO +1 -1
  7. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/pyproject.toml +1 -1
  8. maleo_foundation-0.2.76/maleo_foundation/middlewares/base.py +0 -366
  9. maleo_foundation-0.2.76/maleo_foundation/models/transfers/general/__init__.py +0 -5
  10. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/README.md +0 -0
  11. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/__init__.py +0 -0
  12. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/authentication.py +0 -0
  13. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/authorization.py +0 -0
  14. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/__init__.py +0 -0
  15. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/manager.py +0 -0
  16. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/__init__.py +0 -0
  17. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/encryption/__init__.py +0 -0
  18. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/encryption/aes.py +0 -0
  19. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/encryption/rsa.py +0 -0
  20. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/hash/__init__.py +0 -0
  21. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/hash/bcrypt.py +0 -0
  22. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/hash/hmac.py +0 -0
  23. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/hash/sha256.py +0 -0
  24. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/key.py +0 -0
  25. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/signature.py +0 -0
  26. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/client/services/token.py +0 -0
  27. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/constants.py +0 -0
  28. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/enums.py +0 -0
  29. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/__init__.py +0 -0
  30. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/client.py +0 -0
  31. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/encryption/__init__.py +0 -0
  32. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/encryption/aes.py +0 -0
  33. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/encryption/rsa.py +0 -0
  34. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/general.py +0 -0
  35. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/hash.py +0 -0
  36. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/key.py +0 -0
  37. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/repository.py +0 -0
  38. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/service.py +0 -0
  39. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/signature.py +0 -0
  40. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/expanded_types/token.py +0 -0
  41. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/extended_types.py +0 -0
  42. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/__init__.py +0 -0
  43. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/cache/__init__.py +0 -0
  44. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/cache/base.py +0 -0
  45. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/cache/redis.py +0 -0
  46. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/client/__init__.py +0 -0
  47. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/client/base.py +0 -0
  48. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/client/google/__init__.py +0 -0
  49. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/client/google/base.py +0 -0
  50. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/client/google/parameter.py +0 -0
  51. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/client/google/secret.py +0 -0
  52. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/client/google/storage.py +0 -0
  53. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/client/maleo.py +0 -0
  54. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/db.py +0 -0
  55. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/managers/middleware.py +0 -0
  56. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/middlewares/authentication.py +0 -0
  57. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/middlewares/cors.py +0 -0
  58. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/__init__.py +0 -0
  59. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/responses.py +0 -0
  60. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/schemas/__init__.py +0 -0
  61. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/schemas/encryption.py +0 -0
  62. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/schemas/hash.py +0 -0
  63. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/schemas/key.py +0 -0
  64. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/schemas/parameter.py +0 -0
  65. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/schemas/result.py +0 -0
  66. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/schemas/signature.py +0 -0
  67. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/schemas/token.py +0 -0
  68. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/table.py +0 -0
  69. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/__init__.py +0 -0
  70. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/general/key.py +0 -0
  71. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/general/signature.py +0 -0
  72. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/general/token.py +0 -0
  73. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/__init__.py +0 -0
  74. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/client.py +0 -0
  75. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/encryption/__init__.py +0 -0
  76. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/encryption/aes.py +0 -0
  77. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/encryption/rsa.py +0 -0
  78. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/general.py +0 -0
  79. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/hash/__init__.py +0 -0
  80. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/hash/bcrypt.py +0 -0
  81. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/hash/hmac.py +0 -0
  82. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/hash/sha256.py +0 -0
  83. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/key.py +0 -0
  84. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/service.py +0 -0
  85. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/signature.py +0 -0
  86. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/parameters/token.py +0 -0
  87. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/__init__.py +0 -0
  88. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/client/__init__.py +0 -0
  89. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/client/controllers/__init__.py +0 -0
  90. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/client/controllers/http.py +0 -0
  91. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/client/service.py +0 -0
  92. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/encryption/__init__.py +0 -0
  93. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/encryption/aes.py +0 -0
  94. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/encryption/rsa.py +0 -0
  95. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/hash.py +0 -0
  96. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/key.py +0 -0
  97. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/service/__init__.py +0 -0
  98. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/service/controllers/__init__.py +0 -0
  99. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/service/controllers/rest.py +0 -0
  100. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/service/general.py +0 -0
  101. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/service/repository.py +0 -0
  102. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/signature.py +0 -0
  103. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/models/transfers/results/token.py +0 -0
  104. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/rest_controller_result.py +0 -0
  105. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/types.py +0 -0
  106. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/__init__.py +0 -0
  107. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/client.py +0 -0
  108. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/controller.py +0 -0
  109. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/dependencies/__init__.py +0 -0
  110. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/dependencies/auth.py +0 -0
  111. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/exceptions.py +0 -0
  112. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/extractor.py +0 -0
  113. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/formatter/__init__.py +0 -0
  114. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/formatter/case.py +0 -0
  115. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/loaders/__init__.py +0 -0
  116. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/loaders/credential/__init__.py +0 -0
  117. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/loaders/credential/google.py +0 -0
  118. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/loaders/json.py +0 -0
  119. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/loaders/key/__init__.py +0 -0
  120. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/loaders/key/rsa.py +0 -0
  121. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/loaders/yaml.py +0 -0
  122. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/logging.py +0 -0
  123. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/merger.py +0 -0
  124. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/query.py +0 -0
  125. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/repository.py +0 -0
  126. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation/utils/searcher.py +0 -0
  127. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation.egg-info/SOURCES.txt +0 -0
  128. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation.egg-info/dependency_links.txt +0 -0
  129. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation.egg-info/requires.txt +0 -0
  130. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/maleo_foundation.egg-info/top_level.txt +0 -0
  131. {maleo_foundation-0.2.76 → maleo_foundation-0.2.80}/setup.cfg +0 -0
@@ -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
@@ -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")
@@ -0,0 +1,491 @@
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
+ from uuid import UUID, uuid4
9
+
10
+ from fastapi import FastAPI, Request, Response, status
11
+ from fastapi.responses import JSONResponse
12
+ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
13
+
14
+ from maleo_foundation.authentication import Authentication
15
+ from maleo_foundation.enums import BaseEnums
16
+ from maleo_foundation.client.manager import MaleoFoundationClientManager
17
+ from maleo_foundation.models.schemas import BaseGeneralSchemas
18
+ from maleo_foundation.models.responses import BaseResponses
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
+ from maleo_foundation.utils.extractor import BaseExtractors
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
+ client_ip:str
50
+ ) -> bool:
51
+ """Check if client IP is rate limited and record the request."""
52
+ with self._lock:
53
+ now = datetime.now()
54
+ self._last_seen[client_ip] = now
55
+
56
+ # Remove old requests outside the window
57
+ self._requests[client_ip] = [
58
+ timestamp for timestamp in self._requests[client_ip]
59
+ if now - timestamp <= self.window
60
+ ]
61
+
62
+ # Check rate limit
63
+ if len(self._requests[client_ip]) >= self.limit:
64
+ return True
65
+
66
+ # Record this request
67
+ self._requests[client_ip].append(now)
68
+ return False
69
+
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(
117
+ self,
118
+ request:Request,
119
+ authentication:Authentication,
120
+ response:Response,
121
+ request_timestamp:datetime,
122
+ response_timestamp:datetime,
123
+ process_time:float,
124
+ request_id:UUID
125
+ ) -> Response:
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)}"
154
+ )
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)
163
+ if sign_result.success:
164
+ response.headers["X-Signature"] = sign_result.data.signature
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
199
+ and authentication.credentials.token.type == BaseEnums.TokenType.REFRESH
200
+ and 200 <= response.status_code < 300
201
+ and "logout" not in request.url.path
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
213
+
214
+ def log_request_response(
215
+ self,
216
+ request:Request,
217
+ authentication:Authentication,
218
+ response:Response,
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)
227
+ log_func(
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} | "
231
+ f"Headers: {dict(request.headers)} - Response | Status: {response.status_code}"
232
+ )
233
+
234
+ def log_exception(
235
+ self,
236
+ request:Request,
237
+ authentication:Authentication,
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
+
245
+ error_details = {
246
+ "request_id": str(request_id),
247
+ "error": str(error),
248
+ "traceback": traceback.format_exc().split("\n"),
249
+ "client_ip": client_ip,
250
+ "method": request.method,
251
+ "url": request.url.path,
252
+ "headers": dict(request.headers),
253
+ }
254
+
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)}"
261
+ )
262
+
263
+ def _get_authentication_info(self, authentication:Authentication) -> str:
264
+ """Get authentication info string."""
265
+ if not authentication.user.is_authenticated:
266
+ return "| Unauthenticated"
267
+
268
+ return (
269
+ f"| Token type: {authentication.credentials.token.type} | "
270
+ f"Username: {authentication.user.display_name} | "
271
+ f"Email: {authentication.user.identity}"
272
+ )
273
+
274
+
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)
301
+ )
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)
326
+
327
+ try:
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
332
+ )
333
+
334
+ # Optional preprocessing
335
+ pre_response = await self._request_processor(request)
336
+ if pre_response is not None:
337
+ return self._build_final_response(
338
+ request, authentication, pre_response, client_ip,
339
+ request_timestamp, start_time, request_id
340
+ )
341
+
342
+ # Main request processing
343
+ response = await call_next(request)
344
+ return self._build_final_response(
345
+ request, authentication, response, client_ip,
346
+ request_timestamp, start_time, request_id
347
+ )
348
+
349
+ except Exception as e:
350
+ return self._handle_exception(
351
+ request, authentication, e, client_ip, request_timestamp, start_time, request_id
352
+ )
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
+
433
+ def add_base_middleware(
434
+ app:FastAPI,
435
+ keys:BaseGeneralSchemas.RSAKeys,
436
+ logger:MiddlewareLogger,
437
+ maleo_foundation:MaleoFoundationClientManager,
438
+ allow_origins:Sequence[str] = (),
439
+ allow_methods:Sequence[str] = ("GET",),
440
+ allow_headers:Sequence[str] = (),
441
+ allow_credentials:bool = False,
442
+ limit:int = 10,
443
+ window:int = 1,
444
+ cleanup_interval:int = 60,
445
+ ip_timeout:int = 300
446
+ ) -> None:
447
+ """
448
+ Add Base middleware to the FastAPI application.
449
+
450
+ Args:
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)
463
+
464
+ Example:
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
+ ```
477
+ """
478
+ app.add_middleware(
479
+ BaseMiddleware,
480
+ keys=keys,
481
+ logger=logger,
482
+ maleo_foundation=maleo_foundation,
483
+ allow_origins=allow_origins,
484
+ allow_methods=allow_methods,
485
+ allow_headers=allow_headers,
486
+ allow_credentials=allow_credentials,
487
+ limit=limit,
488
+ window=window,
489
+ cleanup_interval=cleanup_interval,
490
+ ip_timeout=ip_timeout
491
+ )
@@ -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")
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+ from maleo_foundation.models.schemas.general import BaseGeneralSchemas
3
+ from .token import MaleoFoundationTokenGeneralTransfers
4
+
5
+ class BaseGeneralTransfers:
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "maleo_foundation"
7
- version = "0.2.76"
7
+ version = "0.2.80"
8
8
  description = "Foundation package for Maleo"
9
9
  authors = [
10
10
  { name = "Agra Bima Yuda", email = "agra@nexmedis.com" }