fastgenerateapi 0.0.28__py2.py3-none-any.whl → 1.1.6__py2.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.

Potentially problematic release.


This version of fastgenerateapi might be problematic. Click here for more details.

Files changed (80) hide show
  1. fastgenerateapi/__init__.py +2 -2
  2. fastgenerateapi/__version__.py +1 -1
  3. fastgenerateapi/api_view/base_view.py +17 -7
  4. fastgenerateapi/api_view/create_view.py +1 -1
  5. fastgenerateapi/api_view/delete_filter_view.py +1 -1
  6. fastgenerateapi/api_view/delete_tree_view.py +3 -3
  7. fastgenerateapi/api_view/delete_view.py +3 -3
  8. fastgenerateapi/api_view/get_all_view.py +10 -8
  9. fastgenerateapi/api_view/get_one_view.py +1 -1
  10. fastgenerateapi/api_view/get_relation_view.py +1 -1
  11. fastgenerateapi/api_view/get_tree_view.py +1 -1
  12. fastgenerateapi/api_view/mixin/base_mixin.py +11 -7
  13. fastgenerateapi/api_view/mixin/dbmodel_mixin.py +30 -20
  14. fastgenerateapi/api_view/mixin/response_mixin.py +68 -38
  15. fastgenerateapi/api_view/mixin/tool_mixin.py +1 -357
  16. fastgenerateapi/api_view/mixin/utils/__init__.py +0 -0
  17. fastgenerateapi/api_view/mixin/utils/docx_util.py +399 -0
  18. fastgenerateapi/api_view/mixin/utils/file_util.py +30 -0
  19. fastgenerateapi/api_view/mixin/utils/pdf_util.py +76 -0
  20. fastgenerateapi/api_view/mixin/utils/xlsx_util.py +336 -0
  21. fastgenerateapi/api_view/mixin/utils/zip_util.py +50 -0
  22. fastgenerateapi/api_view/switch_view.py +2 -2
  23. fastgenerateapi/api_view/update_relation_view.py +3 -3
  24. fastgenerateapi/api_view/update_view.py +1 -1
  25. fastgenerateapi/cache/cache_decorator.py +1 -1
  26. fastgenerateapi/controller/filter_controller.py +68 -26
  27. fastgenerateapi/controller/router_controller.py +9 -9
  28. fastgenerateapi/controller/rpc_controller.py +1 -1
  29. fastgenerateapi/controller/ws_controller.py +1 -1
  30. fastgenerateapi/deps/filter_params_deps.py +34 -4
  31. fastgenerateapi/deps/paginator_deps.py +4 -4
  32. fastgenerateapi/deps/tree_params_deps.py +4 -4
  33. fastgenerateapi/fastapi_utils/__init__.py +0 -0
  34. fastgenerateapi/fastapi_utils/all.py +5 -0
  35. fastgenerateapi/fastapi_utils/param_utils.py +37 -0
  36. fastgenerateapi/fastapi_utils/response_utils.py +344 -0
  37. fastgenerateapi/model/__init__.py +0 -0
  38. fastgenerateapi/model/base_model.py +56 -0
  39. fastgenerateapi/my_fields/enum_field.py +5 -5
  40. fastgenerateapi/my_fields/validator.py +60 -0
  41. fastgenerateapi/pydantic_utils/base_model.py +46 -20
  42. fastgenerateapi/pydantic_utils/base_settings.py +16 -0
  43. fastgenerateapi/pydantic_utils/json_encoders.py +2 -1
  44. fastgenerateapi/schemas_factory/common_function.py +1 -1
  45. fastgenerateapi/schemas_factory/common_schema_factory.py +4 -4
  46. fastgenerateapi/schemas_factory/create_schema_factory.py +4 -4
  47. fastgenerateapi/schemas_factory/filter_schema_factory.py +6 -6
  48. fastgenerateapi/schemas_factory/get_all_schema_factory.py +5 -5
  49. fastgenerateapi/schemas_factory/get_one_schema_factory.py +4 -3
  50. fastgenerateapi/schemas_factory/get_relation_schema_factory.py +3 -3
  51. fastgenerateapi/schemas_factory/get_tree_schema_factory.py +3 -3
  52. fastgenerateapi/schemas_factory/response_factory.py +3 -3
  53. fastgenerateapi/schemas_factory/sql_get_all_schema_factory.py +3 -3
  54. fastgenerateapi/schemas_factory/update_schema_factory.py +4 -4
  55. fastgenerateapi/settings/__init__.py +6 -0
  56. fastgenerateapi/settings/all_settings.py +91 -0
  57. fastgenerateapi/settings/{settings.py → app_settings.py} +9 -9
  58. fastgenerateapi/settings/db_settings.py +69 -0
  59. fastgenerateapi/settings/file_settings.py +24 -0
  60. fastgenerateapi/settings/jwt_settings.py +23 -0
  61. fastgenerateapi/settings/otlp_settings.py +69 -0
  62. fastgenerateapi/settings/redis_settings.py +16 -0
  63. fastgenerateapi/settings/sms_settings.py +25 -0
  64. fastgenerateapi/settings/system_settings.py +30 -0
  65. fastgenerateapi/utils/auto_discover.py +61 -0
  66. fastgenerateapi/utils/file_utils.py +76 -0
  67. fastgenerateapi/utils/pwd_utils.py +49 -0
  68. fastgenerateapi/utils/ramdom_utils.py +48 -0
  69. fastgenerateapi/utils/snowflake.py +23 -20
  70. fastgenerateapi/utils/str_util.py +120 -0
  71. fastgenerateapi/utils/swagger_to_js.py +26 -0
  72. {fastgenerateapi-0.0.28.dist-info → fastgenerateapi-1.1.6.dist-info}/METADATA +61 -24
  73. fastgenerateapi-1.1.6.dist-info/RECORD +109 -0
  74. {fastgenerateapi-0.0.28.dist-info → fastgenerateapi-1.1.6.dist-info}/WHEEL +1 -1
  75. {fastgenerateapi-0.0.28.dist-info → fastgenerateapi-1.1.6.dist-info}/top_level.txt +1 -0
  76. script/__init__.py +2 -0
  77. fastgenerateapi/settings/register_settings.py +0 -6
  78. fastgenerateapi/utils/parse_str.py +0 -36
  79. fastgenerateapi-0.0.28.dist-info/RECORD +0 -82
  80. {fastgenerateapi-0.0.28.dist-info → fastgenerateapi-1.1.6.dist-info}/LICENSE +0 -0
