pytest-api-framework-alpha 0.3.4__tar.gz → 0.3.5__tar.gz
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.
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/PKG-INFO +1 -1
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/conftest.py +336 -336
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/db/mysql_db.py +12 -5
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/global_attribute.py +4 -1
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/mock_util.py +2 -1
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/validate.py +1 -1
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/pytest_api_framework_alpha.egg-info/PKG-INFO +1 -1
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/setup.py +1 -1
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/__init__.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/base_class.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/db/__init__.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/db/redis_db.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/exceptions.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/exit_code.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/extract.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/http_client.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/render_data.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/report.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/script.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/startapp.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/__init__.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/common.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/date_util.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/encrypt.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/log_util.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/yaml_util.py +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/pytest_api_framework_alpha.egg-info/SOURCES.txt +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/pytest_api_framework_alpha.egg-info/dependency_links.txt +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/pytest_api_framework_alpha.egg-info/requires.txt +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/pytest_api_framework_alpha.egg-info/top_level.txt +0 -0
- {pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/setup.cfg +0 -0
|
@@ -13,6 +13,7 @@ from collections import OrderedDict
|
|
|
13
13
|
from datetime import datetime, timedelta
|
|
14
14
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
15
15
|
|
|
16
|
+
import dill
|
|
16
17
|
import json
|
|
17
18
|
import retry
|
|
18
19
|
import allure
|
|
@@ -64,220 +65,15 @@ def context():
|
|
|
64
65
|
yield context
|
|
65
66
|
|
|
66
67
|
|
|
68
|
+
class Http(object): pass
|
|
69
|
+
|
|
70
|
+
|
|
67
71
|
@retry.retry(tries=3, delay=1)
|
|
68
72
|
@pytest.fixture(scope="function", autouse=True)
|
|
69
73
|
def http():
|
|
70
74
|
yield _FRAMEWORK_CONTEXT.get("_http")
|
|
71
75
|
|
|
72
76
|
|
|
73
|
-
class Http(object):
|
|
74
|
-
...
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def init_mysql():
|
|
78
|
-
"""初始化 MySQL 连接池"""
|
|
79
|
-
try:
|
|
80
|
-
mysql_config = CONFIG.get(app=all_app[0], key="mysql")
|
|
81
|
-
mysql_conns = {item: MysqlDB(**mysql_config[item]) for item in mysql_config}
|
|
82
|
-
for app in all_app:
|
|
83
|
-
_FRAMEWORK_CONTEXT.set(app=app, key="mysql", value=mysql_conns)
|
|
84
|
-
except Exception as e:
|
|
85
|
-
raise MysqlDBError(e)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def init_redis():
|
|
89
|
-
"""初始化 Redis 连接池(16个db)"""
|
|
90
|
-
try:
|
|
91
|
-
redis_config = CONFIG.get(app=all_app[0], key="redis")
|
|
92
|
-
redis_conns = {
|
|
93
|
-
db: [RedisDB(**{**db_info, "db": i}) for i in range(16)]
|
|
94
|
-
for db, db_info in redis_config.items()
|
|
95
|
-
}
|
|
96
|
-
for app in all_app:
|
|
97
|
-
_FRAMEWORK_CONTEXT.set(app=app, key="redis", value=redis_conns)
|
|
98
|
-
except Exception as e:
|
|
99
|
-
raise RedisDBError(e)
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def inner_login(app):
|
|
103
|
-
"""单个系统的登录和资源初始化"""
|
|
104
|
-
|
|
105
|
-
login_cls = getattr(module, f"{snake_to_pascal(app)}Login")
|
|
106
|
-
setattr(Http, app, login_cls(app))
|
|
107
|
-
# Token 过期时间写入上下文
|
|
108
|
-
token_expiry = CONTEXT.get(app).get("token_expiry")
|
|
109
|
-
if token_expiry:
|
|
110
|
-
expire_time = datetime.now() + timedelta(seconds=token_expiry)
|
|
111
|
-
_FRAMEWORK_CONTEXT.set(app=app, key="expire_time", value=expire_time)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def login():
|
|
115
|
-
def safe_call(func, name):
|
|
116
|
-
try:
|
|
117
|
-
logger.info(f"初始化{name}连接...")
|
|
118
|
-
func()
|
|
119
|
-
logger.info(f"{name}连接初始化成功")
|
|
120
|
-
except (MysqlDBError, RedisDBError) as e:
|
|
121
|
-
logger.error(f"{name}连接初始化异常: {e}")
|
|
122
|
-
|
|
123
|
-
# 启动 MySQL 和 Redis 初始化线程
|
|
124
|
-
threads = [
|
|
125
|
-
threading.Thread(target=safe_call, args=(init_mysql, "MySQL")),
|
|
126
|
-
threading.Thread(target=safe_call, args=(init_redis, "Redis"))
|
|
127
|
-
]
|
|
128
|
-
|
|
129
|
-
for t in threads:
|
|
130
|
-
t.start()
|
|
131
|
-
for t in threads:
|
|
132
|
-
t.join()
|
|
133
|
-
|
|
134
|
-
logger.info("登录账号".center(80, "*"))
|
|
135
|
-
with ThreadPoolExecutor(max_workers=len(all_app)) as executor:
|
|
136
|
-
futures = {executor.submit(inner_login, app): app for app in all_app}
|
|
137
|
-
for future in as_completed(futures):
|
|
138
|
-
future.result()
|
|
139
|
-
|
|
140
|
-
logger.info("登录完成".center(80, "*"))
|
|
141
|
-
return Http
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
def find_data_path_by_case(app, case_file_name):
|
|
145
|
-
"""
|
|
146
|
-
基于case文件名称查找与之对应的yml文件路径
|
|
147
|
-
:param app:
|
|
148
|
-
:param case_file_name:
|
|
149
|
-
:return:
|
|
150
|
-
"""
|
|
151
|
-
env = CONTEXT.get("env")
|
|
152
|
-
for file_path in Path(os.path.join(settings.DATA_DIR, env, app)).rglob(f"{case_file_name}.y*"):
|
|
153
|
-
if file_path:
|
|
154
|
-
return file_path
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
def subset_and_diff(small: set, big: set):
|
|
158
|
-
"""
|
|
159
|
-
判断 small 是否为 big 的子集,并返回差集
|
|
160
|
-
"""
|
|
161
|
-
return small.issubset(big), small - big
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def disable_field(scenario, data):
|
|
165
|
-
if not scenario:
|
|
166
|
-
return data
|
|
167
|
-
|
|
168
|
-
def _clean(obj):
|
|
169
|
-
if isinstance(obj, dict):
|
|
170
|
-
keys_to_delete = []
|
|
171
|
-
for k, v in obj.items():
|
|
172
|
-
if isinstance(v, (dict, list)):
|
|
173
|
-
_clean(v)
|
|
174
|
-
elif isinstance(v, str):
|
|
175
|
-
for ak, av in scenario.items():
|
|
176
|
-
if av == "disable" and v == f"${{{ak}}}":
|
|
177
|
-
keys_to_delete.append(k)
|
|
178
|
-
break
|
|
179
|
-
# 统一删除,避免边遍历边删
|
|
180
|
-
for k in keys_to_delete:
|
|
181
|
-
del obj[k]
|
|
182
|
-
|
|
183
|
-
elif isinstance(obj, list):
|
|
184
|
-
for item in obj:
|
|
185
|
-
if isinstance(item, (dict, list)):
|
|
186
|
-
_clean(item)
|
|
187
|
-
|
|
188
|
-
_clean(data)
|
|
189
|
-
return data
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def match_keyword(keyword_expr: str, target_str: str) -> bool:
|
|
193
|
-
"""
|
|
194
|
-
按规则匹配字符串:
|
|
195
|
-
- 空表达式:直接返回 True
|
|
196
|
-
- 单个关键词:直接 in 判断
|
|
197
|
-
- 多关键词:支持 and / or / not / 括号
|
|
198
|
-
"""
|
|
199
|
-
if not keyword_expr.strip():
|
|
200
|
-
return True # 空关键字,默认匹配所有
|
|
201
|
-
|
|
202
|
-
keyword_expr = keyword_expr.strip().lower()
|
|
203
|
-
target_str = target_str.lower()
|
|
204
|
-
|
|
205
|
-
# 单个关键词直接处理
|
|
206
|
-
words = re.findall(r"[^\s()]+", keyword_expr) # 提取非括号非空格的词
|
|
207
|
-
if len(words) == 1:
|
|
208
|
-
return words[0] in target_str
|
|
209
|
-
|
|
210
|
-
# 带逻辑表达式
|
|
211
|
-
# tokens 保留括号,分离 and/or/not/关键词
|
|
212
|
-
tokens = re.findall(r"\(|\)|and|or|not|[^\s()]+", keyword_expr)
|
|
213
|
-
|
|
214
|
-
expr_list = []
|
|
215
|
-
for t in tokens:
|
|
216
|
-
if t in {"and", "or", "not", "(", ")"}:
|
|
217
|
-
expr_list.append(t)
|
|
218
|
-
else:
|
|
219
|
-
# 替换为 True/False
|
|
220
|
-
expr_list.append(str(t in target_str))
|
|
221
|
-
|
|
222
|
-
expr_str = " ".join(expr_list)
|
|
223
|
-
try:
|
|
224
|
-
return eval(expr_str)
|
|
225
|
-
except Exception as e:
|
|
226
|
-
logger.error(f"表达式解析错误: {e}, 表达式: {expr_str}")
|
|
227
|
-
return False
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
def match_mark(keyword_expr: str, target_list: list[str]) -> bool:
|
|
231
|
-
"""
|
|
232
|
-
按规则匹配目标列表:
|
|
233
|
-
- 空表达式:直接返回 True
|
|
234
|
-
- 单个关键词:精准匹配 target_list 中某个元素
|
|
235
|
-
- 多个关键词:支持 and/or/not/括号
|
|
236
|
-
"""
|
|
237
|
-
if not keyword_expr.strip():
|
|
238
|
-
return True # 空关键字,默认匹配所有
|
|
239
|
-
|
|
240
|
-
keyword_expr = keyword_expr.strip().lower()
|
|
241
|
-
target_list = [s.lower() for s in target_list]
|
|
242
|
-
|
|
243
|
-
def contains(word: str) -> bool:
|
|
244
|
-
"""精准匹配:word 必须等于 target_list 的某个元素"""
|
|
245
|
-
return word in target_list
|
|
246
|
-
|
|
247
|
-
# tokens:保留括号,拆分 and/or/not/关键词
|
|
248
|
-
tokens = re.findall(r"\(|\)|and|or|not|[^\s()]+", keyword_expr)
|
|
249
|
-
|
|
250
|
-
expr_list = []
|
|
251
|
-
for t in tokens:
|
|
252
|
-
if t in {"and", "or", "not", "(", ")"}:
|
|
253
|
-
expr_list.append(t)
|
|
254
|
-
else:
|
|
255
|
-
expr_list.append(str(contains(t)))
|
|
256
|
-
|
|
257
|
-
expr_str = " ".join(expr_list)
|
|
258
|
-
try:
|
|
259
|
-
return eval(expr_str)
|
|
260
|
-
except Exception as e:
|
|
261
|
-
logger.error(f"表达式解析错误: {e}, 表达式: {expr_str}")
|
|
262
|
-
return False
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
def init_allure(params):
|
|
266
|
-
"""设置allure中case的 title, description, level"""
|
|
267
|
-
case_level_map = {
|
|
268
|
-
"p0": allure.severity_level.BLOCKER,
|
|
269
|
-
"p1": allure.severity_level.CRITICAL,
|
|
270
|
-
"p2": allure.severity_level.NORMAL,
|
|
271
|
-
"p3": allure.severity_level.MINOR,
|
|
272
|
-
"p4": allure.severity_level.TRIVIAL,
|
|
273
|
-
}
|
|
274
|
-
allure.dynamic.title(params.get("title"))
|
|
275
|
-
allure.dynamic.description(params.get("describe"))
|
|
276
|
-
allure.dynamic.severity(case_level_map.get(params.get("level")))
|
|
277
|
-
allure.dynamic.feature(params.get("module"))
|
|
278
|
-
allure.dynamic.story(params.get("describe"))
|
|
279
|
-
|
|
280
|
-
|
|
281
77
|
def pytest_configure(config):
|
|
282
78
|
"""
|
|
283
79
|
初始化时被调用,可以用于设置全局状态或配置
|
|
@@ -294,32 +90,13 @@ def pytest_configure(config):
|
|
|
294
90
|
sys.path.append(settings.CASES_DIR)
|
|
295
91
|
|
|
296
92
|
|
|
297
|
-
def pytest_unconfigure(config):
|
|
298
|
-
app = CONTEXT.get("app")
|
|
299
|
-
app_handlers = getattr(settings, "APP_FINISH_HANDLER_CLASSES", {})
|
|
300
|
-
if app:
|
|
301
|
-
handlers = app_handlers.get(app) or []
|
|
302
|
-
else:
|
|
303
|
-
handlers = [h for value in app_handlers.values() for h in value] or []
|
|
304
|
-
global_handlers = getattr(settings, "GLOBAL_FINISH_HANDLER_CLASSES", [])
|
|
305
|
-
handlers.extend(global_handlers)
|
|
306
|
-
for handler in handlers:
|
|
307
|
-
try:
|
|
308
|
-
module_path, class_name = handler.rsplit(".", 1)
|
|
309
|
-
cls = getattr(importlib.import_module(module_path), class_name)
|
|
310
|
-
cls().run()
|
|
311
|
-
except Exception as e:
|
|
312
|
-
logger.error(str(e))
|
|
313
|
-
traceback.print_exc()
|
|
314
|
-
pytest.exit(ExitCode.GLOBAL_SCRIPT_ERROR)
|
|
315
|
-
|
|
316
|
-
|
|
317
93
|
def pytest_generate_tests(metafunc):
|
|
318
94
|
"""
|
|
319
95
|
生成(多个)对测试函数的参数化调用
|
|
320
96
|
:param metafunc:
|
|
321
97
|
:return:
|
|
322
98
|
"""
|
|
99
|
+
|
|
323
100
|
keyword_expr = metafunc.config.getoption("keyword")
|
|
324
101
|
mark_expr = metafunc.config.getoption("markexpr")
|
|
325
102
|
node_id = metafunc.definition.keywords.node.nodeid
|
|
@@ -513,115 +290,16 @@ def pytest_collection_modifyitems(items):
|
|
|
513
290
|
item.add_marker(mark)
|
|
514
291
|
|
|
515
292
|
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
""
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
for item in
|
|
523
|
-
key = key_func(item)
|
|
524
|
-
if key is None:
|
|
525
|
-
continue # 跳过key为None的元素
|
|
293
|
+
def pytest_collection_finish(session):
|
|
294
|
+
"""获取最终排序后的 items 列表"""
|
|
295
|
+
logger.info(f"共收集到 {len(session.items)} 个测试用例")
|
|
296
|
+
if not session.items:
|
|
297
|
+
pytest.exit("未收集到用例")
|
|
298
|
+
# 过滤掉item名称是test_setup或test_teardown的
|
|
299
|
+
session.items = [item for item in session.items if item.name not in ["test_setup", "test_teardown"]]
|
|
526
300
|
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
if current_group:
|
|
530
|
-
yield current_key, current_group
|
|
531
|
-
current_key = key
|
|
532
|
-
current_group = [item]
|
|
533
|
-
else:
|
|
534
|
-
current_group.append(item)
|
|
535
|
-
|
|
536
|
-
# 输出最后一组
|
|
537
|
-
if current_group:
|
|
538
|
-
yield current_key, current_group
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
# UPDATE:按类的全路径 进行用例分组 同一个类的test方法分到一组
|
|
542
|
-
def __get_group_key__(item):
|
|
543
|
-
return '::'.join(item.nodeid.split('::')[:2])
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
# UPDATE:改变pytest的原始排序规则
|
|
547
|
-
def sort(case_items):
|
|
548
|
-
# 按测试类全路径分类,同一个类文件的用例归集到一起
|
|
549
|
-
# 使用 groupby 函数进行分组
|
|
550
|
-
item_group_list = [list(group) for _, group in filtered_groupby(case_items, __get_group_key__)]
|
|
551
|
-
|
|
552
|
-
all_item_list = []
|
|
553
|
-
clase_id = None
|
|
554
|
-
for items in item_group_list:
|
|
555
|
-
# 未被test_setup/test_teardown 标记的test方法
|
|
556
|
-
non_custom_scope_items = [item for item in items if
|
|
557
|
-
'test_setup' != item.originalname and 'test_teardown' != item.originalname]
|
|
558
|
-
item_list = []
|
|
559
|
-
# 用例的组数
|
|
560
|
-
case_suite_num = 0
|
|
561
|
-
# 生成每个组当前的索引
|
|
562
|
-
ori_name_temp = None
|
|
563
|
-
ori_name_list = []
|
|
564
|
-
|
|
565
|
-
for item in non_custom_scope_items:
|
|
566
|
-
clase_id = item.cls.__name__
|
|
567
|
-
original_name = item.originalname
|
|
568
|
-
|
|
569
|
-
if ori_name_temp is None or ori_name_temp == original_name:
|
|
570
|
-
ori_name_temp = original_name
|
|
571
|
-
case_suite_num += 1
|
|
572
|
-
ori_name_list.append([original_name, item])
|
|
573
|
-
else:
|
|
574
|
-
break
|
|
575
|
-
|
|
576
|
-
# 根据组数 创建各组的数组 并插入第一个case
|
|
577
|
-
case_dict = dict()
|
|
578
|
-
try:
|
|
579
|
-
for i in range(case_suite_num):
|
|
580
|
-
item = ori_name_list[i][1]
|
|
581
|
-
id = item.callspec.id
|
|
582
|
-
|
|
583
|
-
first_part = id.split('#', 1)[-1]
|
|
584
|
-
index = first_part.split(']')[0]
|
|
585
|
-
case_dict[index] = [item]
|
|
586
|
-
except:
|
|
587
|
-
pass
|
|
588
|
-
|
|
589
|
-
new_start_index = case_suite_num
|
|
590
|
-
# 以new_start_index为起点 重新遍历items
|
|
591
|
-
try:
|
|
592
|
-
for i in range(new_start_index, len(non_custom_scope_items)):
|
|
593
|
-
item = non_custom_scope_items[i]
|
|
594
|
-
id = item.callspec.id
|
|
595
|
-
first_part = id.split('#', 1)[-1]
|
|
596
|
-
index = first_part.split(']')[0]
|
|
597
|
-
case_dict.get(index).append(item)
|
|
598
|
-
except:
|
|
599
|
-
pass
|
|
600
|
-
|
|
601
|
-
index = 0
|
|
602
|
-
for id in case_dict:
|
|
603
|
-
index += 1
|
|
604
|
-
case_item_list = case_dict.get(id)
|
|
605
|
-
for item in case_item_list:
|
|
606
|
-
allure_suite_mark = f'{clase_id}#{index}'
|
|
607
|
-
setattr(item, 'allure_suite_mark', allure_suite_mark)
|
|
608
|
-
item_list += case_item_list
|
|
609
|
-
|
|
610
|
-
all_item_list += item_list
|
|
611
|
-
|
|
612
|
-
return all_item_list
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
def pytest_collection_finish(session):
|
|
616
|
-
"""获取最终排序后的 items 列表"""
|
|
617
|
-
logger.info(f"共收集到 {len(session.items)} 个测试用例")
|
|
618
|
-
if not session.items:
|
|
619
|
-
pytest.exit("未收集到用例")
|
|
620
|
-
# 过滤掉item名称是test_setup或test_teardown的
|
|
621
|
-
session.items = [item for item in session.items if item.name not in ["test_setup", "test_teardown"]]
|
|
622
|
-
|
|
623
|
-
# 1. 筛选出带井号 名称带'#' 的item,并记录原始索引
|
|
624
|
-
hash_items_with_index = [(index, item) for index, item in enumerate(session.items) if "#" in item.name]
|
|
301
|
+
# 1. 筛选出带井号 名称带'#' 的item,并记录原始索引
|
|
302
|
+
hash_items_with_index = [(index, item) for index, item in enumerate(session.items) if "#" in item.name]
|
|
625
303
|
|
|
626
304
|
# 2. 按照 'cls' 对带井号的元素进行分组
|
|
627
305
|
grouped_by_cls = {}
|
|
@@ -678,6 +356,26 @@ def pytest_runtestloop(session):
|
|
|
678
356
|
pytest.exit(ExitCode.GLOBAL_SCRIPT_ERROR)
|
|
679
357
|
|
|
680
358
|
|
|
359
|
+
def pytest_sessionfinish(session, exitstatus):
|
|
360
|
+
app = CONTEXT.get("app")
|
|
361
|
+
app_handlers = getattr(settings, "APP_FINISH_HANDLER_CLASSES", {})
|
|
362
|
+
if app:
|
|
363
|
+
handlers = app_handlers.get(app) or []
|
|
364
|
+
else:
|
|
365
|
+
handlers = [h for value in app_handlers.values() for h in value] or []
|
|
366
|
+
global_handlers = getattr(settings, "GLOBAL_FINISH_HANDLER_CLASSES", [])
|
|
367
|
+
handlers.extend(global_handlers)
|
|
368
|
+
for handler in handlers:
|
|
369
|
+
try:
|
|
370
|
+
module_path, class_name = handler.rsplit(".", 1)
|
|
371
|
+
cls = getattr(importlib.import_module(module_path), class_name)
|
|
372
|
+
cls().run()
|
|
373
|
+
except Exception as e:
|
|
374
|
+
logger.error(str(e))
|
|
375
|
+
traceback.print_exc()
|
|
376
|
+
pytest.exit(ExitCode.GLOBAL_SCRIPT_ERROR)
|
|
377
|
+
|
|
378
|
+
|
|
681
379
|
def pytest_runtest_setup(item):
|
|
682
380
|
allure.dynamic.sub_suite(item.allure_suite_mark)
|
|
683
381
|
if item.funcargs.get("first"):
|
|
@@ -841,3 +539,305 @@ def pytest_exception_interact(node, call, report):
|
|
|
841
539
|
"""
|
|
842
540
|
if call.excinfo.type is AssertionError:
|
|
843
541
|
logger.error(f"{node.nodeid} failed: {call.excinfo.value}\n")
|
|
542
|
+
|
|
543
|
+
|
|
544
|
+
def init_mysql():
|
|
545
|
+
"""初始化 MySQL 连接池"""
|
|
546
|
+
try:
|
|
547
|
+
mysql_config = CONFIG.get(app=all_app[0], key="mysql")
|
|
548
|
+
mysql_conns = {item: MysqlDB(**mysql_config[item]) for item in mysql_config}
|
|
549
|
+
for app in all_app:
|
|
550
|
+
_FRAMEWORK_CONTEXT.set(app=app, key="mysql", value=mysql_conns)
|
|
551
|
+
except Exception as e:
|
|
552
|
+
raise MysqlDBError(e)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def init_redis():
|
|
556
|
+
"""初始化 Redis 连接池(16个db)"""
|
|
557
|
+
try:
|
|
558
|
+
redis_config = CONFIG.get(app=all_app[0], key="redis")
|
|
559
|
+
redis_conns = {
|
|
560
|
+
db: [RedisDB(**{**db_info, "db": i}) for i in range(16)]
|
|
561
|
+
for db, db_info in redis_config.items()
|
|
562
|
+
}
|
|
563
|
+
for app in all_app:
|
|
564
|
+
_FRAMEWORK_CONTEXT.set(app=app, key="redis", value=redis_conns)
|
|
565
|
+
except Exception as e:
|
|
566
|
+
raise RedisDBError(e)
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
def inner_login(app):
|
|
570
|
+
"""单个系统的登录和资源初始化"""
|
|
571
|
+
|
|
572
|
+
login_cls = getattr(module, f"{snake_to_pascal(app)}Login")
|
|
573
|
+
setattr(Http, app, login_cls(app))
|
|
574
|
+
# Token 过期时间写入上下文
|
|
575
|
+
token_expiry = CONTEXT.get(app).get("token_expiry")
|
|
576
|
+
if token_expiry:
|
|
577
|
+
expire_time = datetime.now() + timedelta(seconds=token_expiry)
|
|
578
|
+
_FRAMEWORK_CONTEXT.set(app=app, key="expire_time", value=expire_time)
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def login():
|
|
582
|
+
def safe_call(func, name):
|
|
583
|
+
try:
|
|
584
|
+
logger.info(f"初始化{name}连接...")
|
|
585
|
+
func()
|
|
586
|
+
logger.info(f"{name}连接初始化成功")
|
|
587
|
+
except (MysqlDBError, RedisDBError) as e:
|
|
588
|
+
logger.error(f"{name}连接初始化异常: {e}")
|
|
589
|
+
|
|
590
|
+
# 启动 MySQL 和 Redis 初始化线程
|
|
591
|
+
threads = [
|
|
592
|
+
threading.Thread(target=safe_call, args=(init_mysql, "MySQL")),
|
|
593
|
+
threading.Thread(target=safe_call, args=(init_redis, "Redis"))
|
|
594
|
+
]
|
|
595
|
+
|
|
596
|
+
for t in threads:
|
|
597
|
+
t.start()
|
|
598
|
+
for t in threads:
|
|
599
|
+
t.join()
|
|
600
|
+
logger.info("登录账号".center(80, "*"))
|
|
601
|
+
with ThreadPoolExecutor(max_workers=len(all_app)) as executor:
|
|
602
|
+
futures = {executor.submit(inner_login, app): app for app in all_app}
|
|
603
|
+
for future in as_completed(futures):
|
|
604
|
+
future.result()
|
|
605
|
+
|
|
606
|
+
logger.info("登录完成".center(80, "*"))
|
|
607
|
+
return Http
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def find_data_path_by_case(app, case_file_name):
|
|
611
|
+
"""
|
|
612
|
+
基于case文件名称查找与之对应的yml文件路径
|
|
613
|
+
:param app:
|
|
614
|
+
:param case_file_name:
|
|
615
|
+
:return:
|
|
616
|
+
"""
|
|
617
|
+
env = CONTEXT.get("env")
|
|
618
|
+
for file_path in Path(os.path.join(settings.DATA_DIR, env, app)).rglob(f"{case_file_name}.y*"):
|
|
619
|
+
if file_path:
|
|
620
|
+
return file_path
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
def subset_and_diff(small: set, big: set):
|
|
624
|
+
"""
|
|
625
|
+
判断 small 是否为 big 的子集,并返回差集
|
|
626
|
+
"""
|
|
627
|
+
return small.issubset(big), small - big
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def disable_field(scenario, data):
|
|
631
|
+
if not scenario:
|
|
632
|
+
return data
|
|
633
|
+
|
|
634
|
+
def _clean(obj):
|
|
635
|
+
if isinstance(obj, dict):
|
|
636
|
+
keys_to_delete = []
|
|
637
|
+
for k, v in obj.items():
|
|
638
|
+
if isinstance(v, (dict, list)):
|
|
639
|
+
_clean(v)
|
|
640
|
+
elif isinstance(v, str):
|
|
641
|
+
for ak, av in scenario.items():
|
|
642
|
+
if av == "disable" and v == f"${{{ak}}}":
|
|
643
|
+
keys_to_delete.append(k)
|
|
644
|
+
break
|
|
645
|
+
# 统一删除,避免边遍历边删
|
|
646
|
+
for k in keys_to_delete:
|
|
647
|
+
del obj[k]
|
|
648
|
+
|
|
649
|
+
elif isinstance(obj, list):
|
|
650
|
+
for item in obj:
|
|
651
|
+
if isinstance(item, (dict, list)):
|
|
652
|
+
_clean(item)
|
|
653
|
+
|
|
654
|
+
_clean(data)
|
|
655
|
+
return data
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
def match_keyword(keyword_expr: str, target_str: str) -> bool:
|
|
659
|
+
"""
|
|
660
|
+
按规则匹配字符串:
|
|
661
|
+
- 空表达式:直接返回 True
|
|
662
|
+
- 单个关键词:直接 in 判断
|
|
663
|
+
- 多关键词:支持 and / or / not / 括号
|
|
664
|
+
"""
|
|
665
|
+
if not keyword_expr.strip():
|
|
666
|
+
return True # 空关键字,默认匹配所有
|
|
667
|
+
|
|
668
|
+
keyword_expr = keyword_expr.strip().lower()
|
|
669
|
+
target_str = target_str.lower()
|
|
670
|
+
|
|
671
|
+
# 单个关键词直接处理
|
|
672
|
+
words = re.findall(r"[^\s()]+", keyword_expr) # 提取非括号非空格的词
|
|
673
|
+
if len(words) == 1:
|
|
674
|
+
return words[0] in target_str
|
|
675
|
+
|
|
676
|
+
# 带逻辑表达式
|
|
677
|
+
# tokens 保留括号,分离 and/or/not/关键词
|
|
678
|
+
tokens = re.findall(r"\(|\)|and|or|not|[^\s()]+", keyword_expr)
|
|
679
|
+
|
|
680
|
+
expr_list = []
|
|
681
|
+
for t in tokens:
|
|
682
|
+
if t in {"and", "or", "not", "(", ")"}:
|
|
683
|
+
expr_list.append(t)
|
|
684
|
+
else:
|
|
685
|
+
# 替换为 True/False
|
|
686
|
+
expr_list.append(str(t in target_str))
|
|
687
|
+
|
|
688
|
+
expr_str = " ".join(expr_list)
|
|
689
|
+
try:
|
|
690
|
+
return eval(expr_str)
|
|
691
|
+
except Exception as e:
|
|
692
|
+
logger.error(f"表达式解析错误: {e}, 表达式: {expr_str}")
|
|
693
|
+
return False
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def match_mark(keyword_expr: str, target_list: list[str]) -> bool:
|
|
697
|
+
"""
|
|
698
|
+
按规则匹配目标列表:
|
|
699
|
+
- 空表达式:直接返回 True
|
|
700
|
+
- 单个关键词:精准匹配 target_list 中某个元素
|
|
701
|
+
- 多个关键词:支持 and/or/not/括号
|
|
702
|
+
"""
|
|
703
|
+
if not keyword_expr.strip():
|
|
704
|
+
return True # 空关键字,默认匹配所有
|
|
705
|
+
|
|
706
|
+
keyword_expr = keyword_expr.strip().lower()
|
|
707
|
+
target_list = [s.lower() for s in target_list]
|
|
708
|
+
|
|
709
|
+
def contains(word: str) -> bool:
|
|
710
|
+
"""精准匹配:word 必须等于 target_list 的某个元素"""
|
|
711
|
+
return word in target_list
|
|
712
|
+
|
|
713
|
+
# tokens:保留括号,拆分 and/or/not/关键词
|
|
714
|
+
tokens = re.findall(r"\(|\)|and|or|not|[^\s()]+", keyword_expr)
|
|
715
|
+
|
|
716
|
+
expr_list = []
|
|
717
|
+
for t in tokens:
|
|
718
|
+
if t in {"and", "or", "not", "(", ")"}:
|
|
719
|
+
expr_list.append(t)
|
|
720
|
+
else:
|
|
721
|
+
expr_list.append(str(contains(t)))
|
|
722
|
+
|
|
723
|
+
expr_str = " ".join(expr_list)
|
|
724
|
+
try:
|
|
725
|
+
return eval(expr_str)
|
|
726
|
+
except Exception as e:
|
|
727
|
+
logger.error(f"表达式解析错误: {e}, 表达式: {expr_str}")
|
|
728
|
+
return False
|
|
729
|
+
|
|
730
|
+
|
|
731
|
+
def init_allure(params):
|
|
732
|
+
"""设置allure中case的 title, description, level"""
|
|
733
|
+
case_level_map = {
|
|
734
|
+
"p0": allure.severity_level.BLOCKER,
|
|
735
|
+
"p1": allure.severity_level.CRITICAL,
|
|
736
|
+
"p2": allure.severity_level.NORMAL,
|
|
737
|
+
"p3": allure.severity_level.MINOR,
|
|
738
|
+
"p4": allure.severity_level.TRIVIAL,
|
|
739
|
+
}
|
|
740
|
+
allure.dynamic.title(params.get("title"))
|
|
741
|
+
allure.dynamic.description(params.get("describe"))
|
|
742
|
+
allure.dynamic.severity(case_level_map.get(params.get("level")))
|
|
743
|
+
allure.dynamic.feature(params.get("module"))
|
|
744
|
+
allure.dynamic.story(params.get("describe"))
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
# UPDATE 将用例 按类名进行分组的核心方法
|
|
748
|
+
def filtered_groupby(iterable, key_func):
|
|
749
|
+
"""生成器:过滤key_func返回None的元素,并按key_func分组"""
|
|
750
|
+
current_key = None
|
|
751
|
+
current_group = []
|
|
752
|
+
|
|
753
|
+
for item in iterable:
|
|
754
|
+
key = key_func(item)
|
|
755
|
+
if key is None:
|
|
756
|
+
continue # 跳过key为None的元素
|
|
757
|
+
|
|
758
|
+
# 处理分组逻辑(类似groupby)
|
|
759
|
+
if key != current_key:
|
|
760
|
+
if current_group:
|
|
761
|
+
yield current_key, current_group
|
|
762
|
+
current_key = key
|
|
763
|
+
current_group = [item]
|
|
764
|
+
else:
|
|
765
|
+
current_group.append(item)
|
|
766
|
+
|
|
767
|
+
# 输出最后一组
|
|
768
|
+
if current_group:
|
|
769
|
+
yield current_key, current_group
|
|
770
|
+
|
|
771
|
+
|
|
772
|
+
# UPDATE:按类的全路径 进行用例分组 同一个类的test方法分到一组
|
|
773
|
+
def __get_group_key__(item):
|
|
774
|
+
return '::'.join(item.nodeid.split('::')[:2])
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
# UPDATE:改变pytest的原始排序规则
|
|
778
|
+
def sort(case_items):
|
|
779
|
+
# 按测试类全路径分类,同一个类文件的用例归集到一起
|
|
780
|
+
# 使用 groupby 函数进行分组
|
|
781
|
+
item_group_list = [list(group) for _, group in filtered_groupby(case_items, __get_group_key__)]
|
|
782
|
+
|
|
783
|
+
all_item_list = []
|
|
784
|
+
clase_id = None
|
|
785
|
+
for items in item_group_list:
|
|
786
|
+
# 未被test_setup/test_teardown 标记的test方法
|
|
787
|
+
non_custom_scope_items = [item for item in items if
|
|
788
|
+
'test_setup' != item.originalname and 'test_teardown' != item.originalname]
|
|
789
|
+
item_list = []
|
|
790
|
+
# 用例的组数
|
|
791
|
+
case_suite_num = 0
|
|
792
|
+
# 生成每个组当前的索引
|
|
793
|
+
ori_name_temp = None
|
|
794
|
+
ori_name_list = []
|
|
795
|
+
|
|
796
|
+
for item in non_custom_scope_items:
|
|
797
|
+
clase_id = item.cls.__name__
|
|
798
|
+
original_name = item.originalname
|
|
799
|
+
|
|
800
|
+
if ori_name_temp is None or ori_name_temp == original_name:
|
|
801
|
+
ori_name_temp = original_name
|
|
802
|
+
case_suite_num += 1
|
|
803
|
+
ori_name_list.append([original_name, item])
|
|
804
|
+
else:
|
|
805
|
+
break
|
|
806
|
+
|
|
807
|
+
# 根据组数 创建各组的数组 并插入第一个case
|
|
808
|
+
case_dict = dict()
|
|
809
|
+
try:
|
|
810
|
+
for i in range(case_suite_num):
|
|
811
|
+
item = ori_name_list[i][1]
|
|
812
|
+
id = item.callspec.id
|
|
813
|
+
|
|
814
|
+
first_part = id.split('#', 1)[-1]
|
|
815
|
+
index = first_part.split(']')[0]
|
|
816
|
+
case_dict[index] = [item]
|
|
817
|
+
except:
|
|
818
|
+
pass
|
|
819
|
+
|
|
820
|
+
new_start_index = case_suite_num
|
|
821
|
+
# 以new_start_index为起点 重新遍历items
|
|
822
|
+
try:
|
|
823
|
+
for i in range(new_start_index, len(non_custom_scope_items)):
|
|
824
|
+
item = non_custom_scope_items[i]
|
|
825
|
+
id = item.callspec.id
|
|
826
|
+
first_part = id.split('#', 1)[-1]
|
|
827
|
+
index = first_part.split(']')[0]
|
|
828
|
+
case_dict.get(index).append(item)
|
|
829
|
+
except:
|
|
830
|
+
pass
|
|
831
|
+
|
|
832
|
+
index = 0
|
|
833
|
+
for id in case_dict:
|
|
834
|
+
index += 1
|
|
835
|
+
case_item_list = case_dict.get(id)
|
|
836
|
+
for item in case_item_list:
|
|
837
|
+
allure_suite_mark = f'{clase_id}#{index}'
|
|
838
|
+
setattr(item, 'allure_suite_mark', allure_suite_mark)
|
|
839
|
+
item_list += case_item_list
|
|
840
|
+
|
|
841
|
+
all_item_list += item_list
|
|
842
|
+
|
|
843
|
+
return all_item_list
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/db/mysql_db.py
RENAMED
|
@@ -5,8 +5,9 @@ from datetime import datetime
|
|
|
5
5
|
import retry
|
|
6
6
|
import pymysql
|
|
7
7
|
from pymysql import MySQLError
|
|
8
|
-
|
|
9
8
|
from dbutils.pooled_db import PooledDB
|
|
9
|
+
|
|
10
|
+
import config.settings as settings
|
|
10
11
|
from framework.utils.log_util import logger
|
|
11
12
|
from framework.exceptions import MysqlDBError
|
|
12
13
|
|
|
@@ -15,10 +16,16 @@ def safe_mysql_call(func):
|
|
|
15
16
|
"""装饰器:捕获 Mysql 异常并转成 MysqlDBError"""
|
|
16
17
|
|
|
17
18
|
def wrapper(self, *args, **kwargs):
|
|
19
|
+
sql = None
|
|
20
|
+
# 从参数中尝试提取 SQL
|
|
21
|
+
if len(args) > 0 and isinstance(args[0], str):
|
|
22
|
+
sql = args[0]
|
|
23
|
+
elif 'sql' in kwargs:
|
|
24
|
+
sql = kwargs['sql']
|
|
18
25
|
try:
|
|
19
26
|
return func(self, *args, **kwargs)
|
|
20
27
|
except MySQLError as e:
|
|
21
|
-
logger.error(f"{func.__name__}
|
|
28
|
+
logger.error(f"{func.__name__}执行SQL出错: {sql}: {e}")
|
|
22
29
|
raise MysqlDBError(e)
|
|
23
30
|
except Exception as e:
|
|
24
31
|
logger.error(f"{func.__name__} 未知错误: {e}")
|
|
@@ -31,11 +38,11 @@ class MysqlDB:
|
|
|
31
38
|
def __init__(self, host, username, password, port, db, max_connections=5):
|
|
32
39
|
self.__create_pool(host, username, password, port, db, max_connections)
|
|
33
40
|
|
|
34
|
-
@retry.retry(tries=5, delay=3)
|
|
41
|
+
@retry.retry(tries=getattr(settings, "MYSQL_RETRIES", 5), delay=getattr(settings, "MYSQL_DELAY", 3))
|
|
35
42
|
def __create_pool(self, host, username, password, port, db, max_connections):
|
|
36
43
|
self.pool = PooledDB(
|
|
37
44
|
creator=pymysql,
|
|
38
|
-
maxconnections=
|
|
45
|
+
maxconnections= getattr(settings, "MYSQL_MAX_CONNECTIONS", 5),
|
|
39
46
|
mincached=1,
|
|
40
47
|
maxcached=max_connections,
|
|
41
48
|
blocking=True,
|
|
@@ -136,4 +143,4 @@ class MysqlDB:
|
|
|
136
143
|
return obj.strftime('%Y-%m-%d %H:%M:%S') # 转换时间格式
|
|
137
144
|
elif isinstance(obj, Decimal):
|
|
138
145
|
return float(obj) # 转换 Decimal 为 float
|
|
139
|
-
raise TypeError(f"Type {type(obj)} not serializable")
|
|
146
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/global_attribute.py
RENAMED
|
@@ -91,6 +91,7 @@ class GlobalAttribute(object):
|
|
|
91
91
|
self.set(k, v)
|
|
92
92
|
|
|
93
93
|
def set_from_yaml(self, filename, env, app=None):
|
|
94
|
+
file = None
|
|
94
95
|
try:
|
|
95
96
|
file = os.path.join(ROOT_DIR, filename)
|
|
96
97
|
if not os.path.exists(file):
|
|
@@ -99,9 +100,11 @@ class GlobalAttribute(object):
|
|
|
99
100
|
self.set_from_dict(dict(Box().from_yaml(filename=file, Loader=NoDatesSafeLoader).get(env)), app)
|
|
100
101
|
except BoxError as e:
|
|
101
102
|
logger.error(f"{file}文件内容不是字典类型:{e}")
|
|
102
|
-
traceback.print_exc()
|
|
103
103
|
pytest.exit(ExitCode.CONTEXT_YAML_DATA_FORMAT_ERROR)
|
|
104
104
|
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.warning(f"{filename}获取{env}环境信息异常: {e}")
|
|
107
|
+
|
|
105
108
|
def delete(self, key):
|
|
106
109
|
delattr(self, key)
|
|
107
110
|
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/mock_util.py
RENAMED
|
@@ -9,7 +9,7 @@ from config.settings import AVAILABLE_ENVS, ENV_CONFIG
|
|
|
9
9
|
class PaymentType(Enum):
|
|
10
10
|
"""支付类型枚举"""
|
|
11
11
|
DEPOSIT = 0 # 入金
|
|
12
|
-
|
|
12
|
+
WALLET_SCANNING = 1 # 出金
|
|
13
13
|
CHECKOUT = 2 # 结账
|
|
14
14
|
|
|
15
15
|
|
|
@@ -22,6 +22,7 @@ class RiskLevel(Enum):
|
|
|
22
22
|
MEDIUM_HIGH = 3 # 中高风险
|
|
23
23
|
HIGH = 4 # 高风险
|
|
24
24
|
SEVERE = 5 # 严重风险
|
|
25
|
+
PENDING_KYT = 6 # 严重风险
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
@unique
|
|
@@ -40,7 +40,7 @@ class Validate(object):
|
|
|
40
40
|
def valid(self, validates):
|
|
41
41
|
for valid_item in validates:
|
|
42
42
|
key = list(valid_item.keys())[0]
|
|
43
|
-
valid_list = [i.strip() for i in valid_item.get(key).split(",")]
|
|
43
|
+
valid_list = [i.strip() for i in valid_item.get(key).split(",", 1)]
|
|
44
44
|
expression = valid_list[0]
|
|
45
45
|
try:
|
|
46
46
|
expectant_result = valid_list[1]
|
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/base_class.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/db/__init__.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/db/redis_db.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/exceptions.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/exit_code.py
RENAMED
|
File without changes
|
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/http_client.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/render_data.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/__init__.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/common.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/date_util.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/encrypt.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/log_util.py
RENAMED
|
File without changes
|
{pytest_api_framework_alpha-0.3.4 → pytest_api_framework_alpha-0.3.5}/framework/utils/yaml_util.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|