maleo-foundation 0.0.4__py3-none-any.whl → 0.0.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- maleo_foundation/middlewares/__init__.py +98 -0
- maleo_foundation/middlewares/base.py +286 -0
- maleo_foundation/middlewares/cors.py +63 -0
- {maleo_foundation-0.0.4.dist-info → maleo_foundation-0.0.5.dist-info}/METADATA +1 -1
- {maleo_foundation-0.0.4.dist-info → maleo_foundation-0.0.5.dist-info}/RECORD +7 -4
- {maleo_foundation-0.0.4.dist-info → maleo_foundation-0.0.5.dist-info}/WHEEL +0 -0
- {maleo_foundation-0.0.4.dist-info → maleo_foundation-0.0.5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,98 @@
|
|
1
|
+
from fastapi import FastAPI
|
2
|
+
from logging import Logger
|
3
|
+
from typing import Optional, Sequence
|
4
|
+
from .base import add_base_middleware, RequestProcessor
|
5
|
+
from .cors import add_cors_middleware
|
6
|
+
|
7
|
+
class MiddlewareManager:
|
8
|
+
_default_limit:int = 10
|
9
|
+
_default_window:int = 1
|
10
|
+
_default_cleanup_interval:int = 60
|
11
|
+
_default_ip_timeout:int = 300
|
12
|
+
_default_allow_origins:Sequence[str] = ()
|
13
|
+
_default_allow_methods:Sequence[str] = ("GET",)
|
14
|
+
_default_allow_headers:Sequence[str] = ()
|
15
|
+
_default_allow_credentials:bool = False
|
16
|
+
_default_expose_headers:Sequence[str] = ()
|
17
|
+
_default_request_processor:Optional[RequestProcessor] = None
|
18
|
+
|
19
|
+
def __init__(self, app:FastAPI):
|
20
|
+
self.app = app
|
21
|
+
|
22
|
+
def add_all_middlewares(
|
23
|
+
self,
|
24
|
+
logger:Logger,
|
25
|
+
limit:int = _default_limit,
|
26
|
+
window:int = _default_window,
|
27
|
+
cleanup_interval:int = _default_cleanup_interval,
|
28
|
+
ip_timeout:int = _default_ip_timeout,
|
29
|
+
allow_origins:Sequence[str] = _default_allow_origins,
|
30
|
+
allow_methods:Sequence[str] = _default_allow_methods,
|
31
|
+
allow_headers:Sequence[str] = _default_allow_headers,
|
32
|
+
allow_credentials:bool = _default_allow_credentials,
|
33
|
+
expose_headers:Sequence[str] = _default_expose_headers,
|
34
|
+
request_processor:Optional[RequestProcessor] = _default_request_processor
|
35
|
+
):
|
36
|
+
self.add_cors_middleware(
|
37
|
+
allow_origins=allow_origins,
|
38
|
+
allow_methods=allow_methods,
|
39
|
+
allow_headers=allow_headers,
|
40
|
+
allow_credentials=allow_credentials,
|
41
|
+
expose_headers=expose_headers
|
42
|
+
)
|
43
|
+
self.add_base_middleware(
|
44
|
+
logger=logger,
|
45
|
+
allow_origins=allow_origins,
|
46
|
+
allow_methods=allow_methods,
|
47
|
+
allow_headers=allow_headers,
|
48
|
+
allow_credentials=allow_credentials,
|
49
|
+
limit=limit,
|
50
|
+
window=window,
|
51
|
+
cleanup_interval=cleanup_interval,
|
52
|
+
ip_timeout=ip_timeout,
|
53
|
+
request_processor=request_processor
|
54
|
+
)
|
55
|
+
|
56
|
+
def add_cors_middleware(
|
57
|
+
self,
|
58
|
+
allow_origins:Sequence[str] = _default_allow_origins,
|
59
|
+
allow_methods:Sequence[str] = _default_allow_methods,
|
60
|
+
allow_headers:Sequence[str] = _default_allow_headers,
|
61
|
+
allow_credentials:bool = _default_allow_credentials,
|
62
|
+
expose_headers:Sequence[str] = _default_expose_headers
|
63
|
+
):
|
64
|
+
add_cors_middleware(
|
65
|
+
app=self.app,
|
66
|
+
allow_origins=allow_origins,
|
67
|
+
allow_methods=allow_methods,
|
68
|
+
allow_headers=allow_headers,
|
69
|
+
allow_credentials=allow_credentials,
|
70
|
+
expose_headers=expose_headers
|
71
|
+
)
|
72
|
+
|
73
|
+
def add_base_middleware(
|
74
|
+
self,
|
75
|
+
logger:Logger,
|
76
|
+
allow_origins:Sequence[str] = _default_allow_origins,
|
77
|
+
allow_methods:Sequence[str] = _default_allow_methods,
|
78
|
+
allow_headers:Sequence[str] = _default_allow_headers,
|
79
|
+
allow_credentials:bool = _default_allow_credentials,
|
80
|
+
limit:int = _default_limit,
|
81
|
+
window:int = _default_window,
|
82
|
+
cleanup_interval:int = _default_cleanup_interval,
|
83
|
+
ip_timeout:int = _default_ip_timeout,
|
84
|
+
request_processor:Optional[RequestProcessor] = _default_request_processor
|
85
|
+
):
|
86
|
+
add_base_middleware(
|
87
|
+
app=self.app,
|
88
|
+
logger=logger,
|
89
|
+
allow_origins=allow_origins,
|
90
|
+
allow_methods=allow_methods,
|
91
|
+
allow_headers=allow_headers,
|
92
|
+
allow_credentials=allow_credentials,
|
93
|
+
limit=limit,
|
94
|
+
window=window,
|
95
|
+
cleanup_interval=cleanup_interval,
|
96
|
+
ip_timeout=ip_timeout,
|
97
|
+
request_processor=request_processor
|
98
|
+
)
|
@@ -0,0 +1,286 @@
|
|
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 fastapi import FastAPI, Request, Response, status
|
8
|
+
from fastapi.responses import JSONResponse
|
9
|
+
from logging import Logger
|
10
|
+
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
11
|
+
from typing import Awaitable, Callable, Optional, Sequence
|
12
|
+
from maleo_foundation.models.responses import BaseResponses
|
13
|
+
|
14
|
+
RequestProcessor = Callable[[Request], Awaitable[Optional[Response]]]
|
15
|
+
ResponseProcessor = Callable[[Response], Awaitable[Response]]
|
16
|
+
|
17
|
+
class BaseMiddleware(BaseHTTPMiddleware):
|
18
|
+
def __init__(
|
19
|
+
self,
|
20
|
+
app,
|
21
|
+
logger:Logger,
|
22
|
+
allow_origins:Sequence[str] = (),
|
23
|
+
allow_methods:Sequence[str] = ("GET",),
|
24
|
+
allow_headers:Sequence[str] = (),
|
25
|
+
allow_credentials:bool = False,
|
26
|
+
limit:int = 10,
|
27
|
+
window:int = 1,
|
28
|
+
cleanup_interval:int = 60,
|
29
|
+
ip_timeout:int = 300,
|
30
|
+
request_processor:Optional[RequestProcessor] = None
|
31
|
+
):
|
32
|
+
super().__init__(app)
|
33
|
+
self.logger = logger
|
34
|
+
self.allow_origins = allow_origins
|
35
|
+
self.allow_methods = allow_methods
|
36
|
+
self.allow_headers = allow_headers
|
37
|
+
self.allow_credentials = allow_credentials
|
38
|
+
self.limit = limit
|
39
|
+
self.window = timedelta(seconds=window)
|
40
|
+
self.cleanup_interval = timedelta(seconds=cleanup_interval)
|
41
|
+
self.ip_timeout = timedelta(seconds=ip_timeout)
|
42
|
+
self.requests:dict[str, list[datetime]] = defaultdict(list)
|
43
|
+
self.last_seen: dict[str, datetime] = {}
|
44
|
+
self.last_cleanup = datetime.now()
|
45
|
+
self.request_processor = request_processor if request_processor is not None else self._request_processor
|
46
|
+
self._lock = threading.RLock() #* Use RLock for thread safety
|
47
|
+
|
48
|
+
def _cleanup_old_data(self) -> None:
|
49
|
+
"""
|
50
|
+
Periodically clean up old request data to prevent memory growth.
|
51
|
+
Removes:
|
52
|
+
1. IPs with empty request lists
|
53
|
+
2. IPs that haven't been seen in ip_timeout period
|
54
|
+
"""
|
55
|
+
now = datetime.now()
|
56
|
+
if now - self.last_cleanup > self.cleanup_interval:
|
57
|
+
with self._lock:
|
58
|
+
#* Remove inactive IPs (not seen recently) and empty lists
|
59
|
+
inactive_ips = []
|
60
|
+
for ip in list(self.requests.keys()):
|
61
|
+
#* Remove IPs with empty timestamp lists
|
62
|
+
if not self.requests[ip]:
|
63
|
+
inactive_ips.append(ip)
|
64
|
+
continue
|
65
|
+
|
66
|
+
#* Remove IPs that haven't been active recently
|
67
|
+
last_active = self.last_seen.get(ip, datetime.min)
|
68
|
+
if now - last_active > self.ip_timeout:
|
69
|
+
inactive_ips.append(ip)
|
70
|
+
|
71
|
+
#* Remove the inactive IPs
|
72
|
+
for ip in inactive_ips:
|
73
|
+
if ip in self.requests:
|
74
|
+
del self.requests[ip]
|
75
|
+
if ip in self.last_seen:
|
76
|
+
del self.last_seen[ip]
|
77
|
+
|
78
|
+
# Update last cleanup time
|
79
|
+
self.last_cleanup = now
|
80
|
+
self.logger.debug(f"Cleaned up request cache. Removed {len(inactive_ips)} inactive IPs. Current tracked IPs: {len(self.requests)}")
|
81
|
+
|
82
|
+
def _extract_client_ip(self, request:Request) -> str:
|
83
|
+
"""Extract client IP with more robust handling of proxies"""
|
84
|
+
#* Check for X-Forwarded-For header (common when behind proxy/load balancer)
|
85
|
+
x_forwarded_for = request.headers.get("X-Forwarded-For")
|
86
|
+
if x_forwarded_for:
|
87
|
+
#* The client's IP is the first one in the list
|
88
|
+
ips = [ip.strip() for ip in x_forwarded_for.split(",")]
|
89
|
+
return ips[0]
|
90
|
+
|
91
|
+
#* Check for X-Real-IP header (used by some proxies)
|
92
|
+
x_real_ip = request.headers.get("X-Real-IP")
|
93
|
+
if x_real_ip:
|
94
|
+
return x_real_ip
|
95
|
+
|
96
|
+
#* Fall back to direct client connection
|
97
|
+
return request.client.host if request.client else "unknown"
|
98
|
+
|
99
|
+
def _check_rate_limit(self, client_ip:str) -> bool:
|
100
|
+
"""Check if the client has exceeded their rate limit"""
|
101
|
+
with self._lock:
|
102
|
+
now = datetime.now() #* Define current timestamp
|
103
|
+
self.last_seen[client_ip] = now #* Update last seen timestamp for this IP
|
104
|
+
|
105
|
+
#* Filter requests within the window
|
106
|
+
self.requests[client_ip] = [timestamp for timestamp in self.requests[client_ip] if now - timestamp <= self.window]
|
107
|
+
|
108
|
+
#* Check if the request count exceeds the limit
|
109
|
+
if len(self.requests[client_ip]) >= self.limit:
|
110
|
+
return True
|
111
|
+
|
112
|
+
#* Add the current request timestamp
|
113
|
+
self.requests[client_ip].append(now)
|
114
|
+
return False
|
115
|
+
|
116
|
+
def _append_cors_headers(self, request:Request, response:Response) -> Response:
|
117
|
+
origin = request.headers.get("Origin")
|
118
|
+
|
119
|
+
if origin in self.allow_origins:
|
120
|
+
response.headers["Access-Control-Allow-Origin"] = origin
|
121
|
+
response.headers["Access-Control-Allow-Methods"] = ", ".join(self.allow_methods)
|
122
|
+
response.headers["Access-Control-Allow-Headers"] = ", ".join(self.allow_headers)
|
123
|
+
response.headers["Access-Control-Allow-Credentials"] = "true" if self.allow_credentials else "false"
|
124
|
+
|
125
|
+
return response
|
126
|
+
|
127
|
+
def _add_response_headers(self, request:Request, response:Response, request_timestamp:datetime, process_time:int) -> Response:
|
128
|
+
response.headers["X-Process-Time"] = str(process_time) #* Add Process Time Header
|
129
|
+
response.headers["X-Request-Timestamp"] = request_timestamp.isoformat() #* Add request timestamp header
|
130
|
+
response.headers["X-Response-Timestamp"] = datetime.now(tz=timezone.utc).isoformat() #* Add response timestamp header
|
131
|
+
response = self._append_cors_headers(request=request, response=response) #* Re-append CORS headers
|
132
|
+
return response
|
133
|
+
|
134
|
+
def _build_response(
|
135
|
+
self,
|
136
|
+
request:Request,
|
137
|
+
response:Response,
|
138
|
+
request_timestamp:datetime,
|
139
|
+
process_time:int,
|
140
|
+
log_level:str = "info",
|
141
|
+
client_ip:str = "unknown"
|
142
|
+
) -> Response:
|
143
|
+
response = self._add_response_headers(request, response, request_timestamp, process_time)
|
144
|
+
log_func = getattr(self.logger, log_level)
|
145
|
+
log_func(
|
146
|
+
f"Request | IP: {client_ip} | Method: {request.method} | URL: {request.url.path} | "
|
147
|
+
f"Headers: {dict(request.headers)} - Response | Status: {response.status_code} | "
|
148
|
+
)
|
149
|
+
return response
|
150
|
+
|
151
|
+
def _handle_exception(self, request:Request, error, request_timestamp:datetime, process_time:int, client_ip):
|
152
|
+
traceback_str = traceback.format_exc().split("\n")
|
153
|
+
error_details = {
|
154
|
+
"error": str(error),
|
155
|
+
"traceback": traceback_str,
|
156
|
+
"client_ip": client_ip,
|
157
|
+
"method": request.method,
|
158
|
+
"url": request.url.path,
|
159
|
+
"headers": dict(request.headers),
|
160
|
+
}
|
161
|
+
|
162
|
+
response = JSONResponse(
|
163
|
+
content=BaseResponses.ServerError().model_dump(),
|
164
|
+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
165
|
+
)
|
166
|
+
|
167
|
+
self.logger.error(
|
168
|
+
f"Request | IP: {client_ip} | Method: {request.method} | URL: {request.url.path} | "
|
169
|
+
f"Headers: {dict(request.headers)} - Response | Status: 500 | Exception:\n{json.dumps(error_details, indent=4)}"
|
170
|
+
)
|
171
|
+
|
172
|
+
return self._add_response_headers(request, response, request_timestamp, process_time)
|
173
|
+
|
174
|
+
async def _request_processor(self, request:Request) -> Optional[Response]:
|
175
|
+
return None
|
176
|
+
|
177
|
+
async def dispatch(self, request:Request, call_next:RequestResponseEndpoint):
|
178
|
+
self._cleanup_old_data() #* Run periodic cleanup
|
179
|
+
request_timestamp = datetime.now(tz=timezone.utc) #* Record the request timestamp
|
180
|
+
start_time = time.perf_counter() #* Record the start time
|
181
|
+
client_ip = self._extract_client_ip(request) #* Get request IP with improved extraction
|
182
|
+
|
183
|
+
try:
|
184
|
+
#* 1. Rate limit check
|
185
|
+
if self._check_rate_limit(client_ip):
|
186
|
+
return self._build_response(
|
187
|
+
request=request,
|
188
|
+
response=JSONResponse(
|
189
|
+
content=BaseResponses.RateLimitExceeded().model_dump(),
|
190
|
+
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
191
|
+
),
|
192
|
+
request_timestamp=request_timestamp,
|
193
|
+
process_time=time.perf_counter() - start_time,
|
194
|
+
log_level="warning",
|
195
|
+
client_ip=client_ip,
|
196
|
+
)
|
197
|
+
|
198
|
+
#* 2. Optional preprocessing
|
199
|
+
pre_response = await self.request_processor(request)
|
200
|
+
if pre_response is not None:
|
201
|
+
return self._build_response(
|
202
|
+
request=request,
|
203
|
+
response=pre_response,
|
204
|
+
request_timestamp=request_timestamp,
|
205
|
+
process_time=time.perf_counter() - start_time,
|
206
|
+
log_level="info",
|
207
|
+
client_ip=client_ip,
|
208
|
+
)
|
209
|
+
|
210
|
+
#* 3. Main handler
|
211
|
+
response = await call_next(request)
|
212
|
+
response = self._build_response(
|
213
|
+
request=request,
|
214
|
+
response=response,
|
215
|
+
request_timestamp=request_timestamp,
|
216
|
+
process_time=time.perf_counter() - start_time,
|
217
|
+
log_level="info",
|
218
|
+
client_ip=client_ip,
|
219
|
+
)
|
220
|
+
|
221
|
+
return response
|
222
|
+
|
223
|
+
except Exception as e:
|
224
|
+
return self._handle_exception(request, e, request_timestamp, time.perf_counter() - start_time, client_ip)
|
225
|
+
|
226
|
+
def add_base_middleware(
|
227
|
+
app:FastAPI,
|
228
|
+
logger:Logger,
|
229
|
+
allow_origins:Sequence[str] = (),
|
230
|
+
allow_methods:Sequence[str] = ("GET",),
|
231
|
+
allow_headers:Sequence[str] = (),
|
232
|
+
allow_credentials:bool = False,
|
233
|
+
limit:int = 10,
|
234
|
+
window:int = 1,
|
235
|
+
cleanup_interval:int = 60,
|
236
|
+
ip_timeout:int = 300,
|
237
|
+
request_processor:Optional[RequestProcessor] = None
|
238
|
+
) -> None:
|
239
|
+
"""
|
240
|
+
Adds Base middleware to the FastAPI application.
|
241
|
+
|
242
|
+
Args:
|
243
|
+
app: FastAPI
|
244
|
+
The FastAPI application instance to which the middleware will be added.
|
245
|
+
|
246
|
+
logger: Logger
|
247
|
+
The middleware logger to be used.
|
248
|
+
|
249
|
+
limit: int
|
250
|
+
Request count limit in a specific window of time
|
251
|
+
|
252
|
+
window: int
|
253
|
+
Time window for rate limiting (in seconds).
|
254
|
+
|
255
|
+
cleanup_interval: int
|
256
|
+
How often to clean up old IP data (in seconds).
|
257
|
+
|
258
|
+
ip_timeout: int
|
259
|
+
How long to keep an IP in memory after its last activity (in seconds).
|
260
|
+
Default is 300 seconds (5 minutes).
|
261
|
+
|
262
|
+
Returns:
|
263
|
+
None: The function modifies the FastAPI app by adding Base middleware.
|
264
|
+
|
265
|
+
Note:
|
266
|
+
FastAPI applies middleware in reverse order of registration, so this middleware
|
267
|
+
will execute after any middleware added subsequently.
|
268
|
+
|
269
|
+
Example:
|
270
|
+
```python
|
271
|
+
add_base_middleware(app=app, limit=10, window=1, cleanup_interval=60, ip_timeout=300)
|
272
|
+
```
|
273
|
+
"""
|
274
|
+
app.add_middleware(
|
275
|
+
BaseMiddleware,
|
276
|
+
logger=logger,
|
277
|
+
allow_origins=allow_origins,
|
278
|
+
allow_methods=allow_methods,
|
279
|
+
allow_headers=allow_headers,
|
280
|
+
allow_credentials=allow_credentials,
|
281
|
+
limit=limit,
|
282
|
+
window=window,
|
283
|
+
cleanup_interval=cleanup_interval,
|
284
|
+
ip_timeout=ip_timeout,
|
285
|
+
request_processor=request_processor
|
286
|
+
)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
from fastapi import FastAPI
|
2
|
+
from fastapi.middleware.cors import CORSMiddleware
|
3
|
+
from typing import Sequence
|
4
|
+
|
5
|
+
def add_cors_middleware(
|
6
|
+
app:FastAPI,
|
7
|
+
allow_origins:Sequence[str] = (),
|
8
|
+
allow_methods:Sequence[str] = ("GET",),
|
9
|
+
allow_headers:Sequence[str] = (),
|
10
|
+
allow_credentials:bool = False,
|
11
|
+
expose_headers:Sequence[str] = ()
|
12
|
+
) -> None:
|
13
|
+
"""
|
14
|
+
Adds CORS (Cross-Origin Resource Sharing) middleware to the FastAPI application.
|
15
|
+
|
16
|
+
This middleware allows the server to handle requests from different origins,
|
17
|
+
which is essential for enabling communication between the backend and frontend hosted on different domains.
|
18
|
+
|
19
|
+
Args:
|
20
|
+
app: FastAPI
|
21
|
+
The FastAPI application instance to which the middleware will be added.
|
22
|
+
|
23
|
+
allow_origins: Sequence[str]
|
24
|
+
A Sequence of allowed origins (e.g., ["http://localhost:3000", "https://example.com"]).
|
25
|
+
Use ["*"] to allow requests from any origin.
|
26
|
+
|
27
|
+
allow_methods: Sequence[str]
|
28
|
+
A Sequence of allowed HTTP methods (e.g., ["GET", "POST", "PUT", "DELETE"]).
|
29
|
+
Use ["*"] to allow all methods.
|
30
|
+
|
31
|
+
allow_headers: Sequence[str]
|
32
|
+
A Sequence of allowed request headers (e.g., ["Authorization", "Content-Type"]).
|
33
|
+
Use ["*"] to allow all headers.
|
34
|
+
|
35
|
+
allow_credentials: bool
|
36
|
+
Indicates whether cookies, authorization headers, or TLS client certificates can be included in the request.
|
37
|
+
|
38
|
+
expose_headers: Sequence[str]
|
39
|
+
A Sequence of response headers that the browser can access (e.g., ["X-Custom-Header", "Content-Disposition"]).
|
40
|
+
|
41
|
+
Returns:
|
42
|
+
None: The function modifies the FastAPI app by adding CORS middleware.
|
43
|
+
|
44
|
+
Example:
|
45
|
+
```python
|
46
|
+
add_cors_middleware(
|
47
|
+
app=app,
|
48
|
+
allow_origins=["http://localhost:3000"],
|
49
|
+
allow_methods=["GET", "POST", "PUT", "DELETE"],
|
50
|
+
allow_headers=["Authorization", "Content-Type"],
|
51
|
+
allow_credentials=True,
|
52
|
+
expose_headers=["X-Custom-Header"]
|
53
|
+
)
|
54
|
+
```
|
55
|
+
"""
|
56
|
+
app.add_middleware(
|
57
|
+
CORSMiddleware,
|
58
|
+
allow_origins=allow_origins,
|
59
|
+
allow_methods=allow_methods,
|
60
|
+
allow_headers=allow_headers,
|
61
|
+
allow_credentials=allow_credentials,
|
62
|
+
expose_headers=expose_headers
|
63
|
+
)
|
@@ -13,6 +13,9 @@ maleo_foundation/db/__init__.py,sha256=fFqGxpsiowiws70AqOfcOWFhwaahfOj9_05JSJ0iR
|
|
13
13
|
maleo_foundation/db/database.py,sha256=RQBWeUearfrgq2L8-8cFVBByjNku9OiY3AmucRWXUOw,2064
|
14
14
|
maleo_foundation/db/engine.py,sha256=kw2SMMiWy5KARquh4TLk7v6HwlHW97lUIUvqdFbhNxk,1600
|
15
15
|
maleo_foundation/db/session.py,sha256=tI13DJ6rLo8FOpSIFZrmD4dbuxY_c0RTjwEGb76ZOo0,2681
|
16
|
+
maleo_foundation/middlewares/__init__.py,sha256=bqE2EIFC3rWcR2AwFPR0fk2kSFfeTRzgA24GbnuT5RA,3697
|
17
|
+
maleo_foundation/middlewares/base.py,sha256=KcpODNs0DQXX8Cwc6T9dC6aiZIqxTLtuJjVAGWXPSKk,11633
|
18
|
+
maleo_foundation/middlewares/cors.py,sha256=9uvBvY2N6Vxa9RP_YtESxcWo6Doi6uS0lzAG9iLY7Uc,2288
|
16
19
|
maleo_foundation/models/__init__.py,sha256=Wh92XAduE1Sg9qi2GMhptyXig0fKcTS5AbGo3tE3tLs,348
|
17
20
|
maleo_foundation/models/enums.py,sha256=Ob2v312JxypHEq7hTKZKOV462FEeLkIjIhpx-YF6Jdw,2203
|
18
21
|
maleo_foundation/models/responses.py,sha256=z2XEHwuxU05NDJSgrA7B7e2fzEQP6Kl3ZM2BO-lDw_A,3786
|
@@ -44,7 +47,7 @@ maleo_foundation/utils/exceptions.py,sha256=mcvBwHm6uWpVQkPtO1T2j-GaTYEiyPOeGxiD
|
|
44
47
|
maleo_foundation/utils/logger.py,sha256=ICrFi0MxuAjDy8KTRL7pex1miwzZqZX-HHArgN3niJM,2453
|
45
48
|
maleo_foundation/utils/formatter/__init__.py,sha256=iKf5YCbEdg1qKnFHyKqqcQbqAqEeRUf8mhI3v3dQoj8,78
|
46
49
|
maleo_foundation/utils/formatter/case.py,sha256=TmvvlfzGdC_omMTB5vAa40TZBxQ3hnr-SYeo0M52Rlg,1352
|
47
|
-
maleo_foundation-0.0.
|
48
|
-
maleo_foundation-0.0.
|
49
|
-
maleo_foundation-0.0.
|
50
|
-
maleo_foundation-0.0.
|
50
|
+
maleo_foundation-0.0.5.dist-info/METADATA,sha256=ZoV6fmlg40gLu2oS_bOMIX-oCiT3TWNuiSLBxN_AT2w,3159
|
51
|
+
maleo_foundation-0.0.5.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
52
|
+
maleo_foundation-0.0.5.dist-info/top_level.txt,sha256=_iBos3F_bhEOdjOnzeiEYSrCucasc810xXtLBXI8cQc,17
|
53
|
+
maleo_foundation-0.0.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|