shaapi 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. shaapi/__init__.py +3 -0
  2. shaapi/cli.py +97 -0
  3. shaapi/generator.py +114 -0
  4. shaapi/template/.dockerignore +37 -0
  5. shaapi/template/.env.template +59 -0
  6. shaapi/template/.gitattributes +12 -0
  7. shaapi/template/.gitignore +170 -0
  8. shaapi/template/.gitlab-ci.yml +89 -0
  9. shaapi/template/Dockerfile +59 -0
  10. shaapi/template/LICENSE +21 -0
  11. shaapi/template/README.md +206 -0
  12. shaapi/template/backend/.gitignore +164 -0
  13. shaapi/template/backend/__init__.py +0 -0
  14. shaapi/template/backend/alembic/README +1 -0
  15. shaapi/template/backend/alembic/env.py +102 -0
  16. shaapi/template/backend/alembic/script.py.mako +26 -0
  17. shaapi/template/backend/alembic/versions/2026_06_08_1024-64524c63b666_initial.py +143 -0
  18. shaapi/template/backend/alembic.ini +117 -0
  19. shaapi/template/backend/app/__init__.py +55 -0
  20. shaapi/template/backend/app/admin/__init__.py +1 -0
  21. shaapi/template/backend/app/admin/api/v1/__init__.py +0 -0
  22. shaapi/template/backend/app/admin/api/v1/auth.py +59 -0
  23. shaapi/template/backend/app/admin/api/v1/casbin.py +218 -0
  24. shaapi/template/backend/app/admin/api/v1/login_log.py +63 -0
  25. shaapi/template/backend/app/admin/api/v1/opera_log.py +61 -0
  26. shaapi/template/backend/app/admin/api/v1/role.py +108 -0
  27. shaapi/template/backend/app/admin/api/v1/user.py +47 -0
  28. shaapi/template/backend/app/admin/schema/casbin_rule.py +45 -0
  29. shaapi/template/backend/app/admin/schema/login_log.py +36 -0
  30. shaapi/template/backend/app/admin/schema/opera_log.py +43 -0
  31. shaapi/template/backend/app/admin/schema/role.py +36 -0
  32. shaapi/template/backend/app/admin/schema/sso.py +37 -0
  33. shaapi/template/backend/app/admin/schema/token.py +74 -0
  34. shaapi/template/backend/app/admin/schema/user.py +93 -0
  35. shaapi/template/backend/app/admin/service/auth_service.py +233 -0
  36. shaapi/template/backend/app/admin/service/casbin_service.py +135 -0
  37. shaapi/template/backend/app/admin/service/login_log_service.py +62 -0
  38. shaapi/template/backend/app/admin/service/opera_log_service.py +31 -0
  39. shaapi/template/backend/app/admin/service/role_service.py +79 -0
  40. shaapi/template/backend/app/admin/service/secure_token_service.py +60 -0
  41. shaapi/template/backend/app/admin/service/user_service.py +153 -0
  42. shaapi/template/backend/app/api.py +11 -0
  43. shaapi/template/backend/common/__init__.py +0 -0
  44. shaapi/template/backend/common/cloud_storage/__init__.py +11 -0
  45. shaapi/template/backend/common/cloud_storage/cloud_storage.py +180 -0
  46. shaapi/template/backend/common/dataclasses.py +52 -0
  47. shaapi/template/backend/common/email_conf/email.py +105 -0
  48. shaapi/template/backend/common/enums.py +144 -0
  49. shaapi/template/backend/common/exception/__init__.py +0 -0
  50. shaapi/template/backend/common/exception/errors.py +87 -0
  51. shaapi/template/backend/common/exception/exception_handler.py +280 -0
  52. shaapi/template/backend/common/log.py +123 -0
  53. shaapi/template/backend/common/model.py +68 -0
  54. shaapi/template/backend/common/pagination.py +83 -0
  55. shaapi/template/backend/common/response/__init__.py +0 -0
  56. shaapi/template/backend/common/response/response_code.py +158 -0
  57. shaapi/template/backend/common/response/response_schema.py +110 -0
  58. shaapi/template/backend/common/schema.py +144 -0
  59. shaapi/template/backend/common/security/jwt.py +203 -0
  60. shaapi/template/backend/common/security/rbac.py +98 -0
  61. shaapi/template/backend/common/security/sec_token.py +6 -0
  62. shaapi/template/backend/common/socketio/action.py +11 -0
  63. shaapi/template/backend/common/socketio/server.py +50 -0
  64. shaapi/template/backend/common/sso/base.py +69 -0
  65. shaapi/template/backend/common/sso/google.py +127 -0
  66. shaapi/template/backend/core/conf.py +208 -0
  67. shaapi/template/backend/core/path_conf.py +24 -0
  68. shaapi/template/backend/core/registrar.py +195 -0
  69. shaapi/template/backend/crud/__init__.py +1 -0
  70. shaapi/template/backend/crud/crud_base.py +35 -0
  71. shaapi/template/backend/crud/crud_casbin.py +46 -0
  72. shaapi/template/backend/crud/crud_login_log.py +58 -0
  73. shaapi/template/backend/crud/crud_opera_log.py +58 -0
  74. shaapi/template/backend/crud/crud_role.py +128 -0
  75. shaapi/template/backend/crud/crud_user.py +267 -0
  76. shaapi/template/backend/database/__init__.py +0 -0
  77. shaapi/template/backend/database/db_postgres.py +125 -0
  78. shaapi/template/backend/database/db_redis.py +62 -0
  79. shaapi/template/backend/entrypoint-api.sh +19 -0
  80. shaapi/template/backend/lang/en/app.py +18 -0
  81. shaapi/template/backend/lang/en/auth.py +10 -0
  82. shaapi/template/backend/lang/fr/app.py +18 -0
  83. shaapi/template/backend/lang/fr/auth.py +10 -0
  84. shaapi/template/backend/main.py +54 -0
  85. shaapi/template/backend/middleware/__init__.py +1 -0
  86. shaapi/template/backend/middleware/access_middleware.py +19 -0
  87. shaapi/template/backend/middleware/i18n_middleware.py +19 -0
  88. shaapi/template/backend/middleware/jwt_auth_middleware.py +73 -0
  89. shaapi/template/backend/middleware/opera_log_middleware.py +179 -0
  90. shaapi/template/backend/middleware/state_middleware.py +26 -0
  91. shaapi/template/backend/models/__init__.py +10 -0
  92. shaapi/template/backend/models/associations.py +20 -0
  93. shaapi/template/backend/models/casbin_rule.py +30 -0
  94. shaapi/template/backend/models/login_log.py +28 -0
  95. shaapi/template/backend/models/opera_log.py +36 -0
  96. shaapi/template/backend/models/role.py +27 -0
  97. shaapi/template/backend/models/user.py +30 -0
  98. shaapi/template/backend/seeder/json/admin.json +15 -0
  99. shaapi/template/backend/seeder/json/user.json +15 -0
  100. shaapi/template/backend/seeder/run.py +34 -0
  101. shaapi/template/backend/static/ip2region.xdb +0 -0
  102. shaapi/template/backend/templates/build/meet.html +169 -0
  103. shaapi/template/backend/templates/build/new_account.html +373 -0
  104. shaapi/template/backend/templates/build/reset-password.html +170 -0
  105. shaapi/template/backend/templates/build/test_email.html +25 -0
  106. shaapi/template/backend/templates/build/welcome-one-1.html +160 -0
  107. shaapi/template/backend/templates/build/welcome-one.html +178 -0
  108. shaapi/template/backend/templates/build/welcome-two.html +234 -0
  109. shaapi/template/backend/templates/index.html +0 -0
  110. shaapi/template/backend/templates/src/new_account.mjml +15 -0
  111. shaapi/template/backend/templates/src/reset_password.mjml +19 -0
  112. shaapi/template/backend/templates/src/test_email.mjml +11 -0
  113. shaapi/template/backend/templates/ws/ws.html +70 -0
  114. shaapi/template/backend/utils/demo_site.py +18 -0
  115. shaapi/template/backend/utils/encrypt.py +108 -0
  116. shaapi/template/backend/utils/health_check.py +34 -0
  117. shaapi/template/backend/utils/prometheus.py +135 -0
  118. shaapi/template/backend/utils/request_parse.py +110 -0
  119. shaapi/template/backend/utils/serializers.py +75 -0
  120. shaapi/template/backend/utils/timezone.py +51 -0
  121. shaapi/template/backend/utils/trace_id.py +7 -0
  122. shaapi/template/backend/utils/translator.py +28 -0
  123. shaapi/template/devops/scripts/deploy.sh +7 -0
  124. shaapi/template/devops/scripts/setup_env.sh +62 -0
  125. shaapi/template/docker-compose.monitoring.yml +63 -0
  126. shaapi/template/docker-compose.override.yml +12 -0
  127. shaapi/template/docker-compose.yml +90 -0
  128. shaapi/template/docker-run.sh +99 -0
  129. shaapi/template/etc/dashboards/fastapi-observability.json +1044 -0
  130. shaapi/template/etc/dashboards.yaml +10 -0
  131. shaapi/template/etc/grafana/datasource.yml +79 -0
  132. shaapi/template/etc/prometheus/prometheus.yml +52 -0
  133. shaapi/template/package-lock.json +2102 -0
  134. shaapi/template/package.json +16 -0
  135. shaapi/template/pyproject.toml +78 -0
  136. shaapi/template/uv.lock +2866 -0
  137. shaapi-0.1.0.dist-info/METADATA +92 -0
  138. shaapi-0.1.0.dist-info/RECORD +141 -0
  139. shaapi-0.1.0.dist-info/WHEEL +4 -0
  140. shaapi-0.1.0.dist-info/entry_points.txt +2 -0
  141. shaapi-0.1.0.dist-info/licenses/LICENCE +21 -0
