dtpyfw 0.0.1__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.
- dtpyfw-0.0.1/PKG-INFO +49 -0
- dtpyfw-0.0.1/README.md +11 -0
- dtpyfw-0.0.1/dtpyfw/api/__init__.py +3 -0
- dtpyfw-0.0.1/dtpyfw/api/applications/__init__.py +0 -0
- dtpyfw-0.0.1/dtpyfw/api/applications/main.py +105 -0
- dtpyfw-0.0.1/dtpyfw/api/applications/sub.py +87 -0
- dtpyfw-0.0.1/dtpyfw/api/middlewares/__init__.py +0 -0
- dtpyfw-0.0.1/dtpyfw/api/middlewares/http_exception.py +13 -0
- dtpyfw-0.0.1/dtpyfw/api/middlewares/runtime.py +91 -0
- dtpyfw-0.0.1/dtpyfw/api/middlewares/validation_exception.py +23 -0
- dtpyfw-0.0.1/dtpyfw/api/routes/__init__.py +0 -0
- dtpyfw-0.0.1/dtpyfw/api/routes/authentication.py +65 -0
- dtpyfw-0.0.1/dtpyfw/api/routes/response.py +58 -0
- dtpyfw-0.0.1/dtpyfw/api/routes/route.py +127 -0
- dtpyfw-0.0.1/dtpyfw/api/routes/router.py +82 -0
- dtpyfw-0.0.1/dtpyfw/api/schemas/__init__.py +0 -0
- dtpyfw-0.0.1/dtpyfw/api/schemas/models.py +53 -0
- dtpyfw-0.0.1/dtpyfw/bucket/__init__.py +3 -0
- dtpyfw-0.0.1/dtpyfw/bucket/bucket.py +171 -0
- dtpyfw-0.0.1/dtpyfw/core/__init__.py +3 -0
- dtpyfw-0.0.1/dtpyfw/core/async.py +6 -0
- dtpyfw-0.0.1/dtpyfw/core/chunking.py +9 -0
- dtpyfw-0.0.1/dtpyfw/core/enums.py +6 -0
- dtpyfw-0.0.1/dtpyfw/core/env.py +44 -0
- dtpyfw-0.0.1/dtpyfw/core/exception.py +38 -0
- dtpyfw-0.0.1/dtpyfw/core/file_folder.py +15 -0
- dtpyfw-0.0.1/dtpyfw/core/hashing.py +53 -0
- dtpyfw-0.0.1/dtpyfw/core/jsonable_encoder.py +6 -0
- dtpyfw-0.0.1/dtpyfw/core/request.py +128 -0
- dtpyfw-0.0.1/dtpyfw/core/require_extra.py +10 -0
- dtpyfw-0.0.1/dtpyfw/core/retry.py +116 -0
- dtpyfw-0.0.1/dtpyfw/core/safe_access.py +16 -0
- dtpyfw-0.0.1/dtpyfw/core/singleton.py +22 -0
- dtpyfw-0.0.1/dtpyfw/core/slug.py +21 -0
- dtpyfw-0.0.1/dtpyfw/core/url.py +17 -0
- dtpyfw-0.0.1/dtpyfw/core/validation.py +44 -0
- dtpyfw-0.0.1/dtpyfw/db/__init__.py +3 -0
- dtpyfw-0.0.1/dtpyfw/db/config.py +53 -0
- dtpyfw-0.0.1/dtpyfw/db/database.py +271 -0
- dtpyfw-0.0.1/dtpyfw/db/health.py +17 -0
- dtpyfw-0.0.1/dtpyfw/db/model.py +173 -0
- dtpyfw-0.0.1/dtpyfw/db/search.py +429 -0
- dtpyfw-0.0.1/dtpyfw/db/utils.py +108 -0
- dtpyfw-0.0.1/dtpyfw/ftp/__init__.py +3 -0
- dtpyfw-0.0.1/dtpyfw/ftp/client.py +202 -0
- dtpyfw-0.0.1/dtpyfw/log/__init__.py +3 -0
- dtpyfw-0.0.1/dtpyfw/log/api_handler.py +65 -0
- dtpyfw-0.0.1/dtpyfw/log/config.py +38 -0
- dtpyfw-0.0.1/dtpyfw/log/footprint.py +27 -0
- dtpyfw-0.0.1/dtpyfw/log/formatter.py +13 -0
- dtpyfw-0.0.1/dtpyfw/log/handlers.py +47 -0
- dtpyfw-0.0.1/dtpyfw/log/initializer.py +35 -0
- dtpyfw-0.0.1/dtpyfw/redis/__init__.py +3 -0
- dtpyfw-0.0.1/dtpyfw/redis/caching.py +196 -0
- dtpyfw-0.0.1/dtpyfw/redis/config.py +37 -0
- dtpyfw-0.0.1/dtpyfw/redis/connection.py +62 -0
- dtpyfw-0.0.1/dtpyfw/redis/health.py +8 -0
- dtpyfw-0.0.1/dtpyfw/security/__init__.py +3 -0
- dtpyfw-0.0.1/dtpyfw/security/encryption.py +53 -0
- dtpyfw-0.0.1/dtpyfw/security/hashing.py +14 -0
- dtpyfw-0.0.1/dtpyfw/streamer/__init__.py +3 -0
- dtpyfw-0.0.1/dtpyfw/streamer/consumer.py +244 -0
- dtpyfw-0.0.1/dtpyfw/streamer/sender.py +32 -0
- dtpyfw-0.0.1/dtpyfw/worker/__init__.py +3 -0
- dtpyfw-0.0.1/dtpyfw/worker/task.py +39 -0
- dtpyfw-0.0.1/dtpyfw/worker/worker.py +128 -0
- dtpyfw-0.0.1/pyproject.toml +57 -0
dtpyfw-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: dtpyfw
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: DealerTower Python Framework: reusable building‑blocks for DealerTower services
|
|
5
|
+
Author: Reza Shirazi
|
|
6
|
+
Author-email: reza@dealertower.com
|
|
7
|
+
Requires-Python: >=3.11.9,<3.12.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Provides-Extra: all
|
|
10
|
+
Provides-Extra: api
|
|
11
|
+
Provides-Extra: bucket
|
|
12
|
+
Provides-Extra: db
|
|
13
|
+
Provides-Extra: ftp
|
|
14
|
+
Provides-Extra: normal
|
|
15
|
+
Provides-Extra: redis
|
|
16
|
+
Provides-Extra: security
|
|
17
|
+
Provides-Extra: streamer
|
|
18
|
+
Provides-Extra: worker
|
|
19
|
+
Requires-Dist: SQLAlchemy (>=2.0.39,<2.1.0) ; extra == "all" or extra == "normal" or extra == "db"
|
|
20
|
+
Requires-Dist: asyncpg (>=0.30.0,<0.31.0) ; extra == "all" or extra == "normal" or extra == "db"
|
|
21
|
+
Requires-Dist: boto3 (>=1.37.19,<1.38.0) ; extra == "all" or extra == "bucket"
|
|
22
|
+
Requires-Dist: botocore (>=1.37.19,<1.38.0) ; extra == "all" or extra == "bucket"
|
|
23
|
+
Requires-Dist: celery (>=5.4.0,<5.5.0) ; extra == "all" or extra == "normal" or extra == "worker"
|
|
24
|
+
Requires-Dist: celery-redbeat (>=2.3.2,<2.4.0) ; extra == "all" or extra == "normal" or extra == "worker"
|
|
25
|
+
Requires-Dist: fastapi (>=0.115.12,<0.116.0) ; extra == "all" or extra == "normal" or extra == "api"
|
|
26
|
+
Requires-Dist: gunicorn (>=23.0.0,<23.1.0) ; extra == "all" or extra == "normal" or extra == "api"
|
|
27
|
+
Requires-Dist: paramiko (>=3.5.1,<3.6.0) ; extra == "all" or extra == "ftp"
|
|
28
|
+
Requires-Dist: passlib[bcrypt] (>=1.7.4,<1.8.0) ; extra == "all" or extra == "security"
|
|
29
|
+
Requires-Dist: psycopg2 (>=2.9.10,<2.10.0) ; extra == "all" or extra == "normal" or extra == "db"
|
|
30
|
+
Requires-Dist: python-dateutil (>=2.9.0,<2.10.0) ; extra == "all" or extra == "ftp"
|
|
31
|
+
Requires-Dist: python-jose (>=3.4.0,<3.5.0) ; extra == "all" or extra == "security"
|
|
32
|
+
Requires-Dist: python-multipart (>=0.0.18,<0.1.0) ; extra == "all" or extra == "normal" or extra == "api"
|
|
33
|
+
Requires-Dist: redis (>=5.2.1,<5.3.0) ; extra == "all" or extra == "normal" or extra == "redis" or extra == "streamer" or extra == "worker"
|
|
34
|
+
Requires-Dist: requests (>=2.32.3,<2.33.0)
|
|
35
|
+
Requires-Dist: uvicorn (>=0.34.0,<0.35.0) ; extra == "all" or extra == "normal" or extra == "api"
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# Dealer Tower Python Framework
|
|
39
|
+
|
|
40
|
+
DealerTower Framework: reusable building‑blocks for DealerTower services
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
You can install the package via pip:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install dtpyfw
|
|
48
|
+
```
|
|
49
|
+
|
dtpyfw-0.0.1/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from fastapi.responses import RedirectResponse
|
|
3
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
4
|
+
|
|
5
|
+
from .sub import SubApp
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class MainApp:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
application_title: str,
|
|
12
|
+
applications: list[tuple[str, SubApp]],
|
|
13
|
+
startups: list[callable] = None,
|
|
14
|
+
shutdowns: list[callable] = None,
|
|
15
|
+
version: str = "*",
|
|
16
|
+
redirection_path: str = None,
|
|
17
|
+
redoc_url: str | None = None,
|
|
18
|
+
docs_url: str | None = None,
|
|
19
|
+
allow_credentials: bool = False,
|
|
20
|
+
allow_methods: list[str] | None = None,
|
|
21
|
+
allow_headers: list[str] | None = None,
|
|
22
|
+
allow_origins: list[str] | None = None,
|
|
23
|
+
allow_origin_regex: str | None = None,
|
|
24
|
+
expose_headers: list[str] | None = None,
|
|
25
|
+
access_control_max_age: int = 600,
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Initialize a MainApp instance and create the FastAPI application.
|
|
29
|
+
"""
|
|
30
|
+
self.application_title = application_title
|
|
31
|
+
self.applications = applications
|
|
32
|
+
self.version = version
|
|
33
|
+
self.redirection_path = redirection_path
|
|
34
|
+
self.redoc_url = redoc_url
|
|
35
|
+
self.docs_url = docs_url
|
|
36
|
+
self.allow_credentials = allow_credentials
|
|
37
|
+
self.startups = startups or []
|
|
38
|
+
self.shutdowns = shutdowns or []
|
|
39
|
+
self.allow_methods = allow_methods or ["*"]
|
|
40
|
+
self.allow_headers = allow_headers or ["*"]
|
|
41
|
+
self.allow_origins = allow_origins or ["*"]
|
|
42
|
+
self.allow_origin_regex = allow_origin_regex
|
|
43
|
+
self.expose_headers = expose_headers or []
|
|
44
|
+
self.access_control_max_age = access_control_max_age
|
|
45
|
+
self.app = self._create_app()
|
|
46
|
+
|
|
47
|
+
def _create_app(self) -> FastAPI:
|
|
48
|
+
"""
|
|
49
|
+
Create and configure the FastAPI application.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
# Default redirection path
|
|
53
|
+
if self.redirection_path is None and self.applications:
|
|
54
|
+
self.redirection_path = self.applications[0][0]
|
|
55
|
+
|
|
56
|
+
# Initialize FastAPI app
|
|
57
|
+
app = FastAPI(
|
|
58
|
+
title=self.application_title,
|
|
59
|
+
version=self.version,
|
|
60
|
+
redoc_url=self.redoc_url,
|
|
61
|
+
docs_url=self.docs_url,
|
|
62
|
+
swagger_ui_parameters={"defaultModelsExpandDepth": -1},
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Startup event handlers
|
|
66
|
+
@app.on_event("startup")
|
|
67
|
+
async def startup():
|
|
68
|
+
for startup_application in self.startups:
|
|
69
|
+
startup_application()
|
|
70
|
+
|
|
71
|
+
# Shutdown event handlers
|
|
72
|
+
@app.on_event("shutdown")
|
|
73
|
+
async def shutdown_event():
|
|
74
|
+
for shutdown_application in self.shutdowns:
|
|
75
|
+
shutdown_application()
|
|
76
|
+
|
|
77
|
+
# Add CORS middleware
|
|
78
|
+
app.add_middleware(
|
|
79
|
+
CORSMiddleware,
|
|
80
|
+
allow_credentials=self.allow_credentials,
|
|
81
|
+
allow_methods=self.allow_methods,
|
|
82
|
+
allow_headers=self.allow_headers,
|
|
83
|
+
allow_origins=self.allow_origins,
|
|
84
|
+
allow_origin_regex=self.allow_origin_regex,
|
|
85
|
+
expose_headers=self.expose_headers,
|
|
86
|
+
max_age=self.access_control_max_age,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Mount sub-applications
|
|
90
|
+
for path, application in self.applications:
|
|
91
|
+
app.mount(path, application.get_app())
|
|
92
|
+
|
|
93
|
+
# Redirect root to the specified path
|
|
94
|
+
if self.redirection_path:
|
|
95
|
+
@app.get("/", status_code=302)
|
|
96
|
+
async def redirect():
|
|
97
|
+
return RedirectResponse(self.redirection_path)
|
|
98
|
+
|
|
99
|
+
return app
|
|
100
|
+
|
|
101
|
+
def get_app(self) -> FastAPI:
|
|
102
|
+
"""
|
|
103
|
+
Get the created FastAPI application instance.
|
|
104
|
+
"""
|
|
105
|
+
return self.app
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from fastapi import FastAPI
|
|
2
|
+
from fastapi.exceptions import RequestValidationError
|
|
3
|
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
|
4
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
5
|
+
|
|
6
|
+
from ..middlewares import runtime, http_exception, validation_exception
|
|
7
|
+
from ..routes.router import Router
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SubApp:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
application_title: str,
|
|
14
|
+
version: str = "*",
|
|
15
|
+
redoc_url: str = "/",
|
|
16
|
+
docs_url: str = "/swagger",
|
|
17
|
+
middlewares: list = None,
|
|
18
|
+
routers: list[Router] = None,
|
|
19
|
+
gzip_compression: int = None,
|
|
20
|
+
session_middleware_settings: dict = None,
|
|
21
|
+
):
|
|
22
|
+
"""
|
|
23
|
+
Initialize a SubApp instance and create the FastAPI application.
|
|
24
|
+
"""
|
|
25
|
+
self.application_title = application_title
|
|
26
|
+
self.version = version
|
|
27
|
+
self.redoc_url = redoc_url
|
|
28
|
+
self.docs_url = docs_url
|
|
29
|
+
self.middlewares = middlewares or []
|
|
30
|
+
self.routers = routers or []
|
|
31
|
+
self.gzip_compression = gzip_compression
|
|
32
|
+
self.session_middleware_settings = session_middleware_settings or {}
|
|
33
|
+
self.app = self._create_app()
|
|
34
|
+
|
|
35
|
+
def _create_app(self) -> FastAPI:
|
|
36
|
+
"""
|
|
37
|
+
Create and configure a FastAPI application.
|
|
38
|
+
"""
|
|
39
|
+
app = FastAPI(
|
|
40
|
+
title=self.application_title,
|
|
41
|
+
version=self.version,
|
|
42
|
+
redoc_url=self.redoc_url,
|
|
43
|
+
docs_url=self.docs_url,
|
|
44
|
+
swagger_ui_parameters={"defaultModelsExpandDepth": -1},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Add GZip middleware
|
|
48
|
+
if self.gzip_compression is not None:
|
|
49
|
+
from fastapi.middleware.gzip import GZipMiddleware
|
|
50
|
+
app.add_middleware(GZipMiddleware, minimum_size=self.gzip_compression)
|
|
51
|
+
|
|
52
|
+
# Add custom middlewares
|
|
53
|
+
applicable_middlewares = [runtime.Runtime()] + self.middlewares
|
|
54
|
+
for middleware in applicable_middlewares:
|
|
55
|
+
app.add_middleware(BaseHTTPMiddleware, dispatch=middleware)
|
|
56
|
+
|
|
57
|
+
# Add session middleware if settings provided
|
|
58
|
+
if self.session_middleware_settings:
|
|
59
|
+
from starlette.middleware.sessions import SessionMiddleware
|
|
60
|
+
app.add_middleware(
|
|
61
|
+
SessionMiddleware,
|
|
62
|
+
secret_key=self.session_middleware_settings.get("secret_key"),
|
|
63
|
+
session_cookie=self.session_middleware_settings.get("session_cookie"),
|
|
64
|
+
https_only=self.session_middleware_settings.get("https_only"),
|
|
65
|
+
same_site=self.session_middleware_settings.get("same_site"),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Exception handlers
|
|
69
|
+
@app.exception_handler(StarletteHTTPException)
|
|
70
|
+
async def http_exception_handler(request, exc):
|
|
71
|
+
return await http_exception.http_exception_handler(request, exc)
|
|
72
|
+
|
|
73
|
+
@app.exception_handler(RequestValidationError)
|
|
74
|
+
async def validation_exception_handler(request, exc):
|
|
75
|
+
return await validation_exception.validation_exception_handler(request, exc)
|
|
76
|
+
|
|
77
|
+
# Add routers
|
|
78
|
+
for router in self.routers:
|
|
79
|
+
app.include_router(router=router.get_router())
|
|
80
|
+
|
|
81
|
+
return app
|
|
82
|
+
|
|
83
|
+
def get_app(self) -> FastAPI:
|
|
84
|
+
"""
|
|
85
|
+
Get the created FastAPI application instance.
|
|
86
|
+
"""
|
|
87
|
+
return self.app
|
|
File without changes
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from fastapi.responses import JSONResponse
|
|
2
|
+
|
|
3
|
+
from ..routes.response import return_response
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
async def http_exception_handler(request, exc):
|
|
7
|
+
status_code = exc.status_code
|
|
8
|
+
detail = request.url.path if status_code == 404 else exc.detail
|
|
9
|
+
return return_response(
|
|
10
|
+
data=str(detail),
|
|
11
|
+
status_code=status_code,
|
|
12
|
+
response_class=JSONResponse,
|
|
13
|
+
)
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from fastapi import Request
|
|
3
|
+
from fastapi.encoders import jsonable_encoder
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
|
|
6
|
+
from ...core.exception import exception_to_dict, RequestException
|
|
7
|
+
|
|
8
|
+
from ..routes.response import return_response
|
|
9
|
+
from ...log import footprint
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Runtime:
|
|
13
|
+
|
|
14
|
+
def __init__(self):
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
async def get_request_body(request: Request) -> dict:
|
|
19
|
+
content_length = request.headers.get('content-length')
|
|
20
|
+
content_type = request.headers.get('content-type')
|
|
21
|
+
try:
|
|
22
|
+
if content_length and int(content_length) > (1 * 1024 * 1024):
|
|
23
|
+
return {}
|
|
24
|
+
body = await request.body()
|
|
25
|
+
return {
|
|
26
|
+
'content_length': content_length,
|
|
27
|
+
'content_type': content_type,
|
|
28
|
+
'json': jsonable_encoder(body.decode('utf-8')),
|
|
29
|
+
}
|
|
30
|
+
except Exception:
|
|
31
|
+
return {
|
|
32
|
+
'content_length': content_length,
|
|
33
|
+
'content_type': content_type,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
async def create_payload(request: Request, exception: Exception) -> dict:
|
|
38
|
+
body = await Runtime.get_request_body(request)
|
|
39
|
+
return jsonable_encoder({
|
|
40
|
+
'path': request.url.path,
|
|
41
|
+
'method': request.method,
|
|
42
|
+
'query_parameters': request.query_params,
|
|
43
|
+
'path_parameters': request.path_params,
|
|
44
|
+
'headers': request.headers,
|
|
45
|
+
'body': body,
|
|
46
|
+
**exception_to_dict(exception),
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
async def __call__(self, request: Request, call_next):
|
|
50
|
+
start_time = time.time()
|
|
51
|
+
try:
|
|
52
|
+
response = await call_next(request)
|
|
53
|
+
except RequestException as e:
|
|
54
|
+
payload = await self.create_payload(request, e)
|
|
55
|
+
if not e.skip_footprint:
|
|
56
|
+
footprint.leave(
|
|
57
|
+
log_type='warning',
|
|
58
|
+
message=e.message,
|
|
59
|
+
controller=e.controller,
|
|
60
|
+
subject='Request Error',
|
|
61
|
+
payload=payload,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return return_response(
|
|
65
|
+
data=str(e.message),
|
|
66
|
+
status_code=e.status_code,
|
|
67
|
+
response_class=JSONResponse,
|
|
68
|
+
)
|
|
69
|
+
except Exception as e:
|
|
70
|
+
payload = await self.create_payload(request, e)
|
|
71
|
+
try:
|
|
72
|
+
message = payload.get('args', ['Unrecognized Error'])[0]
|
|
73
|
+
except:
|
|
74
|
+
message = 'Unrecognized Error has happened.'
|
|
75
|
+
|
|
76
|
+
footprint.leave(
|
|
77
|
+
log_type='error',
|
|
78
|
+
message=message,
|
|
79
|
+
controller='runtime',
|
|
80
|
+
subject='Unrecognized Error',
|
|
81
|
+
payload=payload,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
return return_response(
|
|
85
|
+
data='An unexpected issue has occurred; our team has been notified and is working diligently to resolve it promptly.',
|
|
86
|
+
status_code=500,
|
|
87
|
+
response_class=JSONResponse,
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
response.headers["X-Process-Time"] = str(round((time.time() - start_time) * 1000))
|
|
91
|
+
return response
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from fastapi.responses import JSONResponse
|
|
3
|
+
|
|
4
|
+
from ..routes.response import return_response
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def validation_exception_handler(_, exc):
|
|
8
|
+
error = ''
|
|
9
|
+
for error in exc.errors():
|
|
10
|
+
location = " -> ".join([str(l) for l in error["loc"]])
|
|
11
|
+
try:
|
|
12
|
+
input_data = ', input: ' + json.dumps(error['input'], default=str)
|
|
13
|
+
except:
|
|
14
|
+
input_data = ''
|
|
15
|
+
|
|
16
|
+
error = f"Error [location: '{location}'; message: '{error['msg']}'{input_data}'."
|
|
17
|
+
break
|
|
18
|
+
|
|
19
|
+
return return_response(
|
|
20
|
+
data=error,
|
|
21
|
+
status_code=422,
|
|
22
|
+
response_class=JSONResponse,
|
|
23
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from fastapi import Depends, Request
|
|
4
|
+
from fastapi.security import APIKeyHeader, APIKeyQuery
|
|
5
|
+
|
|
6
|
+
from ...core.exception import RequestException
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AuthType(Enum):
|
|
10
|
+
HEADER = "header"
|
|
11
|
+
QUERY = "query"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class Auth:
|
|
16
|
+
auth_type: AuthType
|
|
17
|
+
header_key: str | None = None
|
|
18
|
+
real_value: str | None = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HeaderAuthChecker:
|
|
22
|
+
def __init__(self, key: str, real_value: str):
|
|
23
|
+
self.key = key
|
|
24
|
+
self.real_value = real_value
|
|
25
|
+
|
|
26
|
+
def __call__(self, request: Request):
|
|
27
|
+
auth_token = request.headers.get(self.key)
|
|
28
|
+
if auth_token is None or auth_token != self.real_value:
|
|
29
|
+
raise RequestException(
|
|
30
|
+
controller='dtpyfw.middlewares.authentication.header',
|
|
31
|
+
message='Wrong credential.',
|
|
32
|
+
status_code=403,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class QueryAuthChecker:
|
|
37
|
+
def __init__(self, key: str, real_value: str):
|
|
38
|
+
self.key = key
|
|
39
|
+
self.real_value = real_value
|
|
40
|
+
|
|
41
|
+
def __call__(self, request: Request):
|
|
42
|
+
auth_token = request.query_params.get(self.key)
|
|
43
|
+
if auth_token is None or auth_token != self.real_value:
|
|
44
|
+
raise RequestException(
|
|
45
|
+
controller='dtpyfw.middlewares.authentication.query',
|
|
46
|
+
message='Wrong credential.',
|
|
47
|
+
status_code=403,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def auth_data_class_to_dependency(authentication: Auth) -> list[Depends]:
|
|
52
|
+
if authentication.auth_type == AuthType.HEADER:
|
|
53
|
+
checker = HeaderAuthChecker(key=authentication.header_key, real_value=authentication.real_value)
|
|
54
|
+
return [
|
|
55
|
+
Depends(checker),
|
|
56
|
+
Depends(APIKeyHeader(name=authentication.header_key)),
|
|
57
|
+
]
|
|
58
|
+
elif authentication.auth_type == AuthType.QUERY:
|
|
59
|
+
checker = QueryAuthChecker(key=authentication.header_key, real_value=authentication.real_value)
|
|
60
|
+
return [
|
|
61
|
+
Depends(checker),
|
|
62
|
+
Depends(APIKeyQuery(name=authentication.header_key)),
|
|
63
|
+
]
|
|
64
|
+
else:
|
|
65
|
+
return []
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from fastapi.encoders import jsonable_encoder
|
|
3
|
+
from fastapi.responses import JSONResponse, Response
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def return_response(
|
|
7
|
+
data: Any,
|
|
8
|
+
status_code: int,
|
|
9
|
+
response_class: Response,
|
|
10
|
+
return_json_directly: bool = False,
|
|
11
|
+
headers: dict[int, dict] = None,
|
|
12
|
+
no_cache: bool = True,
|
|
13
|
+
) -> Response:
|
|
14
|
+
if headers is None:
|
|
15
|
+
headers = {}
|
|
16
|
+
|
|
17
|
+
final_headers = headers.get(status_code) or {}
|
|
18
|
+
|
|
19
|
+
if no_cache:
|
|
20
|
+
final_headers.update({
|
|
21
|
+
'Cache-Control': 'private, no-cache, no-store, must-revalidate, max-age=0, s-maxage=0',
|
|
22
|
+
'Pragma': 'no-cache',
|
|
23
|
+
'Expires': '0',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
if response_class != JSONResponse:
|
|
27
|
+
return_json_directly = True
|
|
28
|
+
|
|
29
|
+
if return_json_directly:
|
|
30
|
+
content = data
|
|
31
|
+
else:
|
|
32
|
+
if status_code < 300:
|
|
33
|
+
content = {'success': True, 'data': jsonable_encoder(data)}
|
|
34
|
+
else:
|
|
35
|
+
content = {'success': False, 'message': data}
|
|
36
|
+
|
|
37
|
+
return response_class(
|
|
38
|
+
status_code=status_code,
|
|
39
|
+
content=content,
|
|
40
|
+
headers=final_headers
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def return_json_response(
|
|
45
|
+
data: Any,
|
|
46
|
+
status_code: int,
|
|
47
|
+
return_json_directly: bool = False,
|
|
48
|
+
headers: dict[int, dict] = None,
|
|
49
|
+
no_cache: bool = True,
|
|
50
|
+
) -> JSONResponse:
|
|
51
|
+
return return_response(
|
|
52
|
+
data=data,
|
|
53
|
+
status_code=status_code,
|
|
54
|
+
return_json_directly=return_json_directly,
|
|
55
|
+
headers=headers,
|
|
56
|
+
no_cache=no_cache,
|
|
57
|
+
response_class=JSONResponse,
|
|
58
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Callable, List, Optional, Type, Any, Dict
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
from fastapi.responses import Response, JSONResponse
|
|
7
|
+
|
|
8
|
+
from .authentication import Auth, auth_data_class_to_dependency
|
|
9
|
+
from .response import return_response
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class RouteMethod(Enum):
|
|
13
|
+
GET = "GET"
|
|
14
|
+
PUT = "PUT"
|
|
15
|
+
POST = "POST"
|
|
16
|
+
DELETE = "DELETE"
|
|
17
|
+
PATCH = "PATCH"
|
|
18
|
+
HEAD = "HEAD"
|
|
19
|
+
OPTIONS = "OPTIONS"
|
|
20
|
+
TRACE = "TRACE"
|
|
21
|
+
CONNECT = "CONNECT"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Route:
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
path: str,
|
|
28
|
+
method: RouteMethod,
|
|
29
|
+
handler: Callable,
|
|
30
|
+
wrapping_handler: bool = True,
|
|
31
|
+
authentications: list[Auth] = None,
|
|
32
|
+
response_model: Optional[Type[BaseModel]] = None,
|
|
33
|
+
status_code: int = 200,
|
|
34
|
+
dependencies: List[Any] = None,
|
|
35
|
+
wrapper_kwargs: Dict[str, Any] = None,
|
|
36
|
+
name: Optional[str] = None,
|
|
37
|
+
summary: Optional[str] = None,
|
|
38
|
+
description: Optional[str] = None,
|
|
39
|
+
tags: Optional[List[str]] = None,
|
|
40
|
+
response_description: str = "Successful Response",
|
|
41
|
+
responses: Optional[Dict[int, Dict[str, Any]]] = None,
|
|
42
|
+
deprecated: bool = False,
|
|
43
|
+
operation_id: Optional[str] = None,
|
|
44
|
+
include_in_schema: bool = True,
|
|
45
|
+
response_class: Optional[Response] = JSONResponse,
|
|
46
|
+
response_model_exclude_unset: bool = False,
|
|
47
|
+
response_model_exclude_defaults: bool = False,
|
|
48
|
+
response_model_exclude_none: bool = False,
|
|
49
|
+
response_model_by_alias: bool = True,
|
|
50
|
+
response_return_json_directly: bool = False,
|
|
51
|
+
response_headers: dict[int, dict] = None,
|
|
52
|
+
response_no_cache_headers: bool = True,
|
|
53
|
+
):
|
|
54
|
+
"""
|
|
55
|
+
Initialize a Route instance and create a wrapped handler.
|
|
56
|
+
"""
|
|
57
|
+
dependencies = dependencies or []
|
|
58
|
+
for authentication in (authentications or []):
|
|
59
|
+
dependencies.extend(auth_data_class_to_dependency(authentication))
|
|
60
|
+
|
|
61
|
+
self.dependencies = dependencies
|
|
62
|
+
|
|
63
|
+
self.path = path
|
|
64
|
+
self.method = method
|
|
65
|
+
self.handler = handler
|
|
66
|
+
self.wrapping_handler = wrapping_handler
|
|
67
|
+
self.response_model = response_model
|
|
68
|
+
self.status_code = status_code
|
|
69
|
+
self.wrapper_kwargs = wrapper_kwargs or {}
|
|
70
|
+
self.name = name
|
|
71
|
+
self.summary = summary
|
|
72
|
+
self.description = description
|
|
73
|
+
self.tags = tags
|
|
74
|
+
self.response_description = response_description
|
|
75
|
+
self.responses = responses
|
|
76
|
+
self.deprecated = deprecated
|
|
77
|
+
self.operation_id = operation_id
|
|
78
|
+
self.include_in_schema = include_in_schema
|
|
79
|
+
self.response_class = response_class
|
|
80
|
+
self.response_model_exclude_unset = response_model_exclude_unset
|
|
81
|
+
self.response_model_exclude_defaults = response_model_exclude_defaults
|
|
82
|
+
self.response_model_exclude_none = response_model_exclude_none
|
|
83
|
+
self.response_model_by_alias = response_model_by_alias
|
|
84
|
+
self.response_return_json_directly = response_return_json_directly
|
|
85
|
+
self.response_headers = response_headers
|
|
86
|
+
self.response_no_cache_headers = response_no_cache_headers
|
|
87
|
+
|
|
88
|
+
def wrapped_handler(self) -> Callable:
|
|
89
|
+
"""
|
|
90
|
+
Create and return a wrapped handler based on the response type.
|
|
91
|
+
"""
|
|
92
|
+
is_async = inspect.iscoroutinefunction(self.handler)
|
|
93
|
+
|
|
94
|
+
def wrap_function(wrapped_func: Callable) -> Callable:
|
|
95
|
+
@wraps(wrapped_func)
|
|
96
|
+
async def async_wrapper(*args, **kwargs):
|
|
97
|
+
result = await wrapped_func(*args, **kwargs)
|
|
98
|
+
if self.wrapping_handler:
|
|
99
|
+
return return_response(
|
|
100
|
+
data=result,
|
|
101
|
+
status_code=self.status_code,
|
|
102
|
+
response_class=self.response_class,
|
|
103
|
+
return_json_directly=self.response_return_json_directly,
|
|
104
|
+
headers=self.response_headers,
|
|
105
|
+
no_cache=self.response_no_cache_headers,
|
|
106
|
+
)
|
|
107
|
+
else:
|
|
108
|
+
return result
|
|
109
|
+
|
|
110
|
+
@wraps(wrapped_func)
|
|
111
|
+
def sync_wrapper(*args, **kwargs):
|
|
112
|
+
result = wrapped_func(*args, **kwargs)
|
|
113
|
+
if self.wrapping_handler:
|
|
114
|
+
return return_response(
|
|
115
|
+
data=result,
|
|
116
|
+
status_code=self.status_code,
|
|
117
|
+
response_class=self.response_class,
|
|
118
|
+
return_json_directly=self.response_return_json_directly,
|
|
119
|
+
headers=self.response_headers,
|
|
120
|
+
no_cache=self.response_no_cache_headers,
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
return async_wrapper if is_async else sync_wrapper
|
|
126
|
+
|
|
127
|
+
return wrap_function(self.handler)
|