maleo-foundation 0.1.19__tar.gz → 0.1.20__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.
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/PKG-INFO +1 -1
- maleo_foundation-0.1.20/maleo_foundation/authentication.py +37 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/enums.py +4 -1
- maleo_foundation-0.1.20/maleo_foundation/managers/middleware.py +107 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/service.py +46 -23
- maleo_foundation-0.1.20/maleo_foundation/middlewares/authentication.py +62 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/middlewares/base.py +5 -4
- maleo_foundation-0.1.20/maleo_foundation/utils/extractor.py +18 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/logging.py +38 -1
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation.egg-info/PKG-INFO +1 -1
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation.egg-info/SOURCES.txt +4 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/pyproject.toml +1 -1
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/README.md +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/general/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/general/http.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/base.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/cloud/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/cloud/base.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/cloud/logging.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/cloud/secret.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/cloud/storage.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/secret.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/storage.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/utils/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/utils/logger.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/constants.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/db/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/db/engine.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/db/manager.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/db/session.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/db/table.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/client.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/general.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/query.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/service.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/token.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/extended_types.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/base.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/google/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/google/base.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/google/secret.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/google/storage.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/http.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/maleo.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/db.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/middlewares/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/middlewares/cors.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/responses.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/schemas/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/schemas/general.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/schemas/parameter.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/schemas/result.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/schemas/token.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/general/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/general/token.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/parameters/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/parameters/client.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/parameters/general.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/parameters/service.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/parameters/token.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/client/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/client/controllers/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/client/controllers/http.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/client/service.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/service/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/service/controllers/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/service/controllers/rest.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/service/general.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/service/query.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/results/token.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/services/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/services/token.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/types.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/controller.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/exceptions.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/formatter/__init__.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/formatter/case.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/keyloader.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/logger.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/query.py +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation.egg-info/dependency_links.txt +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation.egg-info/requires.txt +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation.egg-info/top_level.txt +0 -0
- {maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/setup.cfg +0 -0
@@ -0,0 +1,37 @@
|
|
1
|
+
from starlette.authentication import (
|
2
|
+
AuthCredentials,
|
3
|
+
BaseUser
|
4
|
+
)
|
5
|
+
from typing import Optional, Sequence
|
6
|
+
|
7
|
+
class Credentials(AuthCredentials):
|
8
|
+
def __init__(
|
9
|
+
self,
|
10
|
+
token:Optional[str] = None,
|
11
|
+
scopes:Optional[Sequence[str]] = None
|
12
|
+
) -> None:
|
13
|
+
self.token = token
|
14
|
+
super().__init__(scopes)
|
15
|
+
|
16
|
+
class User(BaseUser):
|
17
|
+
def __init__(
|
18
|
+
self,
|
19
|
+
authenticated:bool = True,
|
20
|
+
username:str = "",
|
21
|
+
email:str = ""
|
22
|
+
) -> None:
|
23
|
+
self._authenticated = authenticated
|
24
|
+
self._username = username
|
25
|
+
self._email = email
|
26
|
+
|
27
|
+
@property
|
28
|
+
def is_authenticated(self) -> bool:
|
29
|
+
return self._authenticated
|
30
|
+
|
31
|
+
@property
|
32
|
+
def display_name(self) -> str:
|
33
|
+
return self._username
|
34
|
+
|
35
|
+
@property
|
36
|
+
def identity(self) -> str:
|
37
|
+
return self._email
|
@@ -73,8 +73,11 @@ class BaseEnums:
|
|
73
73
|
BaseEnums.RESTControllerResponseType.FILE: responses.FileResponse,
|
74
74
|
}.get(self, responses.Response)
|
75
75
|
|
76
|
+
class MiddlewareLoggerType(StrEnum):
|
77
|
+
BASE = "base"
|
78
|
+
AUTHENTICATION = "authentication"
|
79
|
+
|
76
80
|
class ServiceLoggerType(StrEnum):
|
77
|
-
MIDDLEWARE = "middleware"
|
78
81
|
DATABASE = "database"
|
79
82
|
APPLICATION = "application"
|
80
83
|
|
@@ -0,0 +1,107 @@
|
|
1
|
+
from fastapi import FastAPI
|
2
|
+
from pydantic import BaseModel, Field
|
3
|
+
from typing import List, Optional, Sequence
|
4
|
+
from maleo_foundation.middlewares.authentication import Backend, add_authentication_middleware
|
5
|
+
from maleo_foundation.middlewares.base import add_base_middleware, RequestProcessor
|
6
|
+
from maleo_foundation.middlewares.cors import add_cors_middleware
|
7
|
+
from maleo_foundation.utils.logging import MiddlewareLogger
|
8
|
+
|
9
|
+
_ALLOW_METHODS:List[str] = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
10
|
+
_ALLOW_HEADERS:List[str] = ["X-Organization", "X-User", "X-Signature"]
|
11
|
+
_EXPOSE_HEADERS:List[str] = ["X-Request-Timestamp", "X-Response-Timestamp", "X-Process-Time", "X-Signature"]
|
12
|
+
|
13
|
+
class GeneralMiddlewareConfigurations(BaseModel):
|
14
|
+
allow_origins:List[str] = Field(default_factory=list, description="Allowed origins")
|
15
|
+
allow_methods:List[str] = Field(_ALLOW_METHODS, description="Allowed methods")
|
16
|
+
allow_headers:list[str] = Field(_ALLOW_HEADERS, description="Allowed headers")
|
17
|
+
allow_credentials:bool = Field(False, description="Allowed credentials")
|
18
|
+
|
19
|
+
class CORSMiddlewareConfigurations(BaseModel):
|
20
|
+
expose_headers:List[str] = Field(_EXPOSE_HEADERS, description="Exposed headers")
|
21
|
+
|
22
|
+
class BaseMiddlewareConfigurations(BaseModel):
|
23
|
+
limit:int = Field(10, description="Request limit (per 'window' seconds)")
|
24
|
+
window:int = Field(1, description="Request limit window (seconds)")
|
25
|
+
cleanup_interval:int = Field(60, description="Interval for middleware cleanup (seconds)")
|
26
|
+
ip_timeout:int = Field(300, description="Idle IP's timeout (seconds)")
|
27
|
+
|
28
|
+
class MiddlewareConfigurations(BaseModel):
|
29
|
+
general:GeneralMiddlewareConfigurations = Field(..., description="Middleware's general configurations")
|
30
|
+
cors:CORSMiddlewareConfigurations = Field(..., description="CORS middleware's configurations")
|
31
|
+
base:BaseMiddlewareConfigurations = Field(..., description="Base middleware's configurations")
|
32
|
+
|
33
|
+
class MiddlewareLoggers(BaseModel):
|
34
|
+
base:MiddlewareLogger = Field(..., description="Base middleware's logger")
|
35
|
+
authentication:MiddlewareLogger = Field(..., description="Authentication middleware's logger")
|
36
|
+
|
37
|
+
class Config:
|
38
|
+
arbitrary_types_allowed=True
|
39
|
+
|
40
|
+
class MiddlewareManager:
|
41
|
+
def __init__(self, app:FastAPI, configurations:MiddlewareConfigurations):
|
42
|
+
self._app = app
|
43
|
+
self._configurations = configurations
|
44
|
+
|
45
|
+
def add_all(
|
46
|
+
self,
|
47
|
+
loggers:MiddlewareLoggers,
|
48
|
+
key:str,
|
49
|
+
authentication_backend:Optional[Backend] = None,
|
50
|
+
request_processor:Optional[RequestProcessor] = None
|
51
|
+
):
|
52
|
+
self.add_cors(
|
53
|
+
allow_origins=self._configurations.general.allow_origins,
|
54
|
+
allow_methods=self._configurations.general.allow_methods,
|
55
|
+
allow_headers=self._configurations.general.allow_headers,
|
56
|
+
allow_credentials=self._configurations.general.allow_credentials,
|
57
|
+
expose_headers=self._configurations.cors.expose_headers
|
58
|
+
)
|
59
|
+
self.add_base(
|
60
|
+
logger=loggers.base,
|
61
|
+
allow_origins=self._configurations.general.allow_origins,
|
62
|
+
allow_methods=self._configurations.general.allow_methods,
|
63
|
+
allow_headers=self._configurations.general.allow_headers,
|
64
|
+
allow_credentials=self._configurations.general.allow_credentials,
|
65
|
+
limit=self._configurations.base.limit,
|
66
|
+
window=self._configurations.base.window,
|
67
|
+
cleanup_interval=self._configurations.base.cleanup_interval,
|
68
|
+
ip_timeout=self._configurations.base.ip_timeout,
|
69
|
+
request_processor=request_processor
|
70
|
+
)
|
71
|
+
if authentication_backend is None:
|
72
|
+
self._authentication_backend = Backend(logger=loggers.authentication, key=key)
|
73
|
+
else:
|
74
|
+
self._authentication_backend = authentication_backend
|
75
|
+
self.add_authentication(backend=self._authentication_backend)
|
76
|
+
|
77
|
+
def add_cors(self) -> None:
|
78
|
+
add_cors_middleware(
|
79
|
+
app=self._app,
|
80
|
+
allow_origins=self._configurations.general.allow_origins,
|
81
|
+
allow_methods=self._configurations.general.allow_methods,
|
82
|
+
allow_headers=self._configurations.general.allow_headers,
|
83
|
+
allow_credentials=self._configurations.general.allow_credentials,
|
84
|
+
expose_headers=self._configurations.cors.expose_headers
|
85
|
+
)
|
86
|
+
|
87
|
+
def add_base(self, logger:MiddlewareLogger, request_processor:Optional[RequestProcessor] = None):
|
88
|
+
add_base_middleware(
|
89
|
+
app=self._app,
|
90
|
+
logger=logger,
|
91
|
+
allow_origins=self._configurations.general.allow_origins,
|
92
|
+
allow_methods=self._configurations.general.allow_methods,
|
93
|
+
allow_headers=self._configurations.general.allow_headers,
|
94
|
+
allow_credentials=self._configurations.general.allow_credentials,
|
95
|
+
limit=self._configurations.base.limit,
|
96
|
+
window=self._configurations.base.window,
|
97
|
+
cleanup_interval=self._configurations.base.cleanup_interval,
|
98
|
+
ip_timeout=self._configurations.base.ip_timeout,
|
99
|
+
request_processor=request_processor
|
100
|
+
)
|
101
|
+
|
102
|
+
def add_authentication(self, logger:MiddlewareLogger, key:str, backend:Optional[Backend] = None):
|
103
|
+
if backend is None:
|
104
|
+
self._authentication_backend = Backend(logger=logger, key=key)
|
105
|
+
else:
|
106
|
+
self._authentication_backend = backend
|
107
|
+
add_authentication_middleware(app=self._app, backend=self._authentication_backend)
|
@@ -1,10 +1,14 @@
|
|
1
1
|
import json
|
2
2
|
import os
|
3
|
+
import uvicorn
|
4
|
+
from fastapi import FastAPI, APIRouter
|
5
|
+
from fastapi.exceptions import RequestValidationError
|
6
|
+
from starlette.exceptions import HTTPException
|
7
|
+
from starlette.types import Lifespan, AppType
|
3
8
|
from pydantic_settings import BaseSettings
|
4
9
|
from pydantic import BaseModel, Field
|
5
10
|
from sqlalchemy import MetaData
|
6
|
-
from typing import
|
7
|
-
from maleo_foundation.db.manager import DatabaseManagerV2
|
11
|
+
from typing import Optional, Type
|
8
12
|
from maleo_foundation.enums import BaseEnums
|
9
13
|
from maleo_foundation.models.transfers.general.token import BaseTokenGeneralTransfers
|
10
14
|
from maleo_foundation.models.transfers.parameters.token import BaseTokenParametersTransfers
|
@@ -13,10 +17,13 @@ from maleo_foundation.managers.client.google.storage import GoogleCloudStorage
|
|
13
17
|
from maleo_foundation.managers.client.http import HTTPClientManager
|
14
18
|
from maleo_foundation.managers.client.maleo import MaleoClientManager
|
15
19
|
from maleo_foundation.managers.db import DatabaseManager
|
20
|
+
from maleo_foundation.managers.middleware import MiddlewareConfigurations, MiddlewareLoggers, MiddlewareManager
|
21
|
+
from maleo_foundation.middlewares.base import RequestProcessor
|
16
22
|
from maleo_foundation.services.token import BaseTokenService
|
17
23
|
from maleo_foundation.types import BaseTypes
|
24
|
+
from maleo_foundation.utils.exceptions import BaseExceptions
|
18
25
|
from maleo_foundation.utils.keyloader import load_key
|
19
|
-
from maleo_foundation.utils.logging import GoogleCloudLogging, ServiceLogger,
|
26
|
+
from maleo_foundation.utils.logging import GoogleCloudLogging, ServiceLogger, MiddlewareLogger
|
20
27
|
|
21
28
|
class LogConfig(BaseModel):
|
22
29
|
logs_dir:str = Field(..., description="Logs directory")
|
@@ -68,10 +75,8 @@ class Credentials(BaseModel):
|
|
68
75
|
class ServiceConfigurations(BaseModel):
|
69
76
|
key:str = Field(..., description="Service's key")
|
70
77
|
name:str = Field(..., description="Service's name")
|
71
|
-
|
72
|
-
|
73
|
-
allowed_origins:List[str] = Field(default_factory=list, description="Allowed origins")
|
74
|
-
service_ips:List[str] = Field(default_factory=list, description="Other service's IPs")
|
78
|
+
host:str = Field(..., description="Service's host")
|
79
|
+
port:int = Field(..., description="Service's port")
|
75
80
|
|
76
81
|
class DatabaseConfigurations(BaseModel):
|
77
82
|
username:str = Field("postgres", description="Database user's username")
|
@@ -113,18 +118,15 @@ class Configurations(BaseModel):
|
|
113
118
|
service:ServiceConfigurations = Field(..., description="Service's configurations")
|
114
119
|
middleware:MiddlewareConfigurations = Field(..., description="Middleware's configurations")
|
115
120
|
database:DatabaseConfigurations = Field(..., description="Database's configurations")
|
116
|
-
client:ClientConfigurations = Field(
|
121
|
+
client:ClientConfigurations = Field(..., description="Service's configurations")
|
117
122
|
|
118
123
|
class Config:
|
119
124
|
arbitrary_types_allowed=True
|
120
125
|
|
121
|
-
ClientLoggerManagers = Dict[str, ClientLoggerManager]
|
122
|
-
|
123
126
|
class Loggers(BaseModel):
|
124
127
|
application:ServiceLogger = Field(..., description="Application logger")
|
125
128
|
database:ServiceLogger = Field(..., description="Database logger")
|
126
|
-
middleware:
|
127
|
-
client:ClientLoggerManagers = Field(default_factory=dict, description="Client logger manager")
|
129
|
+
middleware:MiddlewareLoggers = Field(..., description="Middleware logger")
|
128
130
|
|
129
131
|
class Config:
|
130
132
|
arbitrary_types_allowed=True
|
@@ -185,7 +187,6 @@ class ServiceManager:
|
|
185
187
|
base_dir:BaseTypes.OptionalString = None,
|
186
188
|
settings:Optional[Settings] = None,
|
187
189
|
google_cloud_logging:Optional[GoogleCloudLogging] = None,
|
188
|
-
client_logger_managers:ClientLoggerManagers = {},
|
189
190
|
maleo_client_manager_classes:Optional[MaleoClientManagerClasses] = None
|
190
191
|
):
|
191
192
|
self._db_metadata = db_metadata
|
@@ -217,7 +218,6 @@ class ServiceManager:
|
|
217
218
|
google_cloud_logging=self._google_cloud_logging
|
218
219
|
)
|
219
220
|
|
220
|
-
self._client_logger_managers = client_logger_managers
|
221
221
|
self._initialize_loggers()
|
222
222
|
self._load_credentials()
|
223
223
|
self._parse_keys()
|
@@ -256,14 +256,14 @@ class ServiceManager:
|
|
256
256
|
return self._configs
|
257
257
|
|
258
258
|
def _initialize_loggers(self) -> None:
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
self._loggers = Loggers(application=application, database=database, middleware=middleware
|
259
|
+
#* Service's loggers
|
260
|
+
application = ServiceLogger(type=BaseEnums.LoggerType.APPLICATION, service_key=self._configs.service.key, **self._log_config.model_dump())
|
261
|
+
database = ServiceLogger(type=BaseEnums.LoggerType.DATABASE, service_key=self._configs.service.key, **self._log_config.model_dump())
|
262
|
+
#* Middleware's loggers
|
263
|
+
base = MiddlewareLogger(middleware_type=BaseEnums.MiddlewareLoggerType.BASE, service_key=self._configs.service.key, **self._log_config.model_dump())
|
264
|
+
authentication = MiddlewareLogger(middleware_type=BaseEnums.MiddlewareLoggerType.AUTHENTICATION, service_key=self._configs.service.key, **self._log_config.model_dump())
|
265
|
+
middleware = MiddlewareLoggers(base=base, authentication=authentication)
|
266
|
+
self._loggers = Loggers(application=application, database=database, middleware=middleware)
|
267
267
|
|
268
268
|
@property
|
269
269
|
def loggers(self) -> Loggers:
|
@@ -363,6 +363,28 @@ class ServiceManager:
|
|
363
363
|
raise ValueError("Failed generating token")
|
364
364
|
return result.data.token
|
365
365
|
|
366
|
+
def create_app(self, router:APIRouter, lifespan:Optional[Lifespan[AppType]] = None, request_processor:Optional[RequestProcessor] = None) -> FastAPI:
|
367
|
+
self._app = FastAPI(title=self._configs.service.name, lifespan=lifespan)
|
368
|
+
|
369
|
+
self._middleware = MiddlewareManager(app=self._app, configurations=self._configs.middleware)
|
370
|
+
self._middleware.add_all(loggers=self.loggers.middleware, key=self._keys.public, request_processor=request_processor)
|
371
|
+
|
372
|
+
#* Add exception handler(s)
|
373
|
+
self._app.add_exception_handler(RequestValidationError, BaseExceptions.validation_exception_handler)
|
374
|
+
self._app.add_exception_handler(HTTPException, BaseExceptions.http_exception_handler)
|
375
|
+
|
376
|
+
#* Include router
|
377
|
+
self._app.include_router(router)
|
378
|
+
|
379
|
+
return self._app
|
380
|
+
|
381
|
+
@property
|
382
|
+
def app(self) -> FastAPI:
|
383
|
+
return self._app
|
384
|
+
|
385
|
+
def run_app(self) -> None:
|
386
|
+
uvicorn.run(self._app, host=self._configs.service.host, port=self._configs.service.port, reload=True)
|
387
|
+
|
366
388
|
async def dispose(self) -> None:
|
367
389
|
self._loggers.application.info("Disposing service manager")
|
368
390
|
if self._database is not None:
|
@@ -380,5 +402,6 @@ class ServiceManager:
|
|
380
402
|
if self._loggers is not None:
|
381
403
|
self._loggers.application.dispose()
|
382
404
|
self._loggers.database.dispose()
|
383
|
-
self._loggers.middleware.dispose()
|
405
|
+
self._loggers.middleware.base.dispose()
|
406
|
+
self._loggers.middleware.authentication.dispose()
|
384
407
|
self._loggers = None
|
@@ -0,0 +1,62 @@
|
|
1
|
+
from fastapi import FastAPI
|
2
|
+
from starlette.authentication import AuthenticationBackend
|
3
|
+
from starlette.middleware.authentication import AuthenticationMiddleware
|
4
|
+
from starlette.requests import HTTPConnection
|
5
|
+
from typing import Tuple
|
6
|
+
from maleo_foundation.authentication import Credentials, User
|
7
|
+
from maleo_foundation.models.transfers.parameters.token import BaseTokenParametersTransfers
|
8
|
+
from maleo_foundation.services.token import BaseTokenService
|
9
|
+
from maleo_foundation.utils.extractor import extract_client_ip
|
10
|
+
from maleo_foundation.utils.logging import MiddlewareLogger
|
11
|
+
|
12
|
+
class Backend(AuthenticationBackend):
|
13
|
+
def __init__(self, logger:MiddlewareLogger, key:str):
|
14
|
+
super().__init__()
|
15
|
+
self._logger = logger
|
16
|
+
self._key = key
|
17
|
+
|
18
|
+
async def authenticate(self, conn:HTTPConnection) -> Tuple[Credentials, User]:
|
19
|
+
client_ip = extract_client_ip(conn)
|
20
|
+
if "Authorization" not in conn.headers:
|
21
|
+
self._logger.info(f"Authentication - Request | IP: {client_ip} | URL: {conn.url.path} - Result | General: Header did not contain authorization")
|
22
|
+
return Credentials(), User(authenticated=False)
|
23
|
+
|
24
|
+
auth = conn.headers["Authorization"]
|
25
|
+
scheme, token = auth.split()
|
26
|
+
if scheme != 'Bearer':
|
27
|
+
self._logger.info(f"Authentication - Request | IP: {client_ip} | URL: {conn.url.path} - Result | General: Authorization scheme is not Bearer")
|
28
|
+
return Credentials(), User(authenticated=False)
|
29
|
+
|
30
|
+
decode_token_parameters = BaseTokenParametersTransfers.Decode(key=self._key, token=token)
|
31
|
+
decode_token_result = BaseTokenService.decode(parameters=decode_token_parameters)
|
32
|
+
if not decode_token_result.success:
|
33
|
+
self._logger.error(f"Authentication - Request | IP: {client_ip} | URL: {conn.url.path} - Result | General: Failed decoding authorization token")
|
34
|
+
return Credentials(token=token), User(authenticated=False)
|
35
|
+
|
36
|
+
self._logger.info(f"Authentication - Request | IP: {client_ip} | URL: {conn.url.path} - Result | Username: {decode_token_result.data.u_u} | Email: {decode_token_result.data.u_e}")
|
37
|
+
return Credentials(token=token), User(authenticated=True, username=decode_token_result.data.u_u, email=decode_token_result.data.u_e)
|
38
|
+
|
39
|
+
def add_authentication_middleware(app:FastAPI, backend:Backend) -> None:
|
40
|
+
"""
|
41
|
+
Adds Authentication middleware to the FastAPI application.
|
42
|
+
|
43
|
+
Args:
|
44
|
+
app: FastAPI
|
45
|
+
The FastAPI application instance to which the middleware will be added.
|
46
|
+
|
47
|
+
backend: Backend
|
48
|
+
The authentication middleware backend to be used.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
None: The function modifies the FastAPI app by adding Base middleware.
|
52
|
+
|
53
|
+
Note:
|
54
|
+
FastAPI applies middleware in reverse order of registration, so this middleware
|
55
|
+
will execute after any middleware added subsequently.
|
56
|
+
|
57
|
+
Example:
|
58
|
+
```python
|
59
|
+
add_authentication_middleware(app=app, limit=10, window=1, cleanup_interval=60, ip_timeout=300)
|
60
|
+
```
|
61
|
+
"""
|
62
|
+
app.add_middleware(AuthenticationMiddleware, backend=backend)
|
@@ -6,10 +6,11 @@ from collections import defaultdict
|
|
6
6
|
from datetime import datetime, timedelta, timezone
|
7
7
|
from fastapi import FastAPI, Request, Response, status
|
8
8
|
from fastapi.responses import JSONResponse
|
9
|
-
from logging import Logger
|
10
9
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
11
10
|
from typing import Awaitable, Callable, Optional, Sequence
|
12
11
|
from maleo_foundation.models.responses import BaseResponses
|
12
|
+
from maleo_foundation.utils.extractor import extract_client_ip
|
13
|
+
from maleo_foundation.utils.logging import MiddlewareLogger
|
13
14
|
|
14
15
|
RequestProcessor = Callable[[Request], Awaitable[Optional[Response]]]
|
15
16
|
ResponseProcessor = Callable[[Response], Awaitable[Response]]
|
@@ -18,7 +19,7 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
18
19
|
def __init__(
|
19
20
|
self,
|
20
21
|
app,
|
21
|
-
logger:
|
22
|
+
logger:MiddlewareLogger,
|
22
23
|
allow_origins:Sequence[str] = (),
|
23
24
|
allow_methods:Sequence[str] = ("GET",),
|
24
25
|
allow_headers:Sequence[str] = (),
|
@@ -178,7 +179,7 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
178
179
|
self._cleanup_old_data() #* Run periodic cleanup
|
179
180
|
request_timestamp = datetime.now(tz=timezone.utc) #* Record the request timestamp
|
180
181
|
start_time = time.perf_counter() #* Record the start time
|
181
|
-
client_ip =
|
182
|
+
client_ip = extract_client_ip(request) #* Get request IP with improved extraction
|
182
183
|
|
183
184
|
try:
|
184
185
|
#* 1. Rate limit check
|
@@ -225,7 +226,7 @@ class BaseMiddleware(BaseHTTPMiddleware):
|
|
225
226
|
|
226
227
|
def add_base_middleware(
|
227
228
|
app:FastAPI,
|
228
|
-
logger:
|
229
|
+
logger:MiddlewareLogger,
|
229
230
|
allow_origins:Sequence[str] = (),
|
230
231
|
allow_methods:Sequence[str] = ("GET",),
|
231
232
|
allow_headers:Sequence[str] = (),
|
@@ -0,0 +1,18 @@
|
|
1
|
+
from starlette.requests import HTTPConnection
|
2
|
+
|
3
|
+
def extract_client_ip(conn:HTTPConnection) -> str:
|
4
|
+
"""Extract client IP with more robust handling of proxies"""
|
5
|
+
#* Check for X-Forwarded-For header (common when behind proxy/load balancer)
|
6
|
+
x_forwarded_for = conn.headers.get("X-Forwarded-For")
|
7
|
+
if x_forwarded_for:
|
8
|
+
#* The client's IP is the first one in the list
|
9
|
+
ips = [ip.strip() for ip in x_forwarded_for.split(",")]
|
10
|
+
return ips[0]
|
11
|
+
|
12
|
+
#* Check for X-Real-IP header (used by some proxies)
|
13
|
+
x_real_ip = conn.headers.get("X-Real-IP")
|
14
|
+
if x_real_ip:
|
15
|
+
return x_real_ip
|
16
|
+
|
17
|
+
#* Fall back to direct client connection
|
18
|
+
return conn.client.host if conn.client else "unknown"
|
@@ -45,6 +45,7 @@ class BaseLogger(logging.Logger):
|
|
45
45
|
logs_dir:str,
|
46
46
|
type:BaseEnums.LoggerType,
|
47
47
|
service_key:BaseTypes.OptionalString = None,
|
48
|
+
middleware_type:Optional[BaseEnums.MiddlewareLoggerType] = None,
|
48
49
|
client_key:BaseTypes.OptionalString = None,
|
49
50
|
level:BaseEnums.LoggerLevel = BaseEnums.LoggerLevel.INFO,
|
50
51
|
google_cloud_logging:Optional[GoogleCloudLogging] = None
|
@@ -67,6 +68,11 @@ class BaseLogger(logging.Logger):
|
|
67
68
|
if self._service_key is None:
|
68
69
|
raise ValueError("SERVICE_KEY environment variable must be set if 'service_key' is set to None")
|
69
70
|
|
71
|
+
self._middleware_type = middleware_type #* Declare middleware type
|
72
|
+
|
73
|
+
if self._type == BaseEnums.LoggerType.MIDDLEWARE and self._middleware_type is None:
|
74
|
+
raise ValueError("'middleware_type' parameter must be provided if 'logger_type' is 'middleware'")
|
75
|
+
|
70
76
|
self._client_key = client_key #* Declare client key
|
71
77
|
|
72
78
|
#* Ensure client_key is valid if logger type is a client
|
@@ -75,7 +81,15 @@ class BaseLogger(logging.Logger):
|
|
75
81
|
|
76
82
|
#* Define logger name
|
77
83
|
if self._type == BaseEnums.LoggerType.CLIENT:
|
84
|
+
#* Ensure client_key is valid if logger type is client
|
85
|
+
if self._client_key is None:
|
86
|
+
raise ValueError("'client_key' parameter must be provided if 'logger_type' is 'client'")
|
78
87
|
self._name = f"{self._service_key} - {self._type} - {self._client_key}"
|
88
|
+
elif self._type == BaseEnums.LoggerType.MIDDLEWARE:
|
89
|
+
#* Ensure middleware_type is valid if logger type is middleware
|
90
|
+
if self._middleware_type is None:
|
91
|
+
raise ValueError("'middleware_type' parameter must be provided if 'logger_type' is 'middleware'")
|
92
|
+
self._name = f"{self._service_key} - {self._type} - {self._middleware_type}"
|
79
93
|
else:
|
80
94
|
self._name = f"{self._service_key} - {self._type}"
|
81
95
|
|
@@ -102,7 +116,9 @@ class BaseLogger(logging.Logger):
|
|
102
116
|
self.info("Cloud logging is not configured.")
|
103
117
|
|
104
118
|
#* Define log directory
|
105
|
-
if self._type == BaseEnums.LoggerType.
|
119
|
+
if self._type == BaseEnums.LoggerType.MIDDLEWARE:
|
120
|
+
log_dir = f"{self._type}/{self._middleware_type}"
|
121
|
+
elif self._type == BaseEnums.LoggerType.CLIENT:
|
106
122
|
log_dir = f"{self._type}/{self._client_key}"
|
107
123
|
else:
|
108
124
|
log_dir = f"{self._type}"
|
@@ -145,6 +161,25 @@ class BaseLogger(logging.Logger):
|
|
145
161
|
handler.close()
|
146
162
|
self.handlers.clear()
|
147
163
|
|
164
|
+
class MiddlewareLogger(BaseLogger):
|
165
|
+
def __init__(
|
166
|
+
self,
|
167
|
+
logs_dir:str,
|
168
|
+
service_key:BaseTypes.OptionalString = None,
|
169
|
+
middleware_type = None,
|
170
|
+
level = BaseEnums.LoggerLevel.INFO,
|
171
|
+
google_cloud_logging = None
|
172
|
+
):
|
173
|
+
super().__init__(
|
174
|
+
logs_dir=logs_dir,
|
175
|
+
type=BaseEnums.LoggerType.MIDDLEWARE,
|
176
|
+
service_key=service_key,
|
177
|
+
middleware_type=middleware_type,
|
178
|
+
client_key=None,
|
179
|
+
level=level,
|
180
|
+
google_cloud_logging=google_cloud_logging
|
181
|
+
)
|
182
|
+
|
148
183
|
class ServiceLogger(BaseLogger):
|
149
184
|
def __init__(
|
150
185
|
self,
|
@@ -158,6 +193,7 @@ class ServiceLogger(BaseLogger):
|
|
158
193
|
logs_dir=logs_dir,
|
159
194
|
type=type,
|
160
195
|
service_key=service_key,
|
196
|
+
middleware_type=None,
|
161
197
|
client_key=None,
|
162
198
|
level=level,
|
163
199
|
google_cloud_logging=google_cloud_logging
|
@@ -176,6 +212,7 @@ class ClientLogger(BaseLogger):
|
|
176
212
|
logs_dir=logs_dir,
|
177
213
|
type=BaseEnums.LoggerType.CLIENT,
|
178
214
|
service_key=service_key,
|
215
|
+
middleware_type=None,
|
179
216
|
client_key=client_key,
|
180
217
|
level=level,
|
181
218
|
google_cloud_logging=google_cloud_logging
|
@@ -1,6 +1,7 @@
|
|
1
1
|
README.md
|
2
2
|
pyproject.toml
|
3
3
|
maleo_foundation/__init__.py
|
4
|
+
maleo_foundation/authentication.py
|
4
5
|
maleo_foundation/constants.py
|
5
6
|
maleo_foundation/enums.py
|
6
7
|
maleo_foundation/extended_types.py
|
@@ -37,6 +38,7 @@ maleo_foundation/expanded_types/service.py
|
|
37
38
|
maleo_foundation/expanded_types/token.py
|
38
39
|
maleo_foundation/managers/__init__.py
|
39
40
|
maleo_foundation/managers/db.py
|
41
|
+
maleo_foundation/managers/middleware.py
|
40
42
|
maleo_foundation/managers/service.py
|
41
43
|
maleo_foundation/managers/client/__init__.py
|
42
44
|
maleo_foundation/managers/client/base.py
|
@@ -47,6 +49,7 @@ maleo_foundation/managers/client/google/base.py
|
|
47
49
|
maleo_foundation/managers/client/google/secret.py
|
48
50
|
maleo_foundation/managers/client/google/storage.py
|
49
51
|
maleo_foundation/middlewares/__init__.py
|
52
|
+
maleo_foundation/middlewares/authentication.py
|
50
53
|
maleo_foundation/middlewares/base.py
|
51
54
|
maleo_foundation/middlewares/cors.py
|
52
55
|
maleo_foundation/models/__init__.py
|
@@ -80,6 +83,7 @@ maleo_foundation/services/token.py
|
|
80
83
|
maleo_foundation/utils/__init__.py
|
81
84
|
maleo_foundation/utils/controller.py
|
82
85
|
maleo_foundation/utils/exceptions.py
|
86
|
+
maleo_foundation/utils/extractor.py
|
83
87
|
maleo_foundation/utils/keyloader.py
|
84
88
|
maleo_foundation/utils/logger.py
|
85
89
|
maleo_foundation/utils/logging.py
|
File without changes
|
File without changes
|
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/general/__init__.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/general/http.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/cloud/base.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/cloud/logging.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/cloud/secret.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/cloud/storage.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/secret.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/google/storage.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/utils/__init__.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/clients/utils/logger.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/__init__.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/client.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/general.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/query.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/service.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/expanded_types/token.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/__init__.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/base.py
RENAMED
File without changes
|
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/google/base.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/http.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/managers/client/maleo.py
RENAMED
File without changes
|
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/middlewares/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/schemas/__init__.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/schemas/general.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/schemas/parameter.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/schemas/result.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/schemas/token.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/models/transfers/__init__.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/formatter/__init__.py
RENAMED
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation/utils/formatter/case.py
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|
{maleo_foundation-0.1.19 → maleo_foundation-0.1.20}/maleo_foundation.egg-info/dependency_links.txt
RENAMED
File without changes
|
File without changes
|
File without changes
|
File without changes
|