fastgenerateapi 0.0.28__py2.py3-none-any.whl → 1.1.7__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.
- fastgenerateapi/__init__.py +2 -2
- fastgenerateapi/__version__.py +1 -1
- fastgenerateapi/api_view/base_view.py +17 -7
- fastgenerateapi/api_view/create_view.py +1 -1
- fastgenerateapi/api_view/delete_filter_view.py +1 -1
- fastgenerateapi/api_view/delete_tree_view.py +3 -3
- fastgenerateapi/api_view/delete_view.py +3 -3
- fastgenerateapi/api_view/get_all_view.py +10 -8
- fastgenerateapi/api_view/get_one_view.py +1 -1
- fastgenerateapi/api_view/get_relation_view.py +1 -1
- fastgenerateapi/api_view/get_tree_view.py +1 -1
- fastgenerateapi/api_view/mixin/base_mixin.py +11 -7
- fastgenerateapi/api_view/mixin/dbmodel_mixin.py +30 -20
- fastgenerateapi/api_view/mixin/response_mixin.py +68 -38
- fastgenerateapi/api_view/mixin/tool_mixin.py +1 -357
- fastgenerateapi/api_view/mixin/utils/__init__.py +0 -0
- fastgenerateapi/api_view/mixin/utils/docx_util.py +399 -0
- fastgenerateapi/api_view/mixin/utils/file_util.py +30 -0
- fastgenerateapi/api_view/mixin/utils/pdf_util.py +76 -0
- fastgenerateapi/api_view/mixin/utils/xlsx_util.py +336 -0
- fastgenerateapi/api_view/mixin/utils/zip_util.py +50 -0
- fastgenerateapi/api_view/switch_view.py +2 -2
- fastgenerateapi/api_view/update_relation_view.py +3 -3
- fastgenerateapi/api_view/update_view.py +1 -1
- fastgenerateapi/cache/cache_decorator.py +1 -1
- fastgenerateapi/controller/filter_controller.py +68 -26
- fastgenerateapi/controller/router_controller.py +9 -9
- fastgenerateapi/controller/rpc_controller.py +1 -1
- fastgenerateapi/controller/ws_controller.py +1 -1
- fastgenerateapi/deps/filter_params_deps.py +34 -4
- fastgenerateapi/deps/paginator_deps.py +4 -4
- fastgenerateapi/deps/tree_params_deps.py +4 -4
- fastgenerateapi/fastapi_utils/__init__.py +0 -0
- fastgenerateapi/fastapi_utils/all.py +5 -0
- fastgenerateapi/fastapi_utils/param_utils.py +37 -0
- fastgenerateapi/fastapi_utils/response_utils.py +344 -0
- fastgenerateapi/model/__init__.py +0 -0
- fastgenerateapi/model/base_model.py +56 -0
- fastgenerateapi/my_fields/enum_field.py +5 -5
- fastgenerateapi/my_fields/validator.py +60 -0
- fastgenerateapi/pydantic_utils/base_model.py +46 -20
- fastgenerateapi/pydantic_utils/base_settings.py +16 -0
- fastgenerateapi/pydantic_utils/json_encoders.py +2 -1
- fastgenerateapi/schemas_factory/common_function.py +1 -1
- fastgenerateapi/schemas_factory/common_schema_factory.py +4 -4
- fastgenerateapi/schemas_factory/create_schema_factory.py +4 -4
- fastgenerateapi/schemas_factory/filter_schema_factory.py +6 -6
- fastgenerateapi/schemas_factory/get_all_schema_factory.py +5 -5
- fastgenerateapi/schemas_factory/get_one_schema_factory.py +4 -3
- fastgenerateapi/schemas_factory/get_relation_schema_factory.py +3 -3
- fastgenerateapi/schemas_factory/get_tree_schema_factory.py +3 -3
- fastgenerateapi/schemas_factory/response_factory.py +3 -3
- fastgenerateapi/schemas_factory/sql_get_all_schema_factory.py +3 -3
- fastgenerateapi/schemas_factory/update_schema_factory.py +4 -4
- fastgenerateapi/settings/__init__.py +6 -0
- fastgenerateapi/settings/all_settings.py +91 -0
- fastgenerateapi/settings/{settings.py → app_settings.py} +9 -9
- fastgenerateapi/settings/db_settings.py +69 -0
- fastgenerateapi/settings/file_settings.py +24 -0
- fastgenerateapi/settings/jwt_settings.py +23 -0
- fastgenerateapi/settings/otlp_settings.py +69 -0
- fastgenerateapi/settings/redis_settings.py +16 -0
- fastgenerateapi/settings/sms_settings.py +25 -0
- fastgenerateapi/settings/system_settings.py +30 -0
- fastgenerateapi/utils/auto_discover.py +61 -0
- fastgenerateapi/utils/file_utils.py +76 -0
- fastgenerateapi/utils/pwd_utils.py +49 -0
- fastgenerateapi/utils/ramdom_utils.py +48 -0
- fastgenerateapi/utils/snowflake.py +23 -20
- fastgenerateapi/utils/str_util.py +120 -0
- fastgenerateapi/utils/swagger_to_js.py +26 -0
- {fastgenerateapi-0.0.28.dist-info → fastgenerateapi-1.1.7.dist-info}/METADATA +61 -24
- fastgenerateapi-1.1.7.dist-info/RECORD +109 -0
- {fastgenerateapi-0.0.28.dist-info → fastgenerateapi-1.1.7.dist-info}/WHEEL +1 -1
- {fastgenerateapi-0.0.28.dist-info → fastgenerateapi-1.1.7.dist-info}/top_level.txt +1 -0
- script/__init__.py +2 -0
- fastgenerateapi/settings/register_settings.py +0 -6
- fastgenerateapi/utils/parse_str.py +0 -36
- fastgenerateapi-0.0.28.dist-info/RECORD +0 -82
- {fastgenerateapi-0.0.28.dist-info → fastgenerateapi-1.1.7.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
|
|
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(
|
|
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,
|
|
8
|
-
from fastgenerateapi.settings.
|
|
9
|
-
from fastgenerateapi.utils.
|
|
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__=
|
|
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.
|
|
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,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
|
-
|
|
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_' +
|
|
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
|
|
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
|
+
|