@@ -1,25 +1,55 @@
1
- from typing import Type, Union, Any
1
+ from typing import Type, Union, Any, Optional
2
2
 
3
- from fastapi import Depends
3
+ from fastapi import Depends, Query
4
+ from pydantic import create_model
4
5
  from tortoise import Model
5
6
 
6
7
  from fastgenerateapi.controller.filter_controller import BaseFilter
8
+ from fastgenerateapi.data_type.data_type import PYDANTIC_SCHEMA
9
+ from fastgenerateapi.pydantic_utils.base_model import EmptyPydantic, SearchPydantic
7
10
  from fastgenerateapi.schemas_factory.filter_schema_factory import filter_schema_factory
8
11
 
9
12
 
10
- def filter_params_deps(model_class: Type[Model], fields: list[str, tuple[str, Type], BaseFilter] = None):
13
+ def search_params_deps(fields: Optional[list[str]] = None):
14
+ """
15
+ 目的:当 fields 为空时,不会生成search的文档
16
+ :param fields:
17
+ :return:
18
+ """
19
+ if not fields:
20
+ filter_schema_dep = EmptyPydantic
21
+ else:
22
+ filter_schema_dep = SearchPydantic
23
+
24
+ def filter_search(search_params: filter_schema_dep = Depends()):
25
+ return getattr(search_params, "search", "")
26
+
27
+ return filter_search
28
+
29
+
30
+ def filter_params_deps(
31
+ model_class: Type[Model],
32
+ fields: Optional[list[str, tuple[str, Type], BaseFilter]] = None,
33
+ schema: Optional[PYDANTIC_SCHEMA] = None
34
+ ):
11
35
  """
12
36
  生成filter依赖
13
37
  """
38
+ filter_schema_dep = schema or EmptyPydantic
14
39
  filter_params_model = filter_schema_factory(model_class, fields)
15
40
 
16
- def filter_query(filter_params: filter_params_model = Depends(filter_params_model)) -> dict[str, Any]:
41
+ def filter_query(
42
+ filter_params: filter_params_model = Depends(),
43
+ filter_schema: filter_schema_dep = Depends(),
44
+ ) -> dict[str, Any]:
17
45
  """
18
46
  filter 筛选字段依赖
19
47
  :param filter_params:
48
+ :param filter_schema:
20
49
  :return:
21
50
  """
