pytest-auto-api2-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. cli.py +9 -0
  2. common/__init__.py +0 -0
  3. common/setting.py +78 -0
  4. pytest_auto_api2/__init__.py +4 -0
  5. pytest_auto_api2/cli.py +900 -0
  6. pytest_auto_api2/runtime/__init__.py +1 -0
  7. pytest_auto_api2/runtime/api.py +10 -0
  8. pytest_auto_api2/runtime/loader.py +80 -0
  9. pytest_auto_api2_cli-0.2.0.dist-info/METADATA +945 -0
  10. pytest_auto_api2_cli-0.2.0.dist-info/RECORD +73 -0
  11. pytest_auto_api2_cli-0.2.0.dist-info/WHEEL +5 -0
  12. pytest_auto_api2_cli-0.2.0.dist-info/entry_points.txt +2 -0
  13. pytest_auto_api2_cli-0.2.0.dist-info/top_level.txt +6 -0
  14. run.py +85 -0
  15. test_case/Collect/test_collect_addtool.py +40 -0
  16. test_case/Collect/test_collect_delete_tool.py +40 -0
  17. test_case/Collect/test_collect_tool_list.py +40 -0
  18. test_case/Collect/test_collect_update_tool.py +40 -0
  19. test_case/Login/test_login.py +40 -0
  20. test_case/UserInfo/test_get_user_info.py +40 -0
  21. test_case/__init__.py +6 -0
  22. test_case/conftest.py +132 -0
  23. utils/__init__.py +9 -0
  24. utils/assertion/__init__.py +4 -0
  25. utils/assertion/assert_control.py +179 -0
  26. utils/assertion/assert_type.py +141 -0
  27. utils/cache_process/__init__.py +4 -0
  28. utils/cache_process/cache_control.py +89 -0
  29. utils/cache_process/redis_control.py +106 -0
  30. utils/logging_tool/__init__.py +4 -0
  31. utils/logging_tool/log_control.py +84 -0
  32. utils/logging_tool/log_decorator.py +48 -0
  33. utils/logging_tool/run_time_decorator.py +34 -0
  34. utils/mysql_tool/__init__.py +4 -0
  35. utils/mysql_tool/mysql_control.py +175 -0
  36. utils/notify/__init__.py +1 -0
  37. utils/notify/ding_talk.py +153 -0
  38. utils/notify/lark.py +181 -0
  39. utils/notify/send_mail.py +84 -0
  40. utils/notify/wechat_send.py +109 -0
  41. utils/other_tools/__init__.py +0 -0
  42. utils/other_tools/address_detection.py +73 -0
  43. utils/other_tools/allure_data/__init__.py +4 -0
  44. utils/other_tools/allure_data/allure_report_data.py +84 -0
  45. utils/other_tools/allure_data/allure_tools.py +54 -0
  46. utils/other_tools/allure_data/error_case_excel.py +316 -0
  47. utils/other_tools/exceptions.py +47 -0
  48. utils/other_tools/get_local_ip.py +27 -0
  49. utils/other_tools/install_tool/__init__.py +0 -0
  50. utils/other_tools/install_tool/install_requirements.py +91 -0
  51. utils/other_tools/jsonpath_date_replace.py +28 -0
  52. utils/other_tools/models.py +269 -0
  53. utils/other_tools/thread_tool.py +91 -0
  54. utils/read_files_tools/__init__.py +1 -0
  55. utils/read_files_tools/case_automatic_control.py +138 -0
  56. utils/read_files_tools/clean_files.py +19 -0
  57. utils/read_files_tools/excel_control.py +55 -0
  58. utils/read_files_tools/get_all_files_path.py +27 -0
  59. utils/read_files_tools/get_yaml_data_analysis.py +156 -0
  60. utils/read_files_tools/regular_control.py +209 -0
  61. utils/read_files_tools/swagger_for_yaml.py +145 -0
  62. utils/read_files_tools/testcase_template.py +103 -0
  63. utils/read_files_tools/yaml_control.py +86 -0
  64. utils/recording/__init__.py +0 -0
  65. utils/recording/mitmproxy_control.py +225 -0
  66. utils/requests_tool/__init__.py +0 -0
  67. utils/requests_tool/dependent_case.py +273 -0
  68. utils/requests_tool/encryption_algorithm_control.py +80 -0
  69. utils/requests_tool/request_control.py +443 -0
  70. utils/requests_tool/set_current_request_cache.py +73 -0
  71. utils/requests_tool/teardown_control.py +280 -0
  72. utils/times_tool/__init__.py +0 -0
  73. utils/times_tool/time_control.py +87 -0
