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.
Files changed (67) hide show
  1. dtpyfw-0.0.1/PKG-INFO +49 -0
  2. dtpyfw-0.0.1/README.md +11 -0
  3. dtpyfw-0.0.1/dtpyfw/api/__init__.py +3 -0
  4. dtpyfw-0.0.1/dtpyfw/api/applications/__init__.py +0 -0
  5. dtpyfw-0.0.1/dtpyfw/api/applications/main.py +105 -0
  6. dtpyfw-0.0.1/dtpyfw/api/applications/sub.py +87 -0
  7. dtpyfw-0.0.1/dtpyfw/api/middlewares/__init__.py +0 -0
  8. dtpyfw-0.0.1/dtpyfw/api/middlewares/http_exception.py +13 -0
  9. dtpyfw-0.0.1/dtpyfw/api/middlewares/runtime.py +91 -0
  10. dtpyfw-0.0.1/dtpyfw/api/middlewares/validation_exception.py +23 -0
  11. dtpyfw-0.0.1/dtpyfw/api/routes/__init__.py +0 -0
  12. dtpyfw-0.0.1/dtpyfw/api/routes/authentication.py +65 -0
  13. dtpyfw-0.0.1/dtpyfw/api/routes/response.py +58 -0
  14. dtpyfw-0.0.1/dtpyfw/api/routes/route.py +127 -0
  15. dtpyfw-0.0.1/dtpyfw/api/routes/router.py +82 -0
  16. dtpyfw-0.0.1/dtpyfw/api/schemas/__init__.py +0 -0
  17. dtpyfw-0.0.1/dtpyfw/api/schemas/models.py +53 -0
  18. dtpyfw-0.0.1/dtpyfw/bucket/__init__.py +3 -0
  19. dtpyfw-0.0.1/dtpyfw/bucket/bucket.py +171 -0
  20. dtpyfw-0.0.1/dtpyfw/core/__init__.py +3 -0
  21. dtpyfw-0.0.1/dtpyfw/core/async.py +6 -0
  22. dtpyfw-0.0.1/dtpyfw/core/chunking.py +9 -0
  23. dtpyfw-0.0.1/dtpyfw/core/enums.py +6 -0
  24. dtpyfw-0.0.1/dtpyfw/core/env.py +44 -0
  25. dtpyfw-0.0.1/dtpyfw/core/exception.py +38 -0
  26. dtpyfw-0.0.1/dtpyfw/core/file_folder.py +15 -0
  27. dtpyfw-0.0.1/dtpyfw/core/hashing.py +53 -0
  28. dtpyfw-0.0.1/dtpyfw/core/jsonable_encoder.py +6 -0
  29. dtpyfw-0.0.1/dtpyfw/core/request.py +128 -0
  30. dtpyfw-0.0.1/dtpyfw/core/require_extra.py +10 -0
  31. dtpyfw-0.0.1/dtpyfw/core/retry.py +116 -0
  32. dtpyfw-0.0.1/dtpyfw/core/safe_access.py +16 -0
  33. dtpyfw-0.0.1/dtpyfw/core/singleton.py +22 -0
  34. dtpyfw-0.0.1/dtpyfw/core/slug.py +21 -0
  35. dtpyfw-0.0.1/dtpyfw/core/url.py +17 -0
  36. dtpyfw-0.0.1/dtpyfw/core/validation.py +44 -0
  37. dtpyfw-0.0.1/dtpyfw/db/__init__.py +3 -0
  38. dtpyfw-0.0.1/dtpyfw/db/config.py +53 -0
  39. dtpyfw-0.0.1/dtpyfw/db/database.py +271 -0
  40. dtpyfw-0.0.1/dtpyfw/db/health.py +17 -0
  41. dtpyfw-0.0.1/dtpyfw/db/model.py +173 -0
  42. dtpyfw-0.0.1/dtpyfw/db/search.py +429 -0
  43. dtpyfw-0.0.1/dtpyfw/db/utils.py +108 -0
  44. dtpyfw-0.0.1/dtpyfw/ftp/__init__.py +3 -0
  45. dtpyfw-0.0.1/dtpyfw/ftp/client.py +202 -0
  46. dtpyfw-0.0.1/dtpyfw/log/__init__.py +3 -0
  47. dtpyfw-0.0.1/dtpyfw/log/api_handler.py +65 -0
  48. dtpyfw-0.0.1/dtpyfw/log/config.py +38 -0
  49. dtpyfw-0.0.1/dtpyfw/log/footprint.py +27 -0
  50. dtpyfw-0.0.1/dtpyfw/log/formatter.py +13 -0
  51. dtpyfw-0.0.1/dtpyfw/log/handlers.py +47 -0
  52. dtpyfw-0.0.1/dtpyfw/log/initializer.py +35 -0
  53. dtpyfw-0.0.1/dtpyfw/redis/__init__.py +3 -0
  54. dtpyfw-0.0.1/dtpyfw/redis/caching.py +196 -0
  55. dtpyfw-0.0.1/dtpyfw/redis/config.py +37 -0
  56. dtpyfw-0.0.1/dtpyfw/redis/connection.py +62 -0
  57. dtpyfw-0.0.1/dtpyfw/redis/health.py +8 -0
  58. dtpyfw-0.0.1/dtpyfw/security/__init__.py +3 -0
  59. dtpyfw-0.0.1/dtpyfw/security/encryption.py +53 -0
  60. dtpyfw-0.0.1/dtpyfw/security/hashing.py +14 -0
  61. dtpyfw-0.0.1/dtpyfw/streamer/__init__.py +3 -0
  62. dtpyfw-0.0.1/dtpyfw/streamer/consumer.py +244 -0
  63. dtpyfw-0.0.1/dtpyfw/streamer/sender.py +32 -0
  64. dtpyfw-0.0.1/dtpyfw/worker/__init__.py +3 -0
  65. dtpyfw-0.0.1/dtpyfw/worker/task.py +39 -0
  66. dtpyfw-0.0.1/dtpyfw/worker/worker.py +128 -0
  67. 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
@@ -0,0 +1,11 @@
1
+ # Dealer Tower Python Framework
2
+
3
+ DealerTower Framework: reusable building‑blocks for DealerTower services
4
+
5
+ ## Installation
6
+
7
+ You can install the package via pip:
8
+
9
+ ```bash
10
+ pip install dtpyfw
11
+ ```
@@ -0,0 +1,3 @@
1
+ from ..core.require_extra import require_extra
2
+
3
+ require_extra("api", "fastapi", "starlette", "pydantic")
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)