22
51
  result = filter_params.dict(exclude_none=True)
52
+ result.update(filter_schema.dict(exclude_none=True))
23
53
  return result
24
54
 
25
55
  return filter_query
@@ -4,9 +4,9 @@ from fastapi import Depends, Query
4
4
  from pydantic import create_model
5
5
  from pydantic.fields import FieldInfo
6
6
 
7
- from fastgenerateapi.pydantic_utils.base_model import BaseModel, QueryConfig
8
- from fastgenerateapi.settings.register_settings import settings
9
- from fastgenerateapi.utils.parse_str import parse_str_to_int, parse_str_to_bool
7
+ from fastgenerateapi.pydantic_utils.base_model import BaseModel, model_config
8
+ from fastgenerateapi.settings.all_settings import settings
9
+ from fastgenerateapi.utils.str_util import parse_str_to_int, parse_str_to_bool
10
10
 
11
11
 
12
12
  def paginator_deps():
@@ -42,7 +42,7 @@ def paginator_deps():
42
42
  FieldInfo(default=Query(settings.app_settings.DEFAULT_WHETHER_PAGE), title="是否分页", description="是否分页")
43
43
  ),
44
44
  }
45
- pagination_model: Type[BaseModel] = create_model("paginator", **fields, __config__=QueryConfig)
45
+ pagination_model: Type[BaseModel] = create_model("paginator", **fields, __config__=model_config)
46
46
 
47
47
  def pagination_deps(paginator: pagination_model = Depends(pagination_model)) -> pagination_model:
48
48
  current_page_value = parse_str_to_int(getattr(paginator, settings.app_settings.CURRENT_PAGE_FIELD))
@@ -3,9 +3,10 @@ from typing import Type, Any, Optional
3
3
  from fastapi import Depends, Query
4
4
  from pydantic.fields import FieldInfo
5
5
 
6
- from fastgenerateapi.settings.register_settings import settings
6
+ from fastgenerateapi.pydantic_utils.base_model import model_config
7
+ from fastgenerateapi.pydantic_utils.base_model import ModelConfig
8
+ from fastgenerateapi.settings.all_settings import settings
7
9
 
8
- from fastgenerateapi.pydantic_utils.base_model import QueryConfig
9
10
  from pydantic import BaseModel, create_model
10
11
 
11
12
 
@@ -25,8 +26,7 @@ def tree_params_deps():
25
26
  )
26
27
  }
27
28
 
28
- filter_tree_params_model: Type[BaseModel] = create_model(
29
- __model_name="TreeFilterParams", **model_fields, __config__=QueryConfig)
29
+ filter_tree_params_model: Type[BaseModel] = create_model("TreeFilterParams", __config__=model_config, **model_fields)
30
30
 
31
31
  def filter_query(filter_params: filter_tree_params_model = Depends(filter_tree_params_model)) -> Optional[str]:
