maleo-foundation 0.0.3__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.
@@ -3,21 +3,21 @@ from contextlib import asynccontextmanager
3
3
  from typing import AsyncGenerator, Optional
4
4
 
5
5
  class HTTPClientManager:
6
- _client:Optional[httpx.AsyncClient] = None
6
+ client:Optional[httpx.AsyncClient] = None
7
7
 
8
8
  @classmethod
9
9
  def initialize(cls) -> None:
10
10
  """Initialize the HTTP client if not already initialized."""
11
- if cls._client is None:
12
- cls._client = httpx.AsyncClient()
11
+ if cls.client is None:
12
+ cls.client = httpx.AsyncClient()
13
13
 
14
14
  @classmethod
15
15
  async def _client_handler(cls) -> AsyncGenerator[httpx.AsyncClient, None]:
16
16
  """Reusable generator for client handling."""
17
- if cls._client is None:
17
+ if cls.client is None:
18
18
  raise RuntimeError("Client has not been initialized. Call initialize first.")
19
19
 
20
- yield cls._client
20
+ yield cls.client
21
21
 
22
22
  @classmethod
23
23
  async def inject(cls) -> AsyncGenerator[httpx.AsyncClient, None]:
@@ -36,6 +36,6 @@ class HTTPClientManager:
36
36
  @classmethod
37
37
  async def dispose(cls) -> None:
38
38
  """Dispose of the HTTP client and release any resources."""
39
- if cls._client is not None:
40
- await cls._client.aclose()
41
- cls._client = None
39
+ if cls.client is not None:
40
+ await cls.client.aclose()
41
+ cls.client = None
@@ -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
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: maleo_foundation
3
- Version: 0.0.3
3
+ Version: 0.0.5
4
4
  Summary: Foundation package for Maleo
5
5
  Author-email: Agra Bima Yuda <agra@nexmedis.com>
6
6
  License: MIT
@@ -3,7 +3,7 @@ maleo_foundation/constants.py,sha256=l6WQp0nu2y4Ucs8bFcEdzVUJfR1dRM_nveuHGMhe3rY
3
3
  maleo_foundation/controller.py,sha256=91OK1W4MnO85m0GUOQv1F8m50nlTZpZJ2HJcScH7OGA,2980
4
4
  maleo_foundation/clients/__init__.py,sha256=W8vydJYeDEi6gdmOZSBFSSDsfZJtb8C05CHErZgsZ30,188
5
5
  maleo_foundation/clients/general/__init__.py,sha256=l9eQrBeLW4aXtGU5aK3i6fD-msVR4526W7D9V8WCXIg,91
6
- maleo_foundation/clients/general/http.py,sha256=cE-sYlC1fXURwxE1d8pgNoUU9GKKSlPbwqF4EbslX8Q,1361
6
+ maleo_foundation/clients/general/http.py,sha256=Awvs470hgdhZSZW_uoIFJGcJ5hcfDEIX0A2yUaP9UCA,1353
7
7
  maleo_foundation/clients/google/__init__.py,sha256=1uv6nF9QbATsSAcMimQOT7Y-eBljjDunBojNX6oAtS8,90
8
8
  maleo_foundation/clients/google/cloud/__init__.py,sha256=WGMPxEKKdkz3XGY5dZn9E-nYhD1kv1MgRHbmVnky4zk,245
9
9
  maleo_foundation/clients/google/cloud/logging.py,sha256=l8EL8rZAf1uatMil_ARmRL0z4K4iX2HqUUpdctNGXMw,1584
@@ -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.3.dist-info/METADATA,sha256=lWTnBmXUOk40L3zgDxdlzSxd95bihx8hOdXKePMrHdA,3159
48
- maleo_foundation-0.0.3.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
49
- maleo_foundation-0.0.3.dist-info/top_level.txt,sha256=_iBos3F_bhEOdjOnzeiEYSrCucasc810xXtLBXI8cQc,17
50
- maleo_foundation-0.0.3.dist-info/RECORD,,
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,,