bkflow-sdk 0.0.42__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 bkflow-sdk might be problematic. Click here for more details.

bkflow/client/core.py ADDED
@@ -0,0 +1,289 @@
1
+ """
2
+ Tencent is pleased to support the open source community by making 蓝鲸智云 - PaaS平台 (BlueKing - PaaS System) available.
3
+ Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved.
4
+ Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+ http://opensource.org/licenses/MIT
7
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8
+ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9
+ specific language governing permissions and limitations under the License.
10
+ """
11
+
12
+ import json
13
+ import logging
14
+
15
+ import requests
16
+ from django.conf import settings
17
+
18
+ from bkflow.client.apis.bkflow import CollectionsBKFlow
19
+ from bkflow.common.exceptions import APIException
20
+ from bkflow.common.loader import call_config_function
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ __all__ = [
25
+ "get_client_by_request",
26
+ "get_client_by_user",
27
+ ]
28
+
29
+ HEADER_BK_AUTHORIZATION = "X-Bkapi-Authorization"
30
+ DEFAULT_STAGE = getattr(settings, "BK_APIGW_STAGE_NAME", "prod")
31
+ AVAILABLE_COLLECTIONS = {
32
+ "bkflow": CollectionsBKFlow,
33
+ }
34
+
35
+
36
+ class BaseAPIClient:
37
+ """Base client class for api"""
38
+
39
+ def __init__(
40
+ self,
41
+ app_code=None,
42
+ app_secret=None,
43
+ headers=None,
44
+ common_args=None,
45
+ stage="prod",
46
+ timeout=None,
47
+ bk_apigw_ver="v3",
48
+ ):
49
+ """
50
+ :param str app_code: App code to use
51
+ :param str app_secret: App secret to use
52
+ :param dict headers: headers be sent to api
53
+ :param dict common_args: Args that will apply to every request
54
+ :param str stage: Stage for api gateway
55
+ :param int timeout: timeout for request
56
+ """
57
+
58
+ self.app_code = app_code or settings.APP_CODE
59
+ self.app_secret = app_secret or settings.SECRET_KEY
60
+ self.headers = headers or {}
61
+ self.common_args = common_args or {}
62
+ self.stage = stage
63
+ self.bk_apigw_ver = bk_apigw_ver
64
+ self.timeout = timeout
65
+ self._cached_collections = {}
66
+ self._log_warning()
67
+ self.available_collections = AVAILABLE_COLLECTIONS
68
+
69
+ def _log_warning(self):
70
+ _, _, api_name = __package__.partition(".")
71
+ logger.warning(
72
+ "%s is no longer maintained, recommend to use bkapi.%s as a replacement",
73
+ __package__,
74
+ api_name,
75
+ )
76
+
77
+ def merge_params_data_with_common_args(self, method, params, data):
78
+ """Add common args to params every request"""
79
+ common_args = dict(app_code=self.app_code, **self.common_args)
80
+ if method in ["GET", "HEAD"]:
81
+ _params = common_args.copy()
82
+ _params.update(params or {})
83
+ params = _params
84
+ elif method in ["POST", "PUT", "PATCH", "DELETE"]:
85
+ _data = common_args.copy()
86
+ _data.update(data or {})
87
+ data = json.dumps(_data)
88
+ return params, data
89
+
90
+ def __getattr__(self, key):
91
+ if key not in self.available_collections:
92
+ return getattr(super(), key)
93
+
94
+ if key not in self._cached_collections:
95
+ collection = self.available_collections[key]
96
+ self._cached_collections[key] = collection(self)
97
+ return self._cached_collections[key]
98
+
99
+ def request(self, method, url, params=None, data=None, path_params=None, **kwargs):
100
+ # Merge custom headers with instance headers
101
+ headers = self.headers.copy()
102
+ if "headers" in kwargs:
103
+ headers.update(kwargs.pop("headers"))
104
+ return requests.request(method, url, headers=headers, params=params, json=data, timeout=self.timeout, **kwargs)
105
+
106
+
107
+ class RedirectAPIClient(BaseAPIClient):
108
+ """用于根据View的request配置重定向请求."""
109
+
110
+ def redirect(self, request):
111
+ pass
112
+
113
+
114
+ class RequestAPIClient(BaseAPIClient):
115
+ def request(self, *args, **kwargs):
116
+ # update headers
117
+ headers = kwargs.pop("headers", {})
118
+ headers.update(self.headers)
119
+ method = kwargs.get("method", "GET")
120
+ if method.upper() in ["POST", "PUT", "PATCH", "DELETE"]:
121
+ headers["Content-Type"] = "application/json"
122
+ return self._request(headers=headers, *args, **kwargs)
123
+
124
+ def _request(self, method, url, params=None, data=None, **kwargs):
125
+ """Send request direct"""
126
+ params, data = self.merge_params_data_with_common_args(method, params, data)
127
+ logger.info("Calling %s %s with params=%s, data=%s", method, url, params, data)
128
+ return requests.request(method, url, params=params, json=data, timeout=self.timeout, **kwargs)
129
+
130
+
131
+ def get_client_by_request(request, stage=DEFAULT_STAGE, common_args=None, headers=None):
132
+ """
133
+ 根据当前请求返回一个client
134
+ :param request: 一个django request实例
135
+ :param stage: 请求环境,默认为prod
136
+ :param common_args: 公共请求参数
137
+ :param headers: 头部信息
138
+ :returns: 一个初始化好的APIClint对象
139
+ """
140
+ headers = headers or {}
141
+
142
+ is_authenticated = request.user.is_authenticated
143
+ if callable(is_authenticated):
144
+ is_authenticated = is_authenticated()
145
+ if is_authenticated:
146
+ headers.update(
147
+ {
148
+ HEADER_BK_AUTHORIZATION: json.dumps(
149
+ {
150
+ "bk_ticket": request.COOKIES.get("bk_ticket", ""),
151
+ "bk_app_code": settings.APP_CODE,
152
+ "bk_app_secret": settings.SECRET_KEY,
153
+ }
154
+ )
155
+ }
156
+ )
157
+ else:
158
+ raise APIException("用户未通过验证")
159
+
160
+ return RedirectAPIClient(
161
+ app_code=settings.APP_CODE,
162
+ app_secret=settings.SECRET_KEY,
163
+ headers=headers,
164
+ common_args=common_args,
165
+ stage=stage,
166
+ )
167
+
168
+
169
+ def get_client_by_user(user, stage=DEFAULT_STAGE, common_args=None, headers=None):
170
+ """
171
+ 根据user实例返回一个client
172
+ :param user: 用户
173
+ :param stage: 请求环境,默认为prod
174
+ :param common_args: 公共请求参数
175
+ :param common_args: 公共请求参数
176
+ :param headers: 头部信息
177
+ :returns: 一个初始化好的APIClint对象
178
+ """
179
+ headers = headers or {}
180
+ common_args = common_args or {}
181
+ if hasattr(user, "username"):
182
+ user = user.username
183
+ try:
184
+ from bkoauth import get_access_token_by_user
185
+
186
+ access_token = get_access_token_by_user(user)
187
+ headers.update(
188
+ {
189
+ HEADER_BK_AUTHORIZATION: json.dumps({"access_token": access_token.access_token}),
190
+ }
191
+ )
192
+ except Exception as e:
193
+ logger.warning("get_access_token_by_user error %s, using header authorization", str(e))
194
+ headers.update(
195
+ {
196
+ HEADER_BK_AUTHORIZATION: json.dumps(
197
+ {"bk_username": user, "bk_app_code": settings.APP_CODE, "bk_app_secret": settings.SECRET_KEY}
198
+ )
199
+ }
200
+ )
201
+
202
+ return RedirectAPIClient(
203
+ app_code=settings.APP_CODE,
204
+ app_secret=settings.SECRET_KEY,
205
+ headers=headers,
206
+ common_args=common_args,
207
+ stage=stage,
208
+ )
209
+
210
+
211
+ def get_headers_by_generator(request):
212
+ """
213
+ 通过配置的 generator 函数获取 headers
214
+
215
+ 优先级:
216
+ 1. 如果配置了 BKFLOW_SDK_APIGW_HEADERS_GENERATOR,则调用该函数获取
217
+ 2. 否则返回 None,使用默认的 headers 生成逻辑
218
+
219
+ :param request: 一个django request实例
220
+ :return: headers 字典,如果 generator 未配置或返回 None,则返回 None
221
+ """
222
+ generator_path = getattr(settings, "BKFLOW_SDK_APIGW_HEADERS_GENERATOR", None)
223
+
224
+ if generator_path:
225
+ headers = call_config_function(
226
+ generator_path,
227
+ "BKFLOW_SDK_APIGW_HEADERS_GENERATOR",
228
+ error_message_prefix="调用 BKFLOW_SDK_APIGW_HEADERS_GENERATOR",
229
+ request=request,
230
+ )
231
+ # 验证返回值为字典类型
232
+ if headers is not None and isinstance(headers, dict):
233
+ return headers
234
+
235
+ return None
236
+
237
+
238
+ def get_redirect_client_with_auth(request, **kwargs):
239
+ """
240
+ 根据当前请求返回一个带有认证信息的client
241
+ :param request: 一个django request实例
242
+ :returns: 一个初始化好的APIClint对象
243
+ """
244
+ # 尝试通过 generator 函数获取 headers
245
+ generator_headers = get_headers_by_generator(request)
246
+
247
+ if generator_headers is not None:
248
+ # 如果 generator 返回了 headers,直接使用
249
+ headers = generator_headers
250
+ else:
251
+ # 否则使用默认的 headers 生成逻辑
252
+ bk_ticket = request.COOKIES.get("bk_ticket", None)
253
+ bk_token = request.COOKIES.get("bk_token", None)
254
+ if not bk_ticket and not bk_token:
255
+ raise APIException("用户未通过验证")
256
+ headers = {
257
+ HEADER_BK_AUTHORIZATION: json.dumps(
258
+ {
259
+ "bk_app_code": settings.APP_CODE,
260
+ "bk_app_secret": settings.SECRET_KEY,
261
+ "bk_ticket": bk_ticket,
262
+ "bk_token": bk_token,
263
+ }
264
+ )
265
+ }
266
+
267
+ return RedirectAPIClient(
268
+ app_code=settings.APP_CODE, app_secret=settings.SECRET_KEY, headers=headers, common_args={}, stage=DEFAULT_STAGE
269
+ )
270
+
271
+
272
+ def get_redirect_client(request):
273
+ """
274
+ 根据当前请求返回一个client
275
+ :param request: 一个django request实例
276
+ :returns: 一个初始化好的APIClint对象
277
+ """
278
+ headers = {
279
+ HEADER_BK_AUTHORIZATION: json.dumps(
280
+ {
281
+ "bk_app_code": settings.APP_CODE,
282
+ "bk_app_secret": settings.SECRET_KEY,
283
+ }
284
+ )
285
+ }
286
+
287
+ return RedirectAPIClient(
288
+ app_code=settings.APP_CODE, app_secret=settings.SECRET_KEY, headers=headers, common_args={}, stage=DEFAULT_STAGE
289
+ )
@@ -0,0 +1,10 @@
1
+ """
2
+ Tencent is pleased to support the open source community by making 蓝鲸智云 - PaaS平台 (BlueKing - PaaS System) available.
3
+ Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved.
4
+ Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+ http://opensource.org/licenses/MIT
7
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8
+ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9
+ specific language governing permissions and limitations under the License.
10
+ """
@@ -0,0 +1,107 @@
1
+ """
2
+ Tencent is pleased to support the open source community by making 蓝鲸智云 - PaaS平台 (BlueKing - PaaS System) available.
3
+ Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved.
4
+ Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+ http://opensource.org/licenses/MIT
7
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8
+ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9
+ specific language governing permissions and limitations under the License.
10
+ """
11
+ from functools import wraps
12
+
13
+ from django.core.handlers.wsgi import WSGIRequest
14
+ from django.utils.translation import gettext_lazy as _
15
+ from rest_framework import serializers
16
+ from rest_framework.request import Request
17
+
18
+ from bkflow.common.exceptions import ValidationError
19
+ from bkflow.common.validator import custom_params_valid
20
+ from bkflow.config.default import REQUEST_TOKEN_HEADER_KEY
21
+
22
+
23
+ def token_inject(view_func):
24
+ """
25
+ token注入
26
+ :param view_func: 被装饰的view函数
27
+ """
28
+
29
+ def wrapper(*args, **kwargs):
30
+ request = args[1]
31
+ if not hasattr(request, "token"):
32
+ token = request.META.get(REQUEST_TOKEN_HEADER_KEY, "")
33
+
34
+ if not token:
35
+ raise ValidationError(
36
+ _("当前接口需要token,请在请求头中添加${REQUEST_TOKEN_HEADER_KEY}").format(
37
+ REQUEST_TOKEN_HEADER_KEY=REQUEST_TOKEN_HEADER_KEY
38
+ )
39
+ )
40
+
41
+ setattr(request, "token", token)
42
+
43
+ return view_func(*args, **kwargs)
44
+
45
+ return wraps(view_func)(wrapper)
46
+
47
+
48
+ def params_valid(serializer: serializers.Serializer, add_params: bool = True):
49
+ """参数校验装饰器
50
+
51
+ :param serializer: serializer类
52
+ :param add_params: 是否将校验后的参数添加到request.cleaned_params中
53
+ :return: 参数校验装饰器
54
+ """
55
+
56
+ def decorator(view_func):
57
+ @wraps(view_func)
58
+ def wrapper(*args, **kwargs):
59
+ # 获得Django的request对象
60
+ _request = kwargs.get("request")
61
+
62
+ if not _request:
63
+ for arg in args:
64
+ if isinstance(arg, (Request, WSGIRequest)):
65
+ _request = arg
66
+ break
67
+
68
+ if not _request:
69
+ raise ValidationError(_("该装饰器只允许用于Django的View函数(包括普通View函数和Class-base的View函数)"))
70
+
71
+ # 校验request中的参数
72
+ params = {}
73
+ if _request.method in ["GET"]:
74
+ if isinstance(_request, Request):
75
+ params = _request.query_params
76
+ else:
77
+ params = _request.GET
78
+ elif _request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest":
79
+ if isinstance(_request, Request):
80
+ params = _request.data
81
+ else:
82
+ params = _request.json()
83
+ else:
84
+ if isinstance(_request, Request):
85
+ params = _request.data
86
+ else:
87
+ params = _request.POST
88
+
89
+ cleaned_params = custom_params_valid(serializer=serializer, params=params)
90
+ _request.cleaned_params = cleaned_params
91
+
92
+ # 执行实际的View逻辑
93
+ params_add = False
94
+ try:
95
+ # 语法糖,使用这个decorator的话可直接从view中获得参数的字典
96
+ if "params" not in kwargs and add_params:
97
+ kwargs["params"] = cleaned_params
98
+ params_add = True
99
+ except TypeError:
100
+ if params_add:
101
+ del kwargs["params"]
102
+ resp = view_func(*args, **kwargs)
103
+ return resp
104
+
105
+ return wrapper
106
+
107
+ return decorator
@@ -0,0 +1,46 @@
1
+ """
2
+ Tencent is pleased to support the open source community by making 蓝鲸智云 - PaaS平台 (BlueKing - PaaS System) available.
3
+ Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved.
4
+ Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+ http://opensource.org/licenses/MIT
7
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8
+ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9
+ specific language governing permissions and limitations under the License.
10
+ """
11
+
12
+
13
+ class BkflowSDKException(Exception):
14
+ CODE = None
15
+ MESSAGE = None
16
+ STATUS_CODE = 500
17
+
18
+ def __init__(self, message="", errors=""):
19
+ if message:
20
+ self.MESSAGE = message
21
+ if self.MESSAGE and errors:
22
+ self.message = f"{self.MESSAGE}: {errors}"
23
+ elif self.MESSAGE:
24
+ self.message = self.MESSAGE
25
+ else:
26
+ self.message = f"{errors}"
27
+
28
+ def __str__(self):
29
+ return self.message
30
+
31
+
32
+ class APIException(BkflowSDKException):
33
+ """Exception for API"""
34
+
35
+ def __init__(self, errors, resp=None, url=""):
36
+ self.url = url
37
+ self.error_message = errors
38
+ self.resp = resp
39
+
40
+ if self.resp is not None:
41
+ errors = "%s, resp=%s" % (errors, self.resp.text)
42
+ super(APIException, self).__init__(errors)
43
+
44
+
45
+ class ValidationError(BkflowSDKException):
46
+ pass
@@ -0,0 +1,98 @@
1
+ """
2
+ Tencent is pleased to support the open source community by making 蓝鲸智云 - PaaS平台 (BlueKing - PaaS System) available.
3
+ Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved.
4
+ Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+ http://opensource.org/licenses/MIT
7
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8
+ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9
+ specific language governing permissions and limitations under the License.
10
+ """
11
+ import importlib
12
+ import logging
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def load_function_from_path(function_path, setting_name):
18
+ """
19
+ 从配置的函数路径动态加载函数
20
+
21
+ :param function_path: 函数路径,格式:module.path.function_name 或 module.path.ClassName.method_name
22
+ :param setting_name: 配置项名称,用于错误消息
23
+ :return: 加载的函数对象,如果无法加载则返回 None
24
+ :raises ValueError: 当函数路径格式错误或无法解析时
25
+ """
26
+ if not function_path:
27
+ return None
28
+
29
+ # 解析函数路径,格式:module.path.function_name 或 module.path.ClassName.method_name
30
+ # 从右到左尝试解析,找到最长的可导入模块路径
31
+ parts = function_path.split(".")
32
+ if len(parts) < 2:
33
+ raise ValueError(f"{setting_name} 格式错误,应为 'module.path.function_name'" " 或 'module.path.ClassName.method_name'")
34
+
35
+ # 尝试导入模块,从最长的路径开始
36
+ func = None
37
+ last_exception = None
38
+ for i in range(len(parts) - 1, 0, -1):
39
+ try:
40
+ module_path = ".".join(parts[:i])
41
+ attr_path = parts[i:]
42
+ module = importlib.import_module(module_path)
43
+ # 递归获取属性(支持类方法)
44
+ func = module
45
+ for attr_name in attr_path:
46
+ func = getattr(func, attr_name)
47
+ break
48
+ except ImportError:
49
+ # ImportError 表示模块不存在,继续尝试下一个路径
50
+ continue
51
+ except AttributeError as e:
52
+ # AttributeError 表示模块存在但属性不存在
53
+ # 如果这是第一个成功的模块导入,说明路径格式正确但属性不存在,应该抛出异常
54
+ # 否则继续尝试其他路径
55
+ last_exception = e
56
+ # 如果已经成功导入了模块,说明路径是正确的,只是属性不存在,应该立即抛出异常
57
+ if module is not None:
58
+ raise ValueError(
59
+ f"无法解析 {setting_name}: {function_path}," f"模块 '{module_path}' 存在但属性 '{'.'.join(attr_path)}' 不存在"
60
+ )
61
+ continue
62
+
63
+ if func is None:
64
+ # 如果所有尝试都失败,抛出异常
65
+ if last_exception:
66
+ raise ValueError(f"无法解析 {setting_name}: {function_path},原因:{str(last_exception)}")
67
+ else:
68
+ raise ValueError(f"无法解析 {setting_name}: {function_path}")
69
+
70
+ return func
71
+
72
+
73
+ def call_config_function(function_path, setting_name, error_message_prefix=None, **kwargs):
74
+ """
75
+ 调用配置的函数并返回结果
76
+
77
+ :param function_path: 函数路径,格式:module.path.function_name 或 module.path.ClassName.method_name
78
+ :param setting_name: 配置项名称,用于错误消息
79
+ :param error_message_prefix: 错误消息前缀,用于日志记录
80
+ :param kwargs: 传递给函数的参数
81
+ :return: 函数返回值,如果函数未配置或调用失败则返回 None
82
+ """
83
+ if not function_path:
84
+ return None
85
+
86
+ try:
87
+ func = load_function_from_path(function_path, setting_name)
88
+ if func is None:
89
+ return None
90
+
91
+ # 调用函数
92
+ result = func(**kwargs)
93
+ return result
94
+ except Exception as e:
95
+ # 如果函数调用失败,记录错误
96
+ error_prefix = error_message_prefix or f"调用 {setting_name}"
97
+ logger.warning(f"{error_prefix} 失败: {e},将使用默认逻辑")
98
+ return None
@@ -0,0 +1,96 @@
1
+ """
2
+ Tencent is pleased to support the open source community by making 蓝鲸智云 - PaaS平台 (BlueKing - PaaS System) available.
3
+ Copyright (C) 2022 THL A29 Limited, a Tencent company. All rights reserved.
4
+ Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+ http://opensource.org/licenses/MIT
7
+ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
8
+ an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
9
+ specific language governing permissions and limitations under the License.
10
+ """
11
+ import json
12
+
13
+ from django.utils.translation import gettext_lazy as _
14
+ from rest_framework import serializers
15
+
16
+ from bkflow.common.exceptions import ValidationError
17
+
18
+
19
+ def format_serializer_errors(errors: dict, fields: dict, params: dict, prefix: str = " "):
20
+ """格式化序列化器错误信息
21
+
22
+ :param errors: 错误信息
23
+ :param fields: 序列化器字段
24
+ :param params: 参数
25
+ :return: 格式化后的错误信息
26
+ """
27
+ message = _("参数校验失败:{wrap}").format(wrap="\n") if prefix == " " else "\n"
28
+ for key, field_errors in list(errors.items()):
29
+ sub_message = ""
30
+ label = key
31
+ if key not in fields:
32
+ sub_message = json.dumps(field_errors, ensure_ascii=False)
33
+ else:
34
+ field = fields[key]
35
+ label = field.label or field.field_name
36
+ if (
37
+ hasattr(field, "child")
38
+ and isinstance(field_errors, list)
39
+ and len(field_errors) > 0
40
+ and not isinstance(field_errors[0], str)
41
+ ):
42
+ for index, sub_errors in enumerate(field_errors):
43
+ if sub_errors:
44
+ sub_format = format_serializer_errors(
45
+ sub_errors, field.child.fields, params, prefix=prefix + " "
46
+ )
47
+ # return sub_format
48
+ sub_message += _("{wrap}{prefix}第{index}项:").format(
49
+ wrap="\n",
50
+ prefix=prefix + " ",
51
+ index=index + 1,
52
+ )
53
+ sub_message += sub_format
54
+ else:
55
+ if isinstance(field_errors, dict):
56
+ if hasattr(field, "child"):
57
+ sub_foramt = format_serializer_errors(
58
+ field_errors, field.child.fields, params, prefix=prefix + " "
59
+ )
60
+ else:
61
+ sub_foramt = format_serializer_errors(field_errors, field.fields, params, prefix=prefix + " ")
62
+ sub_message += sub_foramt
63
+ elif isinstance(field_errors, list):
64
+ for index in range(len(field_errors)):
65
+ field_errors[index] = field_errors[index].format(**{key: params.get(key, "")})
66
+ sub_message += "{index}.{error}".format(index=index + 1, error=field_errors[index])
67
+ sub_message += "\n"
68
+ # 对使用 Validate() 时 label == "non_field_errors" 的特殊情况做处理
69
+ if label == "non_field_errors":
70
+ message += f"{prefix} {sub_message}"
71
+ else:
72
+ message += f"{prefix}{label}: {sub_message}"
73
+ return message
74
+
75
+
76
+ def custom_params_valid(serializer: serializers.Serializer, params: dict, many: bool = False):
77
+ """校验参数
78
+
79
+ :param serializer: 校验器
80
+ :param params: 原始参数
81
+ :param many: 是否为多个参数
82
+ :return: 校验通过的参数
83
+ """
84
+ _serializer = serializer(data=params, many=many)
85
+ try:
86
+ _serializer.is_valid(raise_exception=True)
87
+ except serializers.ValidationError as e: # pylint: disable=broad-except # noqa
88
+ try:
89
+ message = format_serializer_errors(_serializer.errors, _serializer.fields, params)
90
+ except Exception as e: # pylint: disable=broad-except
91
+ if isinstance(e.message, str):
92
+ message = e.message
93
+ else:
94
+ message = _("参数校验失败,详情请查看返回的errors")
95
+ raise ValidationError(message=message, errors=_serializer.errors)
96
+ return list(_serializer.validated_data) if many else dict(_serializer.validated_data)