32
32
  """
File without changes
@@ -0,0 +1,5 @@
1
+
2
+ # 统一导入
3
+ from .param_utils import *
4
+ from .response_utils import *
5
+
@@ -0,0 +1,37 @@
1
+ # 用于解决依赖中使用Query、Path等参数时,无法获取相关信息的问题
2
+
3
+ from typing import List, Optional
4
+
5
+ import fastapi
6
+ from fastapi import params
7
+ from fastapi.dependencies.models import Dependant
8
+ from fastapi.dependencies.utils import get_sub_dependant
9
+
10
+
11
+ def get_param_sub_dependant(
12
+ *,
13
+ param_name: str,
14
+ depends: params.Depends,
15
+ path: str,
16
+ security_scopes: Optional[List[str]] = None,
17
+ ) -> Dependant:
18
+ assert depends.dependency
19
+ dependant = get_sub_dependant(
20
+ depends=depends,
21
+ dependency=depends.dependency,
22
+ path=path,
23
+ name=param_name,
24
+ security_scopes=security_scopes,
25
+ )
26
+ for query_param in dependant.query_params:
27
+ query_param_field = depends.dependency.model_fields.get(query_param.name)
28
+ if query_param_field:
29
+ query_param.field_info.description = query_param_field.description or query_param_field.title or ""
30
+ else:
31
+ query_param.field_info.description = query_param_field.description or query_param_field.title or ""
32
+ return dependant
33
+
34
+
35
+ fastapi.dependencies.utils.get_param_sub_dependant = get_param_sub_dependant
36
+
37
+
@@ -0,0 +1,344 @@
1
+ # 解决中间键日志问题
2
+ # request.scope["customize_result"]中存储业务函数的响应
3
+ # request.scope["customize_para"]中存储router 中的name、summary、description参数
4
+
5
+ # 重写fastapi routing 把路由解析参数附加到requests
6
+
7
+ from contextlib import AsyncExitStack
8
+ from fastapi._compat import ModelField, _normalize_errors
9
+ from fastapi.dependencies.utils import get_typed_return_annotation
10
+ from fastapi.types import IncEx
11
+
12
+ import asyncio
13
+ import email.message
14
+ import enum
15
+ import inspect
16
+ import json
17
+ from typing import (
18
+ Any,
19
+ Callable,
20
+ Coroutine,
21
+ Dict,
22
+ List,
23
+ Optional,
24
+ Sequence,
25
+ Set,
26
+ Type,
27
+ Union,
28
+ )
29
+
30
+ from fastapi import params
31
+ from fastapi.datastructures import Default, DefaultPlaceholder
32
+ from fastapi.dependencies.models import Dependant
33
+ from fastapi.dependencies.utils import (
34
+ get_body_field,
35
+ get_dependant,
36
+ get_parameterless_sub_dependant,
37
+ solve_dependencies,
38
+ )
39
+ from fastapi.exceptions import RequestValidationError
40
+ from fastapi.routing import run_endpoint_function
41
+ from fastapi.utils import (
42
+ create_cloned_field,
43
+ create_response_field,
44
+ is_body_allowed_for_status_code, generate_unique_id,
45
+ )
46
+ from fastapi._compat import Undefined
47
+ from pydantic.utils import lenient_issubclass
48
+ from starlette import routing
49
+ from starlette.exceptions import HTTPException
50
+ from starlette.requests import Request
51
+ from starlette.responses import JSONResponse, Response
52
+ from starlette.routing import BaseRoute
53
+ from starlette.routing import (
54
+ compile_path,
55
+ get_name,
56
+ request_response,
57
+ )
58
+ from fastapi.routing import serialize_response
59
+ from fastapi import routing as fastapi_routing
60
+
61
+
62
+ # 把数据储存到request.scope字典中
63
+ def get_request_handler(
64
+ dependant: Dependant,
65
+ body_field: Optional[ModelField] = None,
66
+ status_code: Optional[int] = None,
67
+ response_class: Union[Type[Response], DefaultPlaceholder] = Default(JSONResponse),
68
+ response_field: Optional[ModelField] = None,
69
+ response_model_include: Optional[IncEx] = None,
70
+ response_model_exclude: Optional[IncEx] = None,
71
+ response_model_by_alias: bool = True,
72
+ response_model_exclude_unset: bool = False,
73
+ response_model_exclude_defaults: bool = False,
74
+ response_model_exclude_none: bool = False,
75
+ dependency_overrides_provider: Optional[Any] = None,
76
+ customize_para: Dict = None,
77
+ ) -> Callable[[Request], Coroutine[Any, Any, Response]]:
78
+ assert dependant.call is not None, "dependant.call must be a function"
79
+ is_coroutine = asyncio.iscoroutinefunction(dependant.call)
80
+ is_body_form = body_field and isinstance(body_field.field_info, params.Form)
81
+ if isinstance(response_class, DefaultPlaceholder):
82
+ actual_response_class: Type[Response] = response_class.value
83
+ else:
84
+ actual_response_class = response_class
85
+
86
+ async def app(request: Request) -> Response:
87
+ request.scope["customize_para"] = customize_para # 增加
88
+ try:
89
+ body: Any = None
90
+ if body_field:
91
+ if is_body_form:
92
+ body = await request.form()
93
+ stack = request.scope.get("fastapi_astack")
94
+ assert isinstance(stack, AsyncExitStack)
95
+ stack.push_async_callback(body.close)
96
+ else:
97
+ body_bytes = await request.body()
98
+ if body_bytes:
99
+ json_body: Any = Undefined
100
+ content_type_value = request.headers.get("content-type")
101
+ if not content_type_value:
102
+ json_body = await request.json()
103
+ else:
104
+ message = email.message.Message()
105
+ message["content-type"] = content_type_value
106
+ if message.get_content_maintype() == "application":
107
+ subtype = message.get_content_subtype()
108
+ if subtype == "json" or subtype.endswith("+json"):
109
+ json_body = await request.json()
110
+ if json_body != Undefined:
111
+ body = json_body
112
+ else:
113
+ body = body_bytes
114
+ except json.JSONDecodeError as e:
115
+ raise RequestValidationError(
116
+ [
117
+ {
118
+ "type": "json_invalid",
119
+ "loc": ("body", e.pos),
120
+ "msg": "JSON decode error",
121
+ "input": {},
122
+ "ctx": {"error": e.msg},
123
+ }
124
+ ],
125
+ body=e.doc,
126
+ ) from e
127
+ except HTTPException:
128
+ raise
129
+ except Exception as e:
130
+ raise HTTPException(
131
+ status_code=400, detail="There was an error parsing the body"
132
+ ) from e
133
+ solved_result = await solve_dependencies(
134
+ request=request,
135
+ dependant=dependant,
136
+ body=body,
137
+ dependency_overrides_provider=dependency_overrides_provider,
138
+ )
139
+ values, errors, background_tasks, sub_response, _ = solved_result
140
+ if errors:
141
+ raise RequestValidationError(_normalize_errors(errors), body=body)
142
+ else:
143
+ raw_response = await run_endpoint_function(
144
+ dependant=dependant, values=values, is_coroutine=is_coroutine
145
+ )
146
+
147
+ if isinstance(raw_response, Response):
148
+ if raw_response.background is None:
149
+ raw_response.background = background_tasks
150
+ request.scope["customize_result"] = raw_response # 增加
151
+ return raw_response
152
+
153
+ request.scope["customize_result"] = raw_response # 增加
154
+ response_args: Dict[str, Any] = {"background": background_tasks}
155
+ # If status_code was set, use it, otherwise use the default from the
156
+ # response class, in the case of redirect it's 307
157
+ current_status_code = (
158
+ status_code if status_code else sub_response.status_code
159
+ )
160
+ if current_status_code is not None:
161
+ response_args["status_code"] = current_status_code
162
+ if sub_response.status_code:
163
+ response_args["status_code"] = sub_response.status_code
164
+ content = await serialize_response(
165
+ field=response_field,
166
+ response_content=raw_response,
167
+ include=response_model_include,
168
+ exclude=response_model_exclude,
169
+ by_alias=response_model_by_alias,
170
+ exclude_unset=response_model_exclude_unset,
171
+ exclude_defaults=response_model_exclude_defaults,
172
+ exclude_none=response_model_exclude_none,
173
+ is_coroutine=is_coroutine,
174
+ )
175
+ response = actual_response_class(content, **response_args)
176
+ if not is_body_allowed_for_status_code(response.status_code):
177
+ response.body = b""
178
+ response.headers.raw.extend(sub_response.headers.raw)
179
+ return response
180
+
181
+ return app
182
+
183
+
184
+ # 从__init__中调用方法get_route_handler,并携带参数
185
+ # get_route_handler调用get_request_handler,并携带参数
186
+ class APIRoute(routing.Route):
187
+ def __init__(
188
+ self,
189
+ path: str,
190
+ endpoint: Callable[..., Any],
191
+ *,
192
+ response_model: Any = Default(None),
193
+ status_code: Optional[int] = None,
194
+ tags: Optional[List[Union[str, enum.Enum]]] = None,
195
+ dependencies: Optional[Sequence[params.Depends]] = None,
196
+ summary: Optional[str] = None,
197
+ description: Optional[str] = None,
198
+ response_description: str = "Successful Response",
199
+ responses: Optional[Dict[Union[int, str], Dict[str, Any]]] = None,
200
+ deprecated: Optional[bool] = None,
201
+ name: Optional[str] = None,
202
+ methods: Optional[Union[Set[str], List[str]]] = None,
203
+ operation_id: Optional[str] = None,
204
+ response_model_include: Optional[IncEx] = None,
205
+ response_model_exclude: Optional[IncEx] = None,
206
+ response_model_by_alias: bool = True,
207
+ response_model_exclude_unset: bool = False,
208
+ response_model_exclude_defaults: bool = False,
209
+ response_model_exclude_none: bool = False,
210
+ include_in_schema: bool = True,
211
+ response_class: Union[Type[Response], DefaultPlaceholder] = Default(
212
+ JSONResponse
213
+ ),
214
+ dependency_overrides_provider: Optional[Any] = None,
215
+ callbacks: Optional[List[BaseRoute]] = None,
216
+ openapi_extra: Optional[Dict[str, Any]] = None,
217
+ generate_unique_id_function: Union[
218
+ Callable[["APIRoute"], str], DefaultPlaceholder
219
+ ] = Default(generate_unique_id),
220
+ ) -> None:
221
+ self.path = path
222
+ self.endpoint = endpoint
223
+ if isinstance(response_model, DefaultPlaceholder):
224
+ return_annotation = get_typed_return_annotation(endpoint)
225
+ if lenient_issubclass(return_annotation, Response):
226
+ response_model = None
227
+ else:
228
+ response_model = return_annotation
229
+ self.response_model = response_model
230
+ self.summary = summary
231
+ self.response_description = response_description
232
+ self.deprecated = deprecated
233
+ self.operation_id = operation_id
234
+ self.response_model_include = response_model_include
235
+ self.response_model_exclude = response_model_exclude
236
+ self.response_model_by_alias = response_model_by_alias
237
+ self.response_model_exclude_unset = response_model_exclude_unset
238
+ self.response_model_exclude_defaults = response_model_exclude_defaults
239
+ self.response_model_exclude_none = response_model_exclude_none
240
+ self.include_in_schema = include_in_schema
241
+ self.response_class = response_class
242
+ self.dependency_overrides_provider = dependency_overrides_provider
243
+ self.callbacks = callbacks
244
+ self.openapi_extra = openapi_extra
245
+ self.generate_unique_id_function = generate_unique_id_function
246
+ self.tags = tags or []
247
+ self.responses = responses or {}
248
+ self.name = get_name(endpoint) if name is None else name
249
+ self.path_regex, self.path_format, self.param_convertors = compile_path(path)
250
+ if methods is None:
251
+ methods = ["GET"]
252
+ self.methods: Set[str] = {method.upper() for method in methods}
253
+ if isinstance(generate_unique_id_function, DefaultPlaceholder):
254
+ current_generate_unique_id: Callable[
255
+ ["APIRoute"], str
256
+ ] = generate_unique_id_function.value
257
+ else:
258
+ current_generate_unique_id = generate_unique_id_function
259
+ self.unique_id = self.operation_id or current_generate_unique_id(self)
260
+ # normalize enums e.g. http.HTTPStatus
261
+ if isinstance(status_code, enum.IntEnum):
262
+ status_code = int(status_code)
263
+ self.status_code = status_code
264
+ if self.response_model:
265
+ assert is_body_allowed_for_status_code(
266
+ status_code
267
+ ), f"Status code {status_code} must not have a response body"
268
+ response_name = "Response_" + self.unique_id
269
+ self.response_field = create_response_field(
270
+ name=response_name,
271
+ type_=self.response_model,
272
+ mode="serialization",
273
+ )
274
+ # Create a clone of the field, so that a Pydantic submodel is not returned
275
+ # as is just because it's an instance of a subclass of a more limited class
276
+ # e.g. UserInDB (containing hashed_password) could be a subclass of User
277
+ # that doesn't have the hashed_password. But because it's a subclass, it
278
+ # would pass the validation and be returned as is.
279
+ # By being a new field, no inheritance will be passed as is. A new model
280
+ # will always be created.
281
+ # TODO: remove when deprecating Pydantic v1
282
+ self.secure_cloned_response_field: Optional[
283
+ ModelField
284
+ ] = create_cloned_field(self.response_field)
285
+ else:
286
+ self.response_field = None # type: ignore
287
+ self.secure_cloned_response_field = None
288
+ self.dependencies = list(dependencies or [])
289
+ self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
290
+ # if a "form feed" character (page break) is found in the description text,
291
+ # truncate description text to the content preceding the first "form feed"
292
+ self.description = self.description.split("\f")[0].strip()
293
+ response_fields = {}
294
+ for additional_status_code, response in self.responses.items():
295
+ assert isinstance(response, dict), "An additional response must be a dict"
296
+ model = response.get("model")
297
+ if model:
298
+ assert is_body_allowed_for_status_code(
299
+ additional_status_code
300
+ ), f"Status code {additional_status_code} must not have a response body"
301
+ response_name = f"Response_{additional_status_code}_{self.unique_id}"
302
+ response_field = create_response_field(name=response_name, type_=model)
303
+ response_fields[additional_status_code] = response_field
304
+ if response_fields:
305
+ self.response_fields: Dict[Union[int, str], ModelField] = response_fields
306
+ else:
307
+ self.response_fields = {}
308
+
309
+ assert callable(endpoint), "An endpoint must be a callable"
310
+ self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
311
+ for depends in self.dependencies[::-1]:
312
+ self.dependant.dependencies.insert(
313
+ 0,
314
+ get_parameterless_sub_dependant(depends=depends, path=self.path_format),
315
+ )
316
+ self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
317
+ # 增加参数
318
+ self.app = request_response(self.get_route_handler({
319
+ "summary": self.summary,
320
+ "description": self.description,
321
+ "name": self.name
322
+ }))
323
+
324
+ def get_route_handler(self, customize_para: Dict = None) -> Callable[[Request], Coroutine[Any, Any, Response]]:
325
+ return get_request_handler(
326
+ dependant=self.dependant,
327
+ body_field=self.body_field,
328
+ status_code=self.status_code,
329
+ response_class=self.response_class,
330
+ response_field=self.secure_cloned_response_field,
331
+ response_model_include=self.response_model_include,
332
+ response_model_exclude=self.response_model_exclude,
333
+ response_model_by_alias=self.response_model_by_alias,
334
+ response_model_exclude_unset=self.response_model_exclude_unset,
335
+ response_model_exclude_defaults=self.response_model_exclude_defaults,
336
+ response_model_exclude_none=self.response_model_exclude_none,
337
+ dependency_overrides_provider=self.dependency_overrides_provider,
338
+ customize_para=customize_para, # 增加
339
+ )
340
+
341
+
342
+ fastapi_routing.get_request_handler = get_request_handler
343
+ fastapi_routing.APIRoute.__init__ = APIRoute.__init__
344
+ fastapi_routing.APIRoute.get_route_handler = APIRoute.get_route_handler
File without changes
@@ -0,0 +1,56 @@
1
+ import datetime
2
+ import time
3
+ from typing import Optional
4
+ from tortoise import fields, models, BaseDBAsyncClient
5
+
6
+ from fastgenerateapi import my_fields
7
+
8
+
9
+ class PrimaryKeyMixin(models.Model):
10
+ id: str = my_fields.PrimaryKeyField(pk=True)
11
+
12
+ class Meta:
13
+ abstract = True
14
+
15
+ def __str__(self) -> str:
16
+ return f"{self.id}"
17
+
18
+
19
+ class TimestampMixin(models.Model):
20
+ created_at: Optional[datetime.datetime] = fields.DatetimeField(null=True, auto_now_add=True, description="创建时间")
21
+ updated_at: Optional[datetime.datetime] = fields.DatetimeField(null=True, auto_now=True, description="更新时间")
22
+
23
+ class Meta:
24
+ abstract = True
25
+
26
+
27
+ class BaseDeleteMixin(models.Model):
28
+ deleted_at: Optional[int] = my_fields.SoftDeleteField()
29
+
30
+ async def delete(self, using_db: Optional[BaseDBAsyncClient] = None) -> None:
31
+ self.deleted_at = int(time.time() * 1000)
32
+ await self.save(using_db=using_db)
33
+
34
+ class Meta:
35
+ abstract = True
36
+
37
+
38
+ class BaseActiveMixin(models.Model):
39
+ is_active: Optional[bool] = fields.BooleanField(null=True, default=True, description="数据是否有效")
40
+
41
+ async def delete(self, using_db: Optional[BaseDBAsyncClient] = None) -> None:
42
+ self.is_active = False
43
+ await self.save(using_db=using_db)
44
+
45
+ class Meta:
46
+ abstract = True
47
+
48
+
49
+ class TortoiseOrmAbstractModel(PrimaryKeyMixin, TimestampMixin, BaseDeleteMixin):
50
+ ...
51
+
52
+ class PydanticMeta:
53
+ exclude = ["delete_at"]
54
+
55
+ class Meta:
56
+ abstract = True
@@ -34,9 +34,8 @@ class IntEnumField(IntEnumFieldInstance):
34
34
  """