@@ -0,0 +1,280 @@
1
+ from fastapi import FastAPI, Request
2
+ from fastapi.exceptions import RequestValidationError
3
+ from pydantic import ValidationError
4
+ from pydantic.errors import PydanticUserError
5
+ from starlette.exceptions import HTTPException
6
+ from starlette.middleware.cors import CORSMiddleware
7
+ from uvicorn.protocols.http.h11_impl import STATUS_PHRASES
8
+
9
+ from backend.common.exception.errors import BaseExceptionMixin
10
+ from backend.common.response.response_code import CustomResponseCode, StandardResponseCode
11
+ from backend.common.response.response_schema import response_base
12
+ from backend.common.schema import (
13
+ CUSTOM_USAGE_ERROR_MESSAGES,
14
+ CUSTOM_VALIDATION_ERROR_MESSAGES,
15
+ )
16
+ from backend.core.conf import settings
17
+ from backend.utils.serializers import MsgSpecJSONResponse
18
+ from backend.utils.trace_id import get_request_trace_id
19
+
20
+
21
+ def _get_exception_code(status_code: int):
22
+ """
23
+ Get return status code, OpenAPI, Uvicorn... The available status codes are based on the RFC definitions, see the link below for detailed code.
24
+
25
+ `python Status Code Standard Support <https://github.com/python/cpython/blob/6e3cc72afeaee2532b4327776501eb8234ac787b/Lib/http
26
+ /__init__.py#L7>`__
27
+
28
+ `IANA status code registry <https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml>`__
29
+
30
+ :param status_code:
31
+ :return:
32
+ """
33
+ try:
34
+ STATUS_PHRASES[status_code]
35
+ except Exception:
36
+ code = StandardResponseCode.HTTP_400
37
+ else:
38
+ code = status_code
39
+ return code
40
+
41
+
42
+ async def _validation_exception_handler(request: Request, e: RequestValidationError | ValidationError):
43
+ """
44
+ Data Validation Exception Handling
45
+
46
+ :param e:
47
+ :return:
48
+ """
49
+ errors = []
50
+ for error in e.errors():
51
+ custom_message = CUSTOM_VALIDATION_ERROR_MESSAGES.get(error['type'])
52
+ if custom_message:
53
+ ctx = error.get('ctx')
54
+ if not ctx:
55
+ error['msg'] = custom_message
56
+ else:
57
+ error['msg'] = custom_message.format(**ctx)
58
+ ctx_error = ctx.get('error')
59
+ if ctx_error:
60
+ error['ctx']['error'] = (
61
+ ctx_error.__str__().replace("'", '"') if isinstance(ctx_error, Exception) else None
62
+ )
63
+ errors.append(error)
64
+ error = errors[0]
65
+ if error.get('type') == 'json_invalid':
66
+ message = 'json parsing failure'
67
+ else:
68
+ error_input = error.get('input')
69
+ field = str(error.get('loc')[-1])
70
+ error_msg = error.get('msg')
71
+ message = f'{field} {error_msg} Input:{error_input}' if settings.ENVIRONMENT == 'dev' else error_msg
72
+ msg = f'Illegal request parameters: {message}'
73
+ data = {'errors': errors} if settings.ENVIRONMENT == 'dev' else None
74
+ content = {
75
+ 'code': StandardResponseCode.HTTP_422,
76
+ 'msg': msg,
77
+ 'data': data,
78
+ }
79
+ request.state.__request_validation_exception__ = content # 用于在中间件中获取异常信息
80
+ content.update(trace_id=get_request_trace_id(request))
81
+ return MsgSpecJSONResponse(status_code=422, content=content)
82
+
83
+
84
+ def register_exception(app: FastAPI):
85
+ @app.exception_handler(HTTPException)
86
+ async def http_exception_handler(request: Request, exc: HTTPException):
87
+ """
88
+ Global HTTP Exception Handling
89
+
90
+ :param request:
91
+ :param exc:
92
+ :return:
93
+ """
94
+ if settings.ENVIRONMENT == 'dev':
95
+ content = {
96
+ 'code': exc.status_code,
97
+ 'msg': exc.detail,
98
+ 'data': None,
99
+ }
100
+ else:
101
+ res = response_base.fail(request=request, res=CustomResponseCode.HTTP_400)
102
+ content = res.model_dump()
103
+ request.state.__request_http_exception__ = content
104
+ content.update(trace_id=get_request_trace_id(request))
105
+ return MsgSpecJSONResponse(
106
+ status_code=_get_exception_code(exc.status_code),
107
+ content=content,
108
+ headers=exc.headers,
109
+ )
110
+
111
+ @app.exception_handler(RequestValidationError)
112
+ async def fastapi_validation_exception_handler(request: Request, exc: RequestValidationError):
113
+ """
114
+ fastapi Data Validation Exception Handling
115
+
116
+ :param request:
117
+ :param exc:
118
+ :return:
119
+ """
120
+ return await _validation_exception_handler(request, exc)
121
+
122
+ @app.exception_handler(ValidationError)
123
+ async def pydantic_validation_exception_handler(request: Request, exc: ValidationError):
124
+ """
125
+ pydantic Data Validation Exception Handling
126
+
127
+ :param request:
128
+ :param exc:
129
+ :return:
130
+ """
131
+ return await _validation_exception_handler(request, exc)
132
+
133
+ @app.exception_handler(PydanticUserError)
134
+ async def pydantic_user_error_handler(request: Request, exc: PydanticUserError):
135
+ """
136
+ Pydantic User Exception Handling
137
+
138
+ :param request:
139
+ :param exc:
140
+ :return:
141
+ """
142
+ content = {
143
+ 'code': StandardResponseCode.HTTP_500,
144
+ 'msg': CUSTOM_USAGE_ERROR_MESSAGES.get(exc.code),
145
+ 'data': None,
146
+ }
147
+ request.state.__request_pydantic_user_error__ = content
148
+ content.update(trace_id=get_request_trace_id(request))
149
+ return MsgSpecJSONResponse(
150
+ status_code=StandardResponseCode.HTTP_500,
151
+ content=content,
152
+ )
153
+
154
+ @app.exception_handler(AssertionError)
155
+ async def assertion_error_handler(request: Request, exc: AssertionError):
156
+ """
157
+ assertion error handling
158
+
159
+ :param request:
160
+ :param exc:
161
+ :return:
162
+ """
163
+ if settings.ENVIRONMENT == 'dev':
164
+ content = {
165
+ 'code': StandardResponseCode.HTTP_500,
166
+ 'msg': str(''.join(exc.args) if exc.args else exc.__doc__),
167
+ 'data': None,
168
+ }
169
+ else:
170
+ res = response_base.fail(request=request, res=CustomResponseCode.HTTP_500)
171
+ content = res.model_dump()
172
+ request.state.__request_assertion_error__ = content
173
+ content.update(trace_id=get_request_trace_id(request))
174
+ return MsgSpecJSONResponse(
175
+ status_code=StandardResponseCode.HTTP_500,
176
+ content=content,
177
+ )
178
+
179
+ @app.exception_handler(BaseExceptionMixin)
180
+ async def custom_exception_handler(request: Request, exc: BaseExceptionMixin):
181
+ """
182
+ Global custom exception handling
183
+
184
+ :param request:
185
+ :param exc:
186
+ :return:
187
+ """
188
+ content = {
189
+ 'code': exc.code,
190
+ 'msg': str(exc.msg),
191
+ 'data': exc.data if exc.data else None,
192
+ }
193
+ request.state.__request_custom_exception__ = content
194
+ content.update(trace_id=get_request_trace_id(request))
195
+ return MsgSpecJSONResponse(
196
+ status_code=_get_exception_code(exc.code),
197
+ content=content,
198
+ background=exc.background,
199
+ )
200
+
201
+ @app.exception_handler(Exception)
202
+ async def all_unknown_exception_handler(request: Request, exc: Exception):
203
+ """
204
+ global unknown exception handling (GUE)
205
+
206
+ :param request:
207
+ :param exc:
208
+ :return:
209
+ """
210
+ if settings.ENVIRONMENT == 'dev':
211
+ content = {
212
+ 'code': StandardResponseCode.HTTP_500,
213
+ 'msg': str(exc),
214
+ 'data': None,
215
+ }
216
+ else:
217
+ res = response_base.fail(request=request, res=CustomResponseCode.HTTP_500)
218
+ content = res.model_dump()
219
+ request.state.__request_all_unknown_exception__ = content
220
+ content.update(trace_id=get_request_trace_id(request))
221
+ return MsgSpecJSONResponse(
222
+ status_code=StandardResponseCode.HTTP_500,
223
+ content=content,
224
+ )
225
+
226
+ if settings.MIDDLEWARE_CORS:
227
+
228
+ @app.exception_handler(StandardResponseCode.HTTP_500)
229
+ async def cors_custom_code_500_exception_handler(request, exc):
230
+ """
231
+ Cross Domain Customization 500 Exception Handling
232
+
233
+ `Related issue <https://github.com/encode/starlette/issues/1175>`_
234
+ `Solution <https://github.com/fastapi/fastapi/discussions/7847#discussioncomment-5144709>`_
235
+
236
+ :param request:
237
+ :param exc:
238
+ :return:
239
+ """
240
+ if isinstance(exc, BaseExceptionMixin):
241
+ content = {
242
+ 'code': exc.code,
243
+ 'msg': exc.msg,
244
+ 'data': exc.data,
245
+ }
246
+ else:
247
+ if settings.ENVIRONMENT == 'dev':
248
+ content = {
249
+ 'code': StandardResponseCode.HTTP_500,
250
+ 'msg': str(exc),
251
+ 'data': None,
252
+ }
253
+ else:
254
+ res = response_base.fail(request=request, res=CustomResponseCode.HTTP_500)
255
+ content = res.model_dump()
256
+ request.state.__request_cors_500_exception__ = content
257
+ content.update(trace_id=get_request_trace_id(request))
258
+ response = MsgSpecJSONResponse(
259
+ status_code=exc.code if isinstance(exc, BaseExceptionMixin) else StandardResponseCode.HTTP_500,
260
+ content=content,
261
+ background=exc.background if isinstance(exc, BaseExceptionMixin) else None,
262
+ )
263
+ origin = request.headers.get('origin')
264
+ if origin:
265
+ cors = CORSMiddleware(
266
+ app=app,
267
+ allow_origins=settings.CORS_ALLOWED_ORIGINS,
268
+ allow_credentials=True,
269
+ allow_methods=['*'],
270
+ allow_headers=['*'],
271
+ expose_headers=settings.CORS_EXPOSE_HEADERS,
272
+ )
273
+ response.headers.update(cors.simple_headers)
274
+ has_cookie = 'cookie' in request.headers
275
+ if cors.allow_all_origins and has_cookie:
276
+ response.headers['Access-Control-Allow-Origin'] = origin
277
+ elif not cors.allow_all_origins and cors.is_allowed_origin(origin=origin):
278
+ response.headers['Access-Control-Allow-Origin'] = origin
279
+ response.headers.add_vary_header('Origin')
280
+ return response
@@ -0,0 +1,123 @@
1
+ import inspect
2
+ import logging
3
+ import os
4
+
5
+ from sys import stderr, stdout
6
+
7
+ from asgi_correlation_id import correlation_id
8
+ from loguru import logger
9
+
10
+ from backend.core import path_conf
11
+ from backend.core.conf import settings
12
+
13
+
14
+ class InterceptHandler(logging.Handler):
15
+ """
16
+ Default handler from examples in loguru documentation.
17
+ See https://loguru.readthedocs.io/en/stable/overview.html#entirely-compatible-with-standard-logging
18
+ """
19
+
20
+ def emit(self, record: logging.LogRecord):
21
+ # Get corresponding Loguru level if it exists
22
+ try:
23
+ level = logger.level(record.levelname).name
24
+ except ValueError:
25
+ level = record.levelno
26
+
27
+ # Find caller from where originated the logged message.
28
+ frame, depth = inspect.currentframe(), 0
29
+ while frame and (depth == 0 or frame.f_code.co_filename == logging.__file__):
30
+ frame = frame.f_back
31
+ depth += 1
32
+
33
+ logger.opt(depth=depth, exception=record.exc_info).log(level, record.getMessage())
34
+
35
+
36
+ def setup_logging():
37
+ """
38
+ From https://pawamoy.github.io/posts/unify-logging-for-a-gunicorn-uvicorn-app/
39
+ https://github.com/pawamoy/pawamoy.github.io/issues/17
40
+ """
41
+ # Intercept everything at the root logger
42
+ logging.root.handlers = [InterceptHandler()]
43
+ logging.root.setLevel(settings.LOG_ROOT_LEVEL)
44
+
45
+ # Remove all log handlers and propagate to root logger
46
+ for name in logging.root.manager.loggerDict.keys():
47
+ logging.getLogger(name).handlers = []
48
+ if 'uvicorn.access' in name or 'watchfiles.main' in name:
49
+ logging.getLogger(name).propagate = False
50
+ else:
51
+ logging.getLogger(name).propagate = True
52
+
53
+ # Debug log handlers
54
+ # logging.debug(f'{logging.getLogger(name)}, {logging.getLogger(name).propagate}')
55
+
56
+ # Remove every other logger's handlers
57
+ logger.remove()
58
+
59
+ # Define the correlation_id filter function
60
+ # https://github.com/snok/asgi-correlation-id?tab=readme-ov-file#configure-logging
61
+ # https://github.com/snok/asgi-correlation-id/issues/7
62
+ def correlation_id_filter(record) -> bool:
63
+ cid = correlation_id.get(settings.LOG_CID_DEFAULT_VALUE)
64
+ record['correlation_id'] = cid[: settings.LOG_CID_UUID_LENGTH]
65
+ return True
66
+
67
+ # Configure loguru logger before starts logging
68
+ logger.configure(
69
+ handlers=[
70
+ {
71
+ 'sink': stdout,
72
+ 'level': settings.LOG_STDOUT_LEVEL,
73
+ 'filter': lambda record: correlation_id_filter(record) and record['level'].no <= 25,
74
+ 'format': settings.LOG_STD_FORMAT,
75
+ },
76
+ {
77
+ 'sink': stderr,
78
+ 'level': settings.LOG_STDERR_LEVEL,
79
+ 'filter': lambda record: correlation_id_filter(record) and record['level'].no >= 30,
80
+ 'format': settings.LOG_STD_FORMAT,
81
+ },
82
+ ]
83
+ )
84
+
85
+
86
+ def set_customize_logfile():
87
+ log_path = path_conf.LOG_DIR
88
+ if not os.path.exists(log_path):
89
+ os.mkdir(log_path)
90
+
91
+ # log files
92
+ log_stdout_file = os.path.join(log_path, settings.LOG_STDOUT_FILENAME)
93
+ log_stderr_file = os.path.join(log_path, settings.LOG_STDERR_FILENAME)
94
+
95
+ # loguru logger: https://loguru.readthedocs.io/en/stable/api/logger.html#loguru._logger.Logger.add
96
+ log_config = {
97
+ 'rotation': '10 MB',
98
+ 'retention': '15 days',
99
+ 'compression': 'tar.gz',
100
+ 'enqueue': True,
101
+ 'format': settings.LOG_LOGURU_FORMAT,
102
+ }
103
+
104
+ # stdout file
105
+ logger.add(
106
+ str(log_stdout_file),
107
+ level=settings.LOG_STDOUT_LEVEL,
108
+ **log_config,
109
+ backtrace=False,
110
+ diagnose=False,
111
+ )
112
+
113
+ # stderr file
114
+ logger.add(
115
+ str(log_stderr_file),
116
+ level=settings.LOG_STDERR_LEVEL,
117
+ **log_config,
118
+ backtrace=True,
119
+ diagnose=True,
120
+ )
121
+
122
+
123
+ log = logger
@@ -0,0 +1,68 @@
1
+ from sqlalchemy.orm import DeclarativeBase, Mapped, MappedAsDataclass, declared_attr, mapped_column
2
+ from sqlalchemy import DateTime, func
3
+ from ulid import ULID
4
+ from typing import Annotated
5
+ from datetime import datetime
6
+ from sqlalchemy.sql.functions import current_timestamp
7
+
8
+
9
+
10
+ id_key = Annotated[
11
+ int, mapped_column(primary_key=True, index=True, autoincrement=True, sort_order=-999, comment='Primary key id')
12
+ ]
13
+
14
+ def get_id() -> str:
15
+ """Create a new ULID object from the current timestamp"""
16
+ return str(ULID()).lower()
17
+
18
+ # Mixin: A concept of object-oriented programming, makes the structure clearer, `Wiki <https://en.wikipedia.org/wiki/Mixin/>`__
19
+ class UserMixin(MappedAsDataclass):
20
+ """User Mixin data class"""
21
+
22
+ create_user: Mapped[int] = mapped_column(sort_order=998, comment='Creator')
23
+ update_user: Mapped[int | None] = mapped_column(init=False, default=None, sort_order=998, comment='Updater')
24
+
25
+
26
+ class DateTimeMixin(MappedAsDataclass):
27
+ """Datetime Mixin data class"""
28
+
29
+ created_time: Mapped[datetime] = mapped_column(
30
+ DateTime(timezone=True), init=False, default=current_timestamp(), comment='Creation time'
31
+ )
32
+ updated_time: Mapped[datetime | None] = mapped_column(
33
+ DateTime(timezone=True), init=False, onupdate=func.now(), comment='update time'
34
+ )
35
+
36
+
37
+
38
+ class MappedBase(DeclarativeBase):
39
+ """
40
+ Declarative base class, the original DeclarativeBase class, exists as the parent class of all base or data model classes
41
+
42
+ `DeclarativeBase <https://docs.sqlalchemy.org/en/20/orm/declarative_config.html>`__
43
+ `mapped_column() <https://docs.sqlalchemy.org/en/20/orm/mapping_api.html#sqlalchemy.orm.mapped_column>`__
44
+ """
45
+
46
+ @declared_attr.directive
47
+ def __tablename__(cls) -> str:
48
+ return cls.__name__.lower()
49
+
50
+
51
+ class DataClassBase(MappedAsDataclass, MappedBase):
52
+ """
53
+ Declarative data class base, it integrates with the data class, allowing for more advanced configuration,
54
+ but you must be aware of some of its features, especially when used together with DeclarativeBase
55
+
56
+ `MappedAsDataclass <https://docs.sqlalchemy.org/en/20/orm/dataclasses.html#orm-declarative-native-dataclasses>`__
57
+ """ # noqa: E501
58
+
59
+ __abstract__ = True
60
+
61
+
62
+ class Base(DataClassBase, DateTimeMixin):
63
+ """
64
+ Declarative Mixin data class base, integrates with the data class, and includes the basic table structure of the Mixin data class.
65
+ You can simply understand it as a base class for data classes that includes the basic table structure.
66
+ """ # noqa: E501
67
+
68
+ __abstract__ = True
@@ -0,0 +1,83 @@
1
+ from __future__ import annotations
2
+
3
+ import math
4
+
5
+ from typing import TYPE_CHECKING, Dict, Generic, Sequence, TypeVar
6
+
7
+ from fastapi import Depends, Query
8
+ from fastapi_pagination import pagination_ctx
9
+ from fastapi_pagination.bases import AbstractPage, AbstractParams, RawParams
10
+ from fastapi_pagination.ext.sqlalchemy import paginate
11
+ from fastapi_pagination.links.bases import create_links
12
+ from pydantic import BaseModel
13
+
14
+ if TYPE_CHECKING:
15
+ from sqlalchemy import Select
16
+ from sqlalchemy.ext.asyncio import AsyncSession
17
+
18
+ T = TypeVar('T')
19
+ DataT = TypeVar('DataT')
20
+ SchemaT = TypeVar('SchemaT')
21
+
22
+
23
+ class _Params(BaseModel, AbstractParams):
24
+ page: int = Query(1, ge=1, description='Page number')
25
+ size: int = Query(10, gt=0, le=100, description='Page size') # Default 10 records
26
+
27
+ def to_raw_params(self) -> RawParams:
28
+ return RawParams(
29
+ limit=self.size,
30
+ offset=self.size * (self.page - 1),
31
+ )
32
+
33
+
34
+ class _Page(AbstractPage[T], Generic[T]):
35
+ items: Sequence[T] # Data
36
+ total: int # total data
37
+ page: int # Page n
38
+ size: int # per page
39
+ total_pages: int # of total pages
40
+ links: Dict[str, str | None] # Jump links
41
+
42
+ __params_type__ = _Params # Use custom Params
43
+
44
+ @classmethod
45
+ def create(
46
+ cls,
47
+ items: Sequence[T],
48
+ total: int,
49
+ params: _Params,
50
+ ) -> _Page[T]:
51
+ page = params.page
52
+ size = params.size
53
+ total_pages = math.ceil(total / params.size)
54
+ links = create_links(**{
55
+ 'first': {'page': 1, 'size': f'{size}'},
56
+ 'last': {'page': f'{math.ceil(total / params.size)}', 'size': f'{size}'} if total > 0 else None,
57
+ 'next': {'page': f'{page + 1}', 'size': f'{size}'} if (page + 1) <= total_pages else None,
58
+ 'prev': {'page': f'{page - 1}', 'size': f'{size}'} if (page - 1) >= 1 else None,
59
+ }).model_dump()
60
+
61
+ return cls(items=items, total=total, page=params.page, size=params.size, total_pages=total_pages, links=links)
62
+
63
+
64
+ class _PageData(BaseModel, Generic[DataT]):
65
+ page_data: DataT | None = None
66
+
67
+
68
+ async def paging_data(db: AsyncSession, select: Select, page_data_schema: SchemaT) -> dict:
69
+ """
70
+ Creating Paged Data Based on SQLAlchemy
71
+
72
+ :param db:
73
+ :param select:
74
+ :param page_data_schema:
75
+ :return:
76
+ """
77
+ _paginate = await paginate(db, select)
78
+ page_data = _PageData[_Page[page_data_schema]](page_data=_paginate).model_dump()['page_data']
79
+ return page_data
80
+
81
+
82
+ # Paging dependency injection
83
+ DependsPagination = Depends(pagination_ctx(_Page))
File without changes