@@ -0,0 +1,443 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ # @Time : 2022/3/28 12:52
5
+ # @Author : 余少琪
6
+ """
7
+ import ast
8
+ import os
9
+ import random
10
+ import time
11
+ import urllib
12
+ from typing import Tuple, Dict, Union, Text
13
+ import requests
14
+ import urllib3
15
+ from requests_toolbelt import MultipartEncoder
16
+ from common.setting import ensure_path_sep
17
+ from utils.other_tools.models import RequestType
18
+ from utils.logging_tool.log_decorator import log_decorator
19
+ from utils.mysql_tool.mysql_control import AssertExecution
20
+ from utils.logging_tool.run_time_decorator import execution_duration
21
+ from utils.other_tools.allure_data.allure_tools import allure_step, allure_step_no, allure_attach
22
+ from utils.read_files_tools.regular_control import cache_regular
23
+ from utils.requests_tool.set_current_request_cache import SetCurrentRequestCache
24
+ from utils.other_tools.models import TestCase, ResponseData
25
+ from utils import config
26
+ # from utils.requests_tool.encryption_algorithm_control import encryption
27
+
28
+ urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
29
+
30
+
31
+ class RequestControl:
32
+ """ 封装请求 """
33
+
34
+ def __init__(self, yaml_case):
35
+ self.__yaml_case = TestCase(**yaml_case)
36
+
37
+ def file_data_exit(
38
+ self,
39
+ file_data) -> None:
40
+ """判断上传文件时,data参数是否存在"""
41
+ # 兼容又要上传文件,又要上传其他类型参数
42
+ try:
43
+ _data = self.__yaml_case.data
44
+ for key, value in ast.literal_eval(cache_regular(str(_data)))['data'].items():
45
+ if "multipart/form-data" in str(self.__yaml_case.headers.values()):
46
+ file_data[key] = str(value)
47
+ else:
48
+ file_data[key] = value
49
+ except KeyError:
50
+ ...
51
+
52
+ @classmethod
53
+ def multipart_data(
54
+ cls,
55
+ file_data: Dict):
56
+ """ 处理上传文件数据 """
57
+ multipart = MultipartEncoder(
58
+ fields=file_data, # 字典格式
59
+ boundary='-----------------------------' + str(random.randint(int(1e28), int(1e29 - 1)))
60
+ )
61
+ return multipart
62
+
63
+ @classmethod
64
+ def check_headers_str_null(
65
+ cls,
66
+ headers: Dict) -> Dict:
67
+ """
68
+ 兼容用户未填写headers或者header值为int
69
+ @return:
70
+ """
71
+ headers = ast.literal_eval(cache_regular(str(headers)))
72
+ if headers is None:
73
+ headers = {"headers": None}
74
+ else:
75
+ for key, value in headers.items():
76
+ if not isinstance(value, str):
77
+ headers[key] = str(value)
78
+ return headers
79
+
80
+ @classmethod
81
+ def multipart_in_headers(
82
+ cls,
83
+ request_data: Dict,
84
+ header: Dict):
85
+ """ 判断处理header为 Content-Type: multipart/form-data"""
86
+ header = ast.literal_eval(cache_regular(str(header)))
87
+ request_data = ast.literal_eval(cache_regular(str(request_data)))
88
+
89
+ if header is None:
90
+ header = {"headers": None}
91
+ else:
92
+ # 将header中的int转换成str
93
+ for key, value in header.items():
94
+ if not isinstance(value, str):
95
+ header[key] = str(value)
96
+ if "multipart/form-data" in str(header.values()):
97
+ # 判断请求参数不为空, 并且参数是字典类型
98
+ if request_data and isinstance(request_data, dict):
99
+ # 当 Content-Type 为 "multipart/form-data"时,需要将数据类型转换成 str
100
+ for key, value in request_data.items():
101
+ if not isinstance(value, str):
102
+ request_data[key] = str(value)
103
+
104
+ request_data = MultipartEncoder(request_data)
105
+ header['Content-Type'] = request_data.content_type
106
+
107
+ return request_data, header
108
+
109
+ def file_prams_exit(self) -> Dict:
110
+ """判断上传文件接口,文件参数是否存在"""
111
+ try:
112
+ params = self.__yaml_case.data['params']
113
+ except KeyError:
114
+ params = None
115
+ return params
116
+
117
+ @classmethod
118
+ def text_encode(
119
+ cls,
120
+ text: Text) -> Text:
121
+ """unicode 解码"""
122
+ return text.encode("utf-8").decode("utf-8")
123
+
124
+ @classmethod
125
+ def response_elapsed_total_seconds(
126
+ cls,
127
+ res) -> float:
128
+ """获取接口响应时长"""
129
+ try:
130
+ return round(res.elapsed.total_seconds() * 1000, 2)
131
+ except AttributeError:
132
+ return 0.00
133
+
134
+ def upload_file(
135
+ self) -> Tuple:
136
+ """
137
+ 判断处理上传文件
138
+ :return:
139
+ """
140
+ # 处理上传多个文件的情况
141
+ _files = []
142
+ file_data = {}
143
+ # 兼容又要上传文件,又要上传其他类型参数
144
+ self.file_data_exit(file_data)
145
+ _data = self.__yaml_case.data
146
+ for key, value in ast.literal_eval(cache_regular(str(_data)))['file'].items():
147
+ file_path = ensure_path_sep("\\Files\\" + value)
148
+ file_data[key] = (value, open(file_path, 'rb'), 'application/octet-stream')
149
+ _files.append(file_data)
150
+ # allure中展示该附件
151
+ allure_attach(source=file_path, name=value, extension=value)
152
+ multipart = self.multipart_data(file_data)
153
+ # ast.literal_eval(cache_regular(str(_headers)))['Content-Type'] = multipart.content_type
154
+ self.__yaml_case.headers['Content-Type'] = multipart.content_type
155
+ params_data = ast.literal_eval(cache_regular(str(self.file_prams_exit())))
156
+ return multipart, params_data, self.__yaml_case
157
+
158
+ def request_type_for_json(
159
+ self,
160
+ headers: Dict,
161
+ method: Text,
162
+ **kwargs):
163
+ """ 判断请求类型为json格式 """
164
+ _headers = self.check_headers_str_null(headers)
165
+ _data = self.__yaml_case.data
166
+ _url = self.__yaml_case.url
167
+ res = requests.request(
168
+ method=method,
169
+ url=cache_regular(str(_url)),
170
+ json=ast.literal_eval(cache_regular(str(_data))),
171
+ data={},
172
+ headers=_headers,
173
+ verify=False,
174
+ params=None,
175
+ **kwargs
176
+ )
177
+ return res
178
+
179
+ def request_type_for_none(
180
+ self,
181
+ headers: Dict,
182
+ method: Text,
183
+ **kwargs) -> object:
184
+ """判断 requestType 为 None"""
185
+ _headers = self.check_headers_str_null(headers)
186
+ _url = self.__yaml_case.url
187
+ res = requests.request(
188
+ method=method,
189
+ url=cache_regular(_url),
190
+ data=None,
191
+ headers=_headers,
192
+ verify=False,
193
+ params=None,
194
+ **kwargs
195
+ )
196
+ return res
197
+
198
+ def request_type_for_params(
199
+ self,
200
+ headers: Dict,
201
+ method: Text,
202
+ **kwargs):
203
+
204
+ """处理 requestType 为 params """
205
+ _data = self.__yaml_case.data
206
+ url = self.__yaml_case.url
207
+ if _data is not None:
208
+ # url 拼接的方式传参
209
+ params_data = "?"
210
+ for key, value in _data.items():
211
+ if value is None or value == '':
212
+ params_data += (key + "&")
213
+ else:
214
+ params_data += (key + "=" + str(value) + "&")
215
+ url = self.__yaml_case.url + params_data[:-1]
216
+ _headers = self.check_headers_str_null(headers)
217
+ res = requests.request(
218
+ method=method,
219
+ url=cache_regular(url),
220
+ headers=_headers,
221
+ verify=False,
222
+ data={},
223
+ params=None,
224
+ **kwargs)
225
+ return res
226
+
227
+ def request_type_for_file(
228
+ self,
229
+ method: Text,
230
+ headers,
231
+ **kwargs):
232
+ """处理 requestType 为 file 类型"""
233
+ multipart = self.upload_file()
234
+ yaml_data = multipart[2]
235
+ _headers = multipart[2].headers
236
+ _headers = self.check_headers_str_null(_headers)
237
+ res = requests.request(
238
+ method=method,
239
+ url=cache_regular(yaml_data.url),
240
+ data=multipart[0],
241
+ params=multipart[1],
242
+ headers=ast.literal_eval(cache_regular(str(_headers))),
243
+ verify=False,
244
+ **kwargs
245
+ )
246
+ return res
247
+
248
+ def request_type_for_data(
249
+ self,
250
+ headers: Dict,
251
+ method: Text,
252
+ **kwargs):
253
+ """判断 requestType 为 data 类型"""
254
+ data = self.__yaml_case.data
255
+ _data, _headers = self.multipart_in_headers(
256
+ ast.literal_eval(cache_regular(str(data))),
257
+ headers
258
+ )
259
+ _url = self.__yaml_case.url
260
+ res = requests.request(
261
+ method=method,
262
+ url=cache_regular(_url),
263
+ data=_data,
264
+ headers=_headers,
265
+ verify=False,
266
+ **kwargs)
267
+
268
+ return res
269
+
270
+ @classmethod
271
+ def get_export_api_filename(cls, res):
272
+ """ 处理导出文件 """
273
+ content_disposition = res.headers.get('content-disposition')
274
+ filename_code = content_disposition.split("=")[-1] # 分隔字符串,提取文件名
275
+ filename = urllib.parse.unquote(filename_code) # url解码
276
+ return filename
277
+
278
+ def request_type_for_export(
279
+ self,
280
+ headers: Dict,
281
+ method: Text,
282
+ **kwargs):
283
+ """判断 requestType 为 export 导出类型"""
284
+ _headers = self.check_headers_str_null(headers)
285
+ _data = self.__yaml_case.data
286
+ _url = self.__yaml_case.url
287
+ res = requests.request(
288
+ method=method,
289
+ url=cache_regular(_url),
290
+ json=ast.literal_eval(cache_regular(str(_data))),
291
+ headers=_headers,
292
+ verify=False,
293
+ stream=False,
294
+ data={},
295
+ **kwargs)
296
+ filepath = os.path.join(ensure_path_sep("\\Files\\"), self.get_export_api_filename(res)) # 拼接路径
297
+ if res.status_code == 200:
298
+ if res.text: # 判断文件内容是否为空
299
+ with open(filepath, 'wb') as file:
300
+ # iter_content循环读取信息写入,chunk_size设置文件大小
301
+ for chunk in res.iter_content(chunk_size=1):
302
+ file.write(chunk)
303
+ else:
304
+ print("文件为空")
305
+
306
+ return res
307
+
308
+ @classmethod
309
+ def _request_body_handler(cls, data: Dict, request_type: Text) -> Union[None, Dict]:
310
+ """处理请求参数 """
311
+ if request_type.upper() == 'PARAMS':
312
+ return None
313
+ else:
314
+ return data
315
+
316
+ @classmethod
317
+ def _sql_data_handler(cls, sql_data, res):
318
+ """处理 sql 参数 """
319
+ # 判断数据库开关,开启状态,则返回对应的数据
320
+ if config.mysql_db.switch and sql_data is not None:
321
+ sql_data = AssertExecution().assert_execution(
322
+ sql=sql_data,
323
+ resp=res.json()
324
+ )
325
+
326
+ else:
327
+ sql_data = {"sql": None}
328
+ return sql_data
329
+
330
+ def _check_params(
331
+ self,
332
+ res,
333
+ yaml_data: "TestCase",
334
+ ) -> "ResponseData":
335
+ data = ast.literal_eval(cache_regular(str(yaml_data.data)))
336
+ _data = {
337
+ "url": res.url,
338
+ "is_run": yaml_data.is_run,
339
+ "detail": yaml_data.detail,
340
+ "response_data": res.text,
341
+ # 这个用于日志专用,判断如果是get请求,直接打印url
342
+ "request_body": self._request_body_handler(
343
+ data, yaml_data.requestType
344
+ ),
345
+ "method": res.request.method,
346
+ "sql_data": self._sql_data_handler(sql_data=ast.literal_eval(cache_regular(str(yaml_data.sql))), res=res),
347
+ "yaml_data": yaml_data,
348
+ "headers": res.request.headers,
349
+ "cookie": res.cookies,
350
+ "assert_data": yaml_data.assert_data,
351
+ "res_time": self.response_elapsed_total_seconds(res),
352
+ "status_code": res.status_code,
353
+ "teardown": yaml_data.teardown,
354
+ "teardown_sql": yaml_data.teardown_sql,
355
+ "body": data
356
+ }
357
+ # 抽离出通用模块,判断 http_request 方法中的一些数据校验
358
+ return ResponseData(**_data)
359
+
360
+ @classmethod
361
+ def api_allure_step(
362
+ cls,
363
+ *,
364
+ url: Text,
365
+ headers: Text,
366
+ method: Text,
367
+ data: Text,
368
+ assert_data: Text,
369
+ res_time: Text,
370
+ res: Text
371
+ ) -> None:
372
+ """ 在allure中记录请求数据 """
373
+ allure_step_no(f"请求URL: {url}")
374
+ allure_step_no(f"请求方式: {method}")
375
+ allure_step("请求头: ", headers)
376
+ allure_step("请求数据: ", data)
377
+ allure_step("预期数据: ", assert_data)
378
+ _res_time = res_time
379
+ allure_step_no(f"响应耗时(ms): {str(_res_time)}")
380
+ allure_step("响应结果: ", res)
381
+
382
+ @log_decorator(True)
383
+ @execution_duration(3000)
384
+ # @encryption("md5")
385
+ def http_request(
386
+ self,
387
+ dependent_switch=True,
388
+ **kwargs
389
+ ):
390
+ """
391
+ 请求封装
392
+ :param dependent_switch:
393
+ :param kwargs:
394
+ :return:
395
+ """
396
+ from utils.requests_tool.dependent_case import DependentCase
397
+ requests_type_mapping = {
398
+ RequestType.JSON.value: self.request_type_for_json,
399
+ RequestType.NONE.value: self.request_type_for_none,
400
+ RequestType.PARAMS.value: self.request_type_for_params,
401
+ RequestType.FILE.value: self.request_type_for_file,
402
+ RequestType.DATA.value: self.request_type_for_data,
403
+ RequestType.EXPORT.value: self.request_type_for_export
404
+ }
405
+
406
+ is_run = ast.literal_eval(cache_regular(str(self.__yaml_case.is_run)))
407
+ # 判断用例是否执行
408
+ if is_run is True or is_run is None:
409
+ # 处理多业务逻辑
410
+ if dependent_switch is True:
411
+ DependentCase(self.__yaml_case).get_dependent_data()
412
+
413
+ res = requests_type_mapping.get(self.__yaml_case.requestType)(
414
+ headers=self.__yaml_case.headers,
415
+ method=self.__yaml_case.method,
416
+ **kwargs
417
+ )
418
+
419
+ if self.__yaml_case.sleep is not None:
420
+ time.sleep(self.__yaml_case.sleep)
421
+
422
+ _res_data = self._check_params(
423
+ res=res,
424
+ yaml_data=self.__yaml_case)
425
+
426
+ self.api_allure_step(
427
+ url=_res_data.url,
428
+ headers=str(_res_data.headers),
429
+ method=_res_data.method,
430
+ data=str(_res_data.body),
431
+ assert_data=str(_res_data.assert_data),
432
+ res_time=str(_res_data.res_time),
433
+ res=_res_data.response_data
434
+ )
435
+ # 将当前请求数据存入缓存中
436
+ SetCurrentRequestCache(
437
+ current_request_set_cache=self.__yaml_case.current_request_set_cache,
438
+ request_data=self.__yaml_case.data,
439
+ response_data=res
440
+ ).set_caches_main()
441
+
442
+ return _res_data
443
+
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ # @Time : 2022/6/2 11:30
5
+ # @Author : 余少琪
6
+ # @Email : 1603453211@qq.com
7
+ # @File : set_current_request_cache
8
+ # @describe:
9
+ """
10
+ import json
11
+ from typing import Text
12
+ from jsonpath import jsonpath
13
+ from utils.other_tools.exceptions import ValueNotFoundError
14
+ from utils.cache_process.cache_control import CacheHandler
15
+
16
+
17
+ class SetCurrentRequestCache:
18
+ """将用例中的请求或者响应内容存入缓存"""
19
+
20
+ def __init__(
21
+ self,
22
+ current_request_set_cache,
23
+ request_data,
24
+ response_data
25
+ ):
26
+ self.current_request_set_cache = current_request_set_cache
27
+ self.request_data = {"data": request_data}
28
+ self.response_data = response_data.text
29
+
30
+ def set_request_cache(
31
+ self,
32
+ jsonpath_value: Text,
33
+ cache_name: Text) -> None:
34
+ """将接口的请求参数存入缓存"""
35
+ _request_data = jsonpath(
36
+ self.request_data,
37
+ jsonpath_value
38
+ )
39
+ if _request_data is not False:
40
+ CacheHandler.update_cache(cache_name=cache_name, value=_request_data[0])
41
+ # Cache(cache_name).set_caches(_request_data[0])
42
+ else:
43
+ raise ValueNotFoundError(
44
+ "缓存设置失败,程序中未检测到需要缓存的数据。"
45
+ f"请求参数: {self.request_data}"
46
+ f"提取的 jsonpath 内容: {jsonpath_value}"
47
+ )
48
+
49
+ def set_response_cache(
50
+ self,
51
+ jsonpath_value: Text,
52
+ cache_name
53
+ ):
54
+ """将响应结果存入缓存"""
55
+ _response_data = jsonpath(json.loads(self.response_data), jsonpath_value)
56
+ if _response_data is not False:
57
+ CacheHandler.update_cache(cache_name=cache_name, value=_response_data[0])
58
+ # Cache(cache_name).set_caches(_response_data[0])
59
+ else:
60
+ raise ValueNotFoundError("缓存设置失败,程序中未检测到需要缓存的数据。"
61
+ f"请求参数: {self.response_data}"
62
+ f"提取的 jsonpath 内容: {jsonpath_value}")
63
+
64
+ def set_caches_main(self):
65
+ """设置缓存"""
66
+ if self.current_request_set_cache is not None:
67
+ for i in self.current_request_set_cache:
68
+ _jsonpath = i.jsonpath
69
+ _cache_name = i.name
70
+ if i.type == 'request':
71
+ self.set_request_cache(jsonpath_value=_jsonpath, cache_name=_cache_name)
72
+ elif i.type == 'response':
73
+ self.set_response_cache(jsonpath_value=_jsonpath, cache_name=_cache_name)