35
35
 
36
36
  def __init__(self, enum_list: List[any], **kwargs: Any) -> None:
37
- kwargs.setdefault("description", self.get_description(enum_list))
38
37
  self.enum_list = enum_list
39
- self.description = kwargs.get("description")
38
+ self.description = kwargs.get("description", "") + " >> " + self.get_description(enum_list)
40
39
  super().__init__(enum_type=self.create_enum(enum_list), **kwargs)
41
40
 
42
41
  @property
@@ -85,7 +84,8 @@ class IntEnumField(IntEnumFieldInstance):
85
84
  # 返回创建的枚举类
86
85
  return enum_class
87
86
 
88
- def number_to_words(self, num):
87
+ @classmethod
88
+ def number_to_words(cls, num):
89
89
  # 定义数字到单词的映射
90
90
  ones = ['', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
91
91
  'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen']
@@ -108,7 +108,7 @@ class IntEnumField(IntEnumFieldInstance):
108
108
  if num < 1000:
109
109
  hundred_digit = num // 100
110
110
  remaining = num % 100
111
- return ones[hundred_digit] + '_hundred_' + self.number_to_words(remaining)
111
+ return ones[hundred_digit] + '_hundred_' + cls.number_to_words(remaining)
112
112
 
113
113
  # 对于大于1000的数字,可以进一步扩展这个函数来处理
