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.
- 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.6.dist-info}/METADATA +61 -24
- fastgenerateapi-1.1.6.dist-info/RECORD +109 -0
- {fastgenerateapi-0.0.28.dist-info → fastgenerateapi-1.1.6.dist-info}/WHEEL +1 -1
- {fastgenerateapi-0.0.28.dist-info → fastgenerateapi-1.1.6.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.6.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import io
|
|
3
|
+
import operator
|
|
4
|
+
from tempfile import NamedTemporaryFile
|
|
5
|
+
from typing import List, Union, Optional, Dict, Type
|
|
6
|
+
|
|
7
|
+
import openpyxl
|
|
8
|
+
from fastapi import UploadFile
|
|
9
|
+
from openpyxl.styles import Alignment, PatternFill
|
|
10
|
+
from openpyxl.styles.colors import COLOR_INDEX, Color
|
|
11
|
+
from openpyxl.utils import get_column_letter
|
|
12
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
13
|
+
from starlette._utils import is_async_callable
|
|
14
|
+
from starlette.responses import StreamingResponse, JSONResponse, FileResponse
|
|
15
|
+
from tortoise import Model
|
|
16
|
+
from pydantic import ValidationError
|
|
17
|
+
|
|
18
|
+
from fastgenerateapi import BaseModel, BaseView
|
|
19
|
+
from fastgenerateapi.api_view.mixin.response_mixin import ResponseMixin
|
|
20
|
+
from fastgenerateapi.schemas_factory.common_schema_factory import common_schema_factory
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class XlsxUtil:
|
|
24
|
+
default_align = Alignment(
|
|
25
|
+
horizontal='center',
|
|
26
|
+
vertical='center',
|
|
27
|
+
text_rotation=0,
|
|
28
|
+
wrap_text=True,
|
|
29
|
+
shrink_to_fit=True,
|
|
30
|
+
indent=0,
|
|
31
|
+
)
|
|
32
|
+
default_fill = PatternFill(
|
|
33
|
+
start_color=Color(COLOR_INDEX[44]),
|
|
34
|
+
end_color=Color(COLOR_INDEX[44]),
|
|
35
|
+
fill_type='solid'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def write_headers(sh: Worksheet, headers: List[str]) -> List[int]:
|
|
40
|
+
"""
|
|
41
|
+
写入第一行信息
|
|
42
|
+
:return:
|
|
43
|
+
"""
|
|
44
|
+
col_max_len_list = []
|
|
45
|
+
sh.row_dimensions[1].height = 26
|
|
46
|
+
for col, header in enumerate(headers, 1):
|
|
47
|
+
sh.cell(1, col).value = header
|
|
48
|
+
sh.cell(1, col).alignment = XlsxUtil.default_align
|
|
49
|
+
sh.cell(1, col).fill = XlsxUtil.default_fill
|
|
50
|
+
sh.cell(1, col).alignment = XlsxUtil.default_align
|
|
51
|
+
col_max_len_list.append(len(header.encode('gb18030')))
|
|
52
|
+
|
|
53
|
+
return col_max_len_list
|
|
54
|
+
|
|
55
|
+
@staticmethod
|
|
56
|
+
def write_content(model_list: List[Model]):
|
|
57
|
+
"""
|
|
58
|
+
填写内容部分
|
|
59
|
+
:return:
|
|
60
|
+
"""
|
|
61
|
+
# 跳过标题,从第二行开始写入
|
|
62
|
+
for row, model in enumerate(model_list, 2):
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def adaptive_format(sh: Worksheet, col_max_len_list: List[int], height_num: int):
|
|
69
|
+
"""
|
|
70
|
+
自适应宽度
|
|
71
|
+
:return:
|
|
72
|
+
"""
|
|
73
|
+
# 设置自适应列宽
|
|
74
|
+
for i, col_max_len in enumerate(col_max_len_list, 1):
|
|
75
|
+
# 256*字符数得到excel列宽,为了不显得特别紧凑添加两个字符宽度
|
|
76
|
+
max_width = col_max_len + 4
|
|
77
|
+
if max_width > 256:
|
|
78
|
+
max_width = 256
|
|
79
|
+
sh.column_dimensions[get_column_letter(i)].width = max_width
|
|
80
|
+
for y in range(2, height_num + 2):
|
|
81
|
+
sh.row_dimensions[y].height = 18
|
|
82
|
+
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
async def export_xlsx(
|
|
86
|
+
self,
|
|
87
|
+
model_list: List[Model],
|
|
88
|
+
headers: List[str],
|
|
89
|
+
fields: List[str],
|
|
90
|
+
fields_handler: dict,
|
|
91
|
+
file_save_path: Optional[str] = None,
|
|
92
|
+
# rpc_param: Union[Dict[str, Dict[str, List[str]]], Type[RPCParam], None] = None,
|
|
93
|
+
title: str = None,
|
|
94
|
+
) -> StreamingResponse:
|
|
95
|
+
wb = openpyxl.Workbook()
|
|
96
|
+
col_max_len_list = []
|
|
97
|
+
|
|
98
|
+
def write(sh, row, col, value):
|
|
99
|
+
sh.cell(row, col).value = value
|
|
100
|
+
sh.cell(row, col).alignment = XlsxUtil.default_align
|
|
101
|
+
if col_max_len_list[col - 1] < len(str(value).encode('gb18030')):
|
|
102
|
+
col_max_len_list[col - 1] = len(str(value).encode('gb18030'))
|
|
103
|
+
|
|
104
|
+
start_row = 1
|
|
105
|
+
try:
|
|
106
|
+
sh = wb.active
|
|
107
|
+
sh.title = title if title else f'{self.model_class._meta.table_description}'
|
|
108
|
+
|
|
109
|
+
col_max_len_list = XlsxUtil.write_headers(sh, headers)
|
|
110
|
+
|
|
111
|
+
for row, model in enumerate(model_list, start_row + 1):
|
|
112
|
+
model = await BaseView.getattr_model(model=model, fields=fields)
|
|
113
|
+
# model = await self.setattr_model_rpc(self.model_class, model, rpc_param)
|
|
114
|
+
|
|
115
|
+
for col, field in enumerate(fields, 1):
|
|
116
|
+
info = getattr(model, field, "")
|
|
117
|
+
handler = fields_handler.get(field)
|
|
118
|
+
if handler and hasattr(handler, "__call__"):
|
|
119
|
+
if is_async_callable(handler):
|
|
120
|
+
info = await handler(info)
|
|
121
|
+
else:
|
|
122
|
+
info = handler(info)
|
|
123
|
+
write(sh, row, col, info)
|
|
124
|
+
|
|
125
|
+
XlsxUtil.adaptive_format(sh, col_max_len_list, len(model_list))
|
|
126
|
+
finally:
|
|
127
|
+
if file_save_path:
|
|
128
|
+
wb.save(file_save_path)
|
|
129
|
+
return ResponseMixin.success(msg="请求成功")
|
|
130
|
+
bytes_io = io.BytesIO()
|
|
131
|
+
wb.save(bytes_io)
|
|
132
|
+
bytes_io.seek(0)
|
|
133
|
+
|
|
134
|
+
return ResponseMixin.stream(bytes_io, is_xlsx=True)
|
|
135
|
+
|
|
136
|
+
async def import_xlsx(
|
|
137
|
+
self,
|
|
138
|
+
file: UploadFile,
|
|
139
|
+
headers: List[str],
|
|
140
|
+
# [
|
|
141
|
+
# "name",
|
|
142
|
+
# ("is_male", {"男": True, "女": False} 或者 方法, {"额外字段": 方法}, ...),
|
|
143
|
+
# ]
|
|
144
|
+
# 方法(默认传excel的值)
|
|
145
|
+
fields: List[Union[str, dict, tuple, list]],
|
|
146
|
+
combine_fields: Optional[List[Dict[str, any]]] = None,
|
|
147
|
+
model_class: Optional[Type[Model]] = None,
|
|
148
|
+
create_schema: Optional[Type[BaseModel]] = None,
|
|
149
|
+
# storage_path: Union[str, Path],
|
|
150
|
+
# rpc_param: Union[Dict[str, Dict[str, List[Union[str, tuple]]]], Type[RPCParam]] = None,
|
|
151
|
+
modules: str = "openpyxl",
|
|
152
|
+
) -> JSONResponse:
|
|
153
|
+
"""
|
|
154
|
+
fields: 方法(默认传excel的值)
|
|
155
|
+
例如:
|
|
156
|
+
[
|
|
157
|
+
"name", # 传入值是 name 字段的值
|
|
158
|
+
("is_male", {"男": True, "女": False} 或者 方法, {"额外字段": 方法}, ...),
|
|
159
|
+
# 值 "男" 获取为bool值,不在字典里为None, 页可以自定义 同步或异步方法 获取值
|
|
160
|
+
]
|
|
161
|
+
"""
|
|
162
|
+
limit_modules = ["openpyxl"]
|
|
163
|
+
if modules not in limit_modules:
|
|
164
|
+
return ResponseMixin.error(msg=f"export xlsx modules only import {'、'.join(limit_modules)}")
|
|
165
|
+
|
|
166
|
+
if not file:
|
|
167
|
+
return ResponseMixin.fail(msg=f"请先选择合适的文件")
|
|
168
|
+
|
|
169
|
+
if not model_class:
|
|
170
|
+
model_class = self.model_class
|
|
171
|
+
if not create_schema:
|
|
172
|
+
create_schema = common_schema_factory(model_class, name=f"{model_class.__name__}ExcelImportSchema")
|
|
173
|
+
|
|
174
|
+
with NamedTemporaryFile() as tmp2:
|
|
175
|
+
tmp2.write(await file.read())
|
|
176
|
+
try:
|
|
177
|
+
wb = importlib.import_module(modules).load_workbook(tmp2, read_only=True, data_only=True)
|
|
178
|
+
except Exception:
|
|
179
|
+
return ResponseMixin.error(msg=f"please pip install {modules}")
|
|
180
|
+
|
|
181
|
+
try:
|
|
182
|
+
ws = wb.active
|
|
183
|
+
|
|
184
|
+
header_row = ws[1]
|
|
185
|
+
header_list = []
|
|
186
|
+
for msg in header_row:
|
|
187
|
+
header_list.append(str(msg.value).replace(" ", ''))
|
|
188
|
+
|
|
189
|
+
if len(header_list) != len(headers):
|
|
190
|
+
return ResponseMixin.fail(message="文件首行长度校验错误")
|
|
191
|
+
|
|
192
|
+
if not operator.eq(header_list, headers):
|
|
193
|
+
return ResponseMixin.fail(message="文件首行内容校验错误")
|
|
194
|
+
|
|
195
|
+
# if ws.max_row < 2:
|
|
196
|
+
# return ResponseMixin.fail(msg="导入数据不能为空")
|
|
197
|
+
|
|
198
|
+
create_list = []
|
|
199
|
+
effective_row = 0
|
|
200
|
+
for row in range(2, ws.max_row + 1):
|
|
201
|
+
data = {}
|
|
202
|
+
# data_schema = {}
|
|
203
|
+
row_data = ws[row]
|
|
204
|
+
if await self.excel_row_is_empty(row_data):
|
|
205
|
+
continue
|
|
206
|
+
effective_row += 1
|
|
207
|
+
for col, field_input in enumerate(fields):
|
|
208
|
+
if type(field_input) in [str, int]:
|
|
209
|
+
data[field_input] = row_data[col].value
|
|
210
|
+
# data_schema[field_input] = (type(row_data[col].value), ...)
|
|
211
|
+
|
|
212
|
+
if type(field_input) == tuple or type(field_input) == list:
|
|
213
|
+
key = field_input[0]
|
|
214
|
+
val = field_input[1]
|
|
215
|
+
required_doc = {}
|
|
216
|
+
if len(field_input) > 2:
|
|
217
|
+
required_doc = field_input[2]
|
|
218
|
+
if required_doc == "required":
|
|
219
|
+
required_doc = {"required": True}
|
|
220
|
+
if type(val) == dict:
|
|
221
|
+
model_val = val.get(row_data[col].value)
|
|
222
|
+
if not model_val and required_doc.get("required"):
|
|
223
|
+
return ResponseMixin.fail(
|
|
224
|
+
msg=required_doc.get("error",
|
|
225
|
+
"") or f"第{row}行{self.get_field_description(key)}不能为空")
|
|
226
|
+
data[key] = model_val
|
|
227
|
+
# data_schema[key] = (type(model_val), ...)
|
|
228
|
+
elif hasattr(val, "__call__"):
|
|
229
|
+
if is_async_callable(val):
|
|
230
|
+
model_val = await val(row_data[col].value)
|
|
231
|
+
else:
|
|
232
|
+
model_val = val(row_data[col].value)
|
|
233
|
+
data[key] = model_val
|
|
234
|
+
# data_schema[key] = (type(model_val), ...)
|
|
235
|
+
else:
|
|
236
|
+
raise NotImplemented
|
|
237
|
+
else:
|
|
238
|
+
raise NotImplemented
|
|
239
|
+
for combine_field in combine_fields:
|
|
240
|
+
field = combine_field.get("field", None)
|
|
241
|
+
value = combine_field.get("value", None)
|
|
242
|
+
function = combine_field.get("function", None)
|
|
243
|
+
args = combine_field.get("args", None)
|
|
244
|
+
if not field or (not function and not value):
|
|
245
|
+
continue
|
|
246
|
+
if value:
|
|
247
|
+
data[field] = value
|
|
248
|
+
else:
|
|
249
|
+
if not args:
|
|
250
|
+
if is_async_callable(function):
|
|
251
|
+
model_val = await function()
|
|
252
|
+
else:
|
|
253
|
+
model_val = function()
|
|
254
|
+
else:
|
|
255
|
+
args_list = []
|
|
256
|
+
for arg in args:
|
|
257
|
+
args_list.append(data.get(arg, ""))
|
|
258
|
+
if is_async_callable(function):
|
|
259
|
+
model_val = await function(*args_list)
|
|
260
|
+
else:
|
|
261
|
+
model_val = function(*args_list)
|
|
262
|
+
data[field] = model_val
|
|
263
|
+
try:
|
|
264
|
+
create_obj = model_class(**create_schema(**data).dict(exclude_unset=True))
|
|
265
|
+
except ValidationError as e:
|
|
266
|
+
error_field = e.errors()[0].get('loc')[0]
|
|
267
|
+
description = self.get_field_description(error_field)
|
|
268
|
+
if not data.get(error_field):
|
|
269
|
+
return ResponseMixin.fail(message=f"第{row}行{description}不能为空")
|
|
270
|
+
return ResponseMixin.fail(message=f"第{row}行{description}填写错误")
|
|
271
|
+
await self.check_unique_field(create_obj, model_class=model_class)
|
|
272
|
+
create_list.append(create_obj)
|
|
273
|
+
|
|
274
|
+
await model_class.bulk_create(create_list)
|
|
275
|
+
finally:
|
|
276
|
+
wb.close()
|
|
277
|
+
if effective_row == 0:
|
|
278
|
+
return ResponseMixin.fail(message="导入数据不能为空")
|
|
279
|
+
return ResponseMixin.success(msg='创建成功')
|
|
280
|
+
|
|
281
|
+
@staticmethod
|
|
282
|
+
async def excel_row_is_empty(row_list) -> bool:
|
|
283
|
+
is_empty = True
|
|
284
|
+
for row in row_list:
|
|
285
|
+
if row.value is not None:
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
return is_empty
|
|
289
|
+
|
|
290
|
+
async def excel_model(
|
|
291
|
+
self,
|
|
292
|
+
headers: List[str] = None,
|
|
293
|
+
model_class: Optional[Model] = None,
|
|
294
|
+
excel_model_path: Optional[str] = None,
|
|
295
|
+
modules: str = "openpyxl",
|
|
296
|
+
title: Optional[str] = None,
|
|
297
|
+
) -> Union[FileResponse, StreamingResponse]:
|
|
298
|
+
if excel_model_path:
|
|
299
|
+
return FileResponse(
|
|
300
|
+
path=excel_model_path,
|
|
301
|
+
filename="导入模板.xlsx",
|
|
302
|
+
media_type="xlsx",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
limit_modules = ["openpyxl", "xlsxwriter"]
|
|
306
|
+
if modules not in limit_modules:
|
|
307
|
+
return ResponseMixin.error(msg=f"export xlsx modules only import {'、'.join(limit_modules)}")
|
|
308
|
+
try:
|
|
309
|
+
wb = importlib.import_module(modules).Workbook()
|
|
310
|
+
except Exception:
|
|
311
|
+
return ResponseMixin.error(msg=f"please pip install {modules}")
|
|
312
|
+
if modules == "openpyxl":
|
|
313
|
+
def write(sh, row, col, value):
|
|
314
|
+
sh.cell(row, col).value = value
|
|
315
|
+
|
|
316
|
+
start_col = 1
|
|
317
|
+
start_row = 1
|
|
318
|
+
else:
|
|
319
|
+
def write(sh, row, col, value):
|
|
320
|
+
sh.write(row, col, value)
|
|
321
|
+
|
|
322
|
+
start_col = 0
|
|
323
|
+
start_row = 0
|
|
324
|
+
try:
|
|
325
|
+
sh = wb.active
|
|
326
|
+
sh.title = title if title else f'{model_class._meta.table_description}'
|
|
327
|
+
|
|
328
|
+
for col, header in enumerate(headers, start_col):
|
|
329
|
+
write(sh, start_row, col, header)
|
|
330
|
+
|
|
331
|
+
finally:
|
|
332
|
+
bytes_io = io.BytesIO()
|
|
333
|
+
wb.save(bytes_io)
|
|
334
|
+
bytes_io.seek(0)
|
|
335
|
+
|
|
336
|
+
return ResponseMixin.stream(bytes_io, is_xlsx=True)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import os
|
|
3
|
+
import zipfile
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ZipUtil:
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def folder_to_zip(folder_path: str, zip_path: Union[str, io.BytesIO]):
|
|
11
|
+
"""
|
|
12
|
+
把文件夹打包成zip
|
|
13
|
+
:param folder_path: 文件夹路径
|
|
14
|
+
:param zip_path: 打包后zip路径
|
|
15
|
+
:return:
|
|
16
|
+
"""
|
|
17
|
+
# 创建一个 ZIP 文件对象,使用 'w' 模式表示写入
|
|
18
|
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
19
|
+
# 遍历文件夹及其子文件夹
|
|
20
|
+
for root, dirs, files in os.walk(folder_path):
|
|
21
|
+
# 处理当前目录,将其添加到压缩包中,实现打包空文件夹
|
|
22
|
+
relative_path = os.path.relpath(root, folder_path)
|
|
23
|
+
if relative_path and relative_path != ".":
|
|
24
|
+
# 确保相对路径不是空字符串,避免将根目录重复添加
|
|
25
|
+
zip_info = zipfile.ZipInfo(relative_path + '/')
|
|
26
|
+
zipf.writestr(zip_info, '')
|
|
27
|
+
# 处理文件
|
|
28
|
+
for file in files:
|
|
29
|
+
# 获取文件的完整路径
|
|
30
|
+
file_path = os.path.join(root, file)
|
|
31
|
+
# 获取文件相对于文件夹的相对路径
|
|
32
|
+
relative_path = os.path.relpath(file_path, folder_path)
|
|
33
|
+
# 将文件添加到 ZIP 文件中
|
|
34
|
+
zipf.write(file_path, relative_path)
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def folder_to_zip_bytes_io(folder_path: str):
|
|
38
|
+
"""
|
|
39
|
+
把文件夹打包zip后转换成io格式
|
|
40
|
+
:param folder_path: 文件夹路径
|
|
41
|
+
:return:
|
|
42
|
+
"""
|
|
43
|
+
bytes_io = io.BytesIO()
|
|
44
|
+
ZipUtil.folder_to_zip(folder_path, bytes_io)
|
|
45
|
+
bytes_io.seek(0)
|
|
46
|
+
return bytes_io
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
@@ -6,9 +6,9 @@ from starlette.responses import JSONResponse
|
|
|
6
6
|
|
|
7
7
|
from fastgenerateapi.api_view.base_view import BaseView
|
|
8
8
|
from fastgenerateapi.schemas_factory import response_factory, get_one_schema_factory
|
|
9
|
-
from fastgenerateapi.settings.
|
|
9
|
+
from fastgenerateapi.settings.all_settings import settings
|
|
10
10
|
from fastgenerateapi.utils.exception import NOT_FOUND
|
|
11
|
-
from fastgenerateapi.utils.
|
|
11
|
+
from fastgenerateapi.utils.str_util import parse_str_to_bool
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class SwitchView(BaseView):
|
|
@@ -10,9 +10,9 @@ from tortoise.transactions import atomic
|
|
|
10
10
|
from fastgenerateapi.api_view.base_view import BaseView
|
|
11
11
|
from fastgenerateapi.api_view.mixin.save_mixin import SaveMixin
|
|
12
12
|
from fastgenerateapi.data_type.data_type import DEPENDENCIES
|
|
13
|
-
from fastgenerateapi.pydantic_utils.base_model import
|
|
13
|
+
from fastgenerateapi.pydantic_utils.base_model import IdList
|
|
14
14
|
from fastgenerateapi.schemas_factory import response_factory
|
|
15
|
-
from fastgenerateapi.settings.
|
|
15
|
+
from fastgenerateapi.settings.all_settings import settings
|
|
16
16
|
from fastgenerateapi.utils.exception import NOT_FOUND
|
|
17
17
|
|
|
18
18
|
|
|
@@ -20,7 +20,7 @@ class UpdateRelationView(BaseView, SaveMixin):
|
|
|
20
20
|
|
|
21
21
|
path_id_name: str
|
|
22
22
|
relation_id_name: str
|
|
23
|
-
update_relation_schema: Optional[Type[BaseModel]] =
|
|
23
|
+
update_relation_schema: Optional[Type[BaseModel]] = IdList
|
|
24
24
|
update_relation_route: Union[bool, DEPENDENCIES] = True
|
|
25
25
|
"""
|
|
26
26
|
path_id_name: 路径id在模型中对应的字段名
|
|
@@ -12,7 +12,7 @@ from fastgenerateapi.api_view.mixin.save_mixin import SaveMixin
|
|
|
12
12
|
from fastgenerateapi.data_type.data_type import DEPENDENCIES, CALLABLE
|
|
13
13
|
from fastgenerateapi.pydantic_utils.base_model import BaseModel
|
|
14
14
|
from fastgenerateapi.schemas_factory import update_schema_factory, get_one_schema_factory, response_factory
|
|
15
|
-
from fastgenerateapi.settings.
|
|
15
|
+
from fastgenerateapi.settings.all_settings import settings
|
|
16
16
|
from fastgenerateapi.utils.exception import NOT_FOUND
|
|
17
17
|
|
|
18
18
|
|
|
@@ -1,9 +1,50 @@
|
|
|
1
|
-
from
|
|
1
|
+
from datetime import datetime, date, time
|
|
2
|
+
from typing import Union, Any, Optional
|
|
2
3
|
|
|
3
4
|
from tortoise.expressions import Q
|
|
4
5
|
from tortoise.queryset import QuerySet
|
|
5
6
|
|
|
6
|
-
from fastgenerateapi.settings.
|
|
7
|
+
from fastgenerateapi.settings.all_settings import settings
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FilterUtils:
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def date_to_datetime_23(date_value: Union[None, str, date, datetime]) -> Optional[datetime]:
|
|
14
|
+
"""
|
|
15
|
+
使用场景:如创建时间筛选2025-01-01 至 2025-12-31;由于储存数据为datetime类型,会导致2025-12-31当天数据不包含
|
|
16
|
+
:param cls:
|
|
17
|
+
:param date_value:
|
|
18
|
+
:return:
|
|
19
|
+
"""
|
|
20
|
+
if date_value is None:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
# 如果 date_value 是字符串,尝试解析为日期
|
|
24
|
+
if isinstance(date_value, str):
|
|
25
|
+
try:
|
|
26
|
+
# 尝试解析为 date 类型,这里假设日期字符串的格式是 YYYY-MM-DD
|
|
27
|
+
parsed_date = datetime.strptime(date_value, '%Y-%m-%d').date()
|
|
28
|
+
except ValueError:
|
|
29
|
+
# 如果解析失败,则尝试解析为 datetime 类型(可能包含时间信息)
|
|
30
|
+
try:
|
|
31
|
+
parsed_datetime = datetime.strptime(date_value, '%Y-%m-%d %H:%M:%S')
|
|
32
|
+
parsed_date = parsed_datetime.date()
|
|
33
|
+
except ValueError:
|
|
34
|
+
# 如果仍然失败,则返回 None
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
# 如果 date_value 已经是 date 类型,直接使用
|
|
38
|
+
elif isinstance(date_value, date):
|
|
39
|
+
parsed_date = date_value
|
|
40
|
+
|
|
41
|
+
# 其他类型返回 None
|
|
42
|
+
else:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
# 将日期加上时间 "23:59:59" 转换为 datetime 类型
|
|
46
|
+
result_datetime = datetime.combine(parsed_date, datetime.min.time().replace(hour=23, minute=59, second=59))
|
|
47
|
+
return result_datetime
|
|
7
48
|
|
|
8
49
|
|
|
9
50
|
class BaseFilter:
|
|
@@ -14,37 +55,36 @@ class BaseFilter:
|
|
|
14
55
|
def __init__(self, filter_str: Union[str, tuple]):
|
|
15
56
|
"""
|
|
16
57
|
:param filter_str: Union[str, tuple]
|
|
58
|
+
当tuple时,第一个为str,后面参数无顺序和数量要求,可以是 类型、重命名字符串、用于修改传值的方法
|
|
17
59
|
example: name__contains or (create_at__gt, datetime) or (create_at__gt, datetime, create_time)
|
|
18
60
|
"""
|
|
19
61
|
field_type = str
|
|
20
62
|
model_field = filter_str
|
|
21
|
-
filter_field =
|
|
22
|
-
|
|
23
|
-
if settings.app_settings.FILTER_UNDERLINE_WHETHER_DOUBLE_TO_SINGLE and "__" in filter_str:
|
|
24
|
-
filter_field = filter_str.replace("__", "_")
|
|
63
|
+
filter_field = None
|
|
64
|
+
filter_func = None
|
|
25
65
|
# 判断filter表达式的类型
|
|
26
66
|
if isinstance(filter_str, tuple):
|
|
27
67
|
model_field = filter_str[0]
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
filter_field = filter_str[1]
|
|
68
|
+
filter_field = filter_str[0]
|
|
69
|
+
for f in filter_str[1:]:
|
|
70
|
+
if type(f) == type:
|
|
71
|
+
field_type = f
|
|
72
|
+
continue
|
|
73
|
+
if type(f) == str:
|
|
74
|
+
filter_field = f
|
|
75
|
+
continue
|
|
76
|
+
if callable(f):
|
|
77
|
+
filter_func = f
|
|
78
|
+
if not filter_field:
|
|
79
|
+
if settings.app_settings.FILTER_UNDERLINE_WHETHER_DOUBLE_TO_SINGLE:
|
|
80
|
+
filter_field = model_field.replace("__", "_")
|
|
81
|
+
else:
|
|
82
|
+
filter_field = model_field
|
|
44
83
|
|
|
84
|
+
self.field_type = field_type
|
|
45
85
|
self.model_field = model_field
|
|
46
86
|
self.filter_field = filter_field
|
|
47
|
-
self.
|
|
87
|
+
self.filter_func = filter_func
|
|
48
88
|
|
|
49
89
|
def generate_q(self, value: Union[str, list, bool]) -> Q:
|
|
50
90
|
"""
|
|
@@ -52,8 +92,8 @@ class BaseFilter:
|
|
|
52
92
|
:param value:
|
|
53
93
|
:return:
|
|
54
94
|
"""
|
|
55
|
-
if isinstance(value, str):
|
|
56
|
-
if value.upper() in ["NONE", "NULL", "NIL"
|
|
95
|
+
if isinstance(value, (str, datetime, date, time)):
|
|
96
|
+
if hasattr(value, "upper") and value.upper() in ["NONE", "NULL", "NIL"]:
|
|
57
97
|
return eval(f"Q({self.model_field}={None})")
|
|
58
98
|
return eval(f"Q({self.model_field}='{value}')")
|
|
59
99
|
return eval(f"Q({self.model_field}={value})")
|
|
@@ -65,6 +105,8 @@ class BaseFilter:
|
|
|
65
105
|
:param value:
|
|
66
106
|
:return:
|
|
67
107
|
"""
|
|
108
|
+
if self.filter_func:
|
|
109
|
+
value = self.filter_func(value)
|
|
68
110
|
queryset = queryset.filter(self.generate_q(value=value))
|
|
69
111
|
return queryset
|
|
70
112
|
|
|
@@ -90,7 +132,7 @@ class FilterController:
|
|
|
90
132
|
for k in values:
|
|
91
133
|
f = self.filter_map.get(k, None)
|
|
92
134
|
v = values[k]
|
|
93
|
-
if f is not None and (isinstance(v, bool)
|
|
135
|
+
if f is not None and (v or v == 0 or isinstance(v, bool)):
|
|
94
136
|
queryset = f.query(queryset=queryset, value=v)
|
|
95
137
|
|
|
96
138
|
return queryset
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import inspect
|
|
2
2
|
from typing import Union
|
|
3
3
|
|
|
4
|
-
from fastgenerateapi.settings.
|
|
4
|
+
from fastgenerateapi.settings.all_settings import settings
|
|
5
5
|
from pydantic import BaseModel
|
|
6
6
|
|
|
7
7
|
from fastgenerateapi.api_view.mixin.response_mixin import ResponseMixin
|
|
@@ -80,17 +80,17 @@ class RouterController:
|
|
|
80
80
|
else:
|
|
81
81
|
middle_field = "_".join(middle_list)
|
|
82
82
|
if method == "GET":
|
|
83
|
-
prefix_field = settings.app_settings.RESTFUL_GET_ROUTER_ADD_PREFIX
|
|
84
|
-
suffix_field = settings.app_settings.RESTFUL_GET_ROUTER_ADD_SUFFIX
|
|
83
|
+
prefix_field = settings.app_settings.RESTFUL_GET_ROUTER_ADD_PREFIX or ""
|
|
84
|
+
suffix_field = settings.app_settings.RESTFUL_GET_ROUTER_ADD_SUFFIX or ""
|
|
85
85
|
elif method == "POST":
|
|
86
|
-
prefix_field = settings.app_settings.RESTFUL_POST_ROUTER_ADD_PREFIX
|
|
87
|
-
suffix_field = settings.app_settings.RESTFUL_POST_ROUTER_ADD_SUFFIX
|
|
86
|
+
prefix_field = settings.app_settings.RESTFUL_POST_ROUTER_ADD_PREFIX or ""
|
|
87
|
+
suffix_field = settings.app_settings.RESTFUL_POST_ROUTER_ADD_SUFFIX or ""
|
|
88
88
|
elif method == "PUT":
|
|
89
|
-
prefix_field = settings.app_settings.RESTFUL_PUT_ROUTER_ADD_PREFIX
|
|
90
|
-
suffix_field = settings.app_settings.RESTFUL_PUT_ROUTER_ADD_SUFFIX
|
|
89
|
+
prefix_field = settings.app_settings.RESTFUL_PUT_ROUTER_ADD_PREFIX or ""
|
|
90
|
+
suffix_field = settings.app_settings.RESTFUL_PUT_ROUTER_ADD_SUFFIX or ""
|
|
91
91
|
elif method == "DELETE":
|
|
92
|
-
prefix_field = settings.app_settings.RESTFUL_DELETE_ROUTER_ADD_PREFIX
|
|
93
|
-
suffix_field = settings.app_settings.RESTFUL_DELETE_ROUTER_ADD_SUFFIX
|
|
92
|
+
prefix_field = settings.app_settings.RESTFUL_DELETE_ROUTER_ADD_PREFIX or ""
|
|
93
|
+
suffix_field = settings.app_settings.RESTFUL_DELETE_ROUTER_ADD_SUFFIX or ""
|
|
94
94
|
else:
|
|
95
95
|
prefix_field = ""
|
|
96
96
|
suffix_field = ""
|