114
114
  # 但请注意,标准的英文数字读法会变得复杂,涉及"thousand", "million", "billion"等词
@@ -116,7 +116,7 @@ class IntEnumField(IntEnumFieldInstance):
116
116
  # 这里仅处理到9999,如果需要处理更大的数字,请继续扩展这个函数
117
117
  thousand_digit = num // 1000
118
118
  remaining = num % 1000
119
- return self.number_to_words(thousand_digit) + '_thousand_' + self.number_to_words(remaining)
119
+ return cls.number_to_words(thousand_digit) + '_thousand_' + cls.number_to_words(remaining)
120
120
 
121
121
 
122
122
  class IntEnumClass:
@@ -0,0 +1,60 @@
1
+ import datetime
2
+ import re
3
+
4
+ from tortoise.validators import Validator
5
+
6
+ from fastgenerateapi.api_view.mixin.response_mixin import ResponseMixin
7
+
8
+
9
+ class BlankStringValidator(Validator):
10
+ """
11
+ 不允许空字符串校验
12
+ """
13
+ def __init__(self, description: str):
14
+ self.description = description
15
+
16
+ def __call__(self, value: str):
17
+ if isinstance(value, str):
18
+ value = value.strip()
19
+ if not value:
20
+ return ResponseMixin.error(status_code=422, message=f"{self.description}不能为空")
21
+
22
+
23
+ class IDCardValidator(Validator):
24
+ """
25
+ 身份证校验
26
+ """
27
+
28
+ def __call__(self, value: str):
29
+ if len(value) == 15:
30
+ return value
31
+ if len(value) != 18:
32
+ return ResponseMixin.error(message="身份证输入错误")
33
+ this_year = datetime.datetime.now().year
34
+ if int(value[6:10]) < 1000 or int(value[6:10]) > this_year:
35
+ return ResponseMixin.error(message="身份证输入错误")
36
+ if int(value[10:12]) < 1 or int(value[10:12]) > 12 or int(value[12:14]) < 1 or int(value[12:14]) > 31:
37
+ return ResponseMixin.error(message="身份证输入错误")
38
+ arg_list = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
39
+ check_list = ["1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2"]
40
+ sum_num = 0
41
+ for index, num in enumerate(value[:-1]):
42
+ sum_num += arg_list[index] * int(num)
43
+ if value[-1] != check_list[sum_num % 11]:
44
+ return ResponseMixin.error(message=f"身份证: {value} 输入错误")
45
+ return value
46
+
47
+
48
+ class PhoneValidator(Validator):
49
+ """
50
+ 手机号码校验
51
+ """
52
+
53
+ def __call__(self, value: str):
54
+ ret = re.compile('^(?:(?:\+|00)86)?1[3-9]\d{9}$')
55
+ res = ret.match(value)
56
+ if res:
57
+ return res.group()
58
+ return ResponseMixin.error(message="手机号码校验不通过")
59
+
60
+