pytest-api-framework-alpha 0.3.18__tar.gz → 0.3.20__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.
Files changed (34) hide show
  1. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/PKG-INFO +1 -1
  2. pytest_api_framework_alpha-0.3.20/framework/assert_webhook.py +78 -0
  3. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/base_class.py +97 -3
  4. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/conftest.py +5 -1
  5. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/db/redis_db.py +7 -1
  6. pytest_api_framework_alpha-0.3.20/framework/retry_assert.py +58 -0
  7. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/utils/lark_util.py +1 -1
  8. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/validate.py +3 -3
  9. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/pytest_api_framework_alpha.egg-info/PKG-INFO +1 -1
  10. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/pytest_api_framework_alpha.egg-info/SOURCES.txt +2 -0
  11. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/setup.py +1 -1
  12. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/__init__.py +0 -0
  13. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/db/__init__.py +0 -0
  14. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/db/mysql_db.py +0 -0
  15. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/exceptions.py +0 -0
  16. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/exit_code.py +0 -0
  17. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/extract.py +0 -0
  18. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/global_attribute.py +0 -0
  19. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/http_client.py +0 -0
  20. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/render_data.py +0 -0
  21. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/report.py +0 -0
  22. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/script.py +0 -0
  23. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/startapp.py +0 -0
  24. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/utils/__init__.py +0 -0
  25. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/utils/common.py +0 -0
  26. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/utils/date_util.py +0 -0
  27. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/utils/encrypt.py +0 -0
  28. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/utils/log_util.py +0 -0
  29. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/utils/mock_util.py +0 -0
  30. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/framework/utils/yaml_util.py +0 -0
  31. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/pytest_api_framework_alpha.egg-info/dependency_links.txt +0 -0
  32. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/pytest_api_framework_alpha.egg-info/requires.txt +0 -0
  33. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/pytest_api_framework_alpha.egg-info/top_level.txt +0 -0
  34. {pytest_api_framework_alpha-0.3.18 → pytest_api_framework_alpha-0.3.20}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-api-framework-alpha
3
- Version: 0.3.18
3
+ Version: 0.3.20
4
4
  Author: alpha
5
5
  Author-email:
6
6
  Requires-Python: >=3.6
@@ -0,0 +1,78 @@
1
+ import re
2
+ from box import Box, BoxList
3
+ from framework.utils.log_util import logger
4
+
5
+
6
+ class AssertWebhook(object):
7
+
8
+ def __init__(self, webhook):
9
+ self.webhook_format = "dict"
10
+ if isinstance(webhook, dict):
11
+ self.webhook = Box(webhook)
12
+ elif isinstance(webhook, list):
13
+ self.webhook = BoxList(webhook)
14
+ self.webhook_format = "list"
15
+ else:
16
+ self.webhook = webhook
17
+ self.webhook_format = "other"
18
+
19
+ def assert_equal(self, expression, expected_result, desc=None):
20
+ """
21
+ 校验webhook中的数据等于预期结果
22
+ :param expression:
23
+ :param expected_result:
24
+ :param desc:
25
+ :return:
26
+ """
27
+ if self.webhook_format == "other":
28
+ logger.warning(f"不支持非标准格式的webhook(json)校验")
29
+ return
30
+ practical_result = self.get_nested_value(self.webhook, expression)
31
+ logger.info(f"WEBHOOK断言: 断言类型: assert_equal, 断言结果: {practical_result} == {expected_result}")
32
+ assert practical_result == expected_result, f"{desc},实际结果: {practical_result}"
33
+ return self
34
+
35
+ def assert_is_null(self, expression, desc=None):
36
+ """
37
+ 校验webhook中的数据等于预期结果
38
+ :param expression:
39
+ :param desc:
40
+ :return:
41
+ """
42
+ if self.webhook_format == "other":
43
+ logger.warning(f"不支持非标准格式的webhook(json)校验")
44
+ return
45
+ practical_result = self.get_nested_value(self.webhook, expression)
46
+ logger.info(f"WEBHOOK断言: 断言类型: assert_is_null, 断言结果: {practical_result}")
47
+ assert not practical_result, f"{desc},实际结果: {practical_result}"
48
+ return self
49
+
50
+ def assert_not_null(self, expression, desc=None):
51
+ """
52
+ 校验webhook中的数据等于预期结果
53
+ :param expression:
54
+ :param desc:
55
+ :return:
56
+ """
57
+ if self.webhook_format == "other":
58
+ logger.warning(f"不支持非标准格式的webhook(json)校验")
59
+ return
60
+ practical_result = self.get_nested_value(self.webhook, expression)
61
+ logger.info(f"WEBHOOK断言: 断言类型: assert_not_null, 断言结果: {practical_result}")
62
+ assert practical_result is not None, f"{desc},实际结果: {practical_result}"
63
+ return self
64
+
65
+ @staticmethod
66
+ def get_nested_value(obj, attr_path):
67
+ """通过字符串路径(如 'a.b[0].c')获取嵌套属性值"""
68
+ # 使用正则表达式分解路径,支持属性和索引的组合
69
+ path_elements = re.findall(r'(\w+)|\[(\d+)]', attr_path)
70
+ try:
71
+ for attr, index in path_elements:
72
+ if attr: # 属性部分
73
+ obj = getattr(obj, attr)
74
+ if index: # 索引部分
75
+ obj = obj[int(index)]
76
+ return obj
77
+ except Exception:
78
+ return None
@@ -1,3 +1,6 @@
1
+ import uuid
2
+ import time
3
+ import json
1
4
  import traceback
2
5
  import importlib
3
6
  from typing import Union
@@ -15,6 +18,9 @@ from framework.utils.log_util import logger
15
18
  from framework.render_data import RenderData
16
19
  from framework.http_client import ResponseUtil
17
20
  from framework.utils.date_util import DateUtil
21
+ from framework.retry_assert import RetryAssert
22
+ from framework.assert_webhook import AssertWebhook
23
+ from framework.utils.encrypt import b64_encode, b64_decode
18
24
  from framework.utils.common import snake_to_pascal, SingletonFaker
19
25
  from framework.global_attribute import GlobalAttribute, _FRAMEWORK_CONTEXT, CONTEXT
20
26
  from framework.exceptions import ValidateException, RenderException, RequestException, GetAccountError, GetAppHttpError
@@ -22,7 +28,7 @@ from framework.utils.mock_util import get_customized_kytmock, set_customized_kyt
22
28
  mock_mq
23
29
 
24
30
  from handlers.extend_base_test_case_attr import ExtendBaseTestCase
25
- from config.settings import UNAUTHORIZED_CODE, FAKER_LANGUAGE
31
+ from config.settings import UNAUTHORIZED_CODE, FAKER_LANGUAGE, GET_WEBHOOK_TIMEOUT, GET_WEBHOOK_TRIES
26
32
 
27
33
  module = importlib.import_module("test_case.conftest")
28
34
 
@@ -39,6 +45,7 @@ class BaseTestCase(ExtendBaseTestCase):
39
45
  http = None
40
46
  data: Box = None
41
47
  belong_app = None
48
+ retry_assert = RetryAssert.eventually
42
49
  scenario: Scenario = None
43
50
  response: ResponseUtil = None
44
51
  context: Union[GlobalAttribute, Box] = None
@@ -64,6 +71,7 @@ class BaseTestCase(ExtendBaseTestCase):
64
71
  data = RenderData(data).render()
65
72
  data.request.url = self.replace_domain(data.request.url, domain)
66
73
  try:
74
+ self.set_webhook_url(account)
67
75
  self.response = getattr(app_http, account).request(data=data, **kwargs)
68
76
  except AttributeError as e:
69
77
  raise GetAccountError(e)
@@ -173,6 +181,90 @@ class BaseTestCase(ExtendBaseTestCase):
173
181
  )
174
182
  return urlunparse(updated_url)
175
183
 
184
+ def set_webhook_url(self, account):
185
+ """
186
+ 修改webhook url地址,在原地址基础上拼接上/uuid,保证地址唯一
187
+ :param account: 账户名
188
+ :return:
189
+ """
190
+ account_info = self.context.get(self.belong_app).get("accounts").get(account)
191
+ info = self.mysql_conn("db_camp_crm").query(
192
+ f"SELECT webhook_url FROM tbl_participant WHERE participant_code = '{account_info.client_id}';",
193
+ log=False)
194
+ if not info:
195
+ # self.logger.warning(f"账号{account}未配置webhook地址")
196
+ return None
197
+ original_webhook_url = info.get("webhook_url").rsplit("/", 1)[0]
198
+ new_webhook_url = f"{original_webhook_url}/{str(uuid.uuid4()).replace("-", "_")}"
199
+ try:
200
+ self.mysql_conn("db_camp_crm").execute(
201
+ f"UPDATE tbl_participant set webhook_url = '{new_webhook_url}' WHERE participant_code = '{account_info.client_id}';",
202
+ log=False)
203
+ self.context_set(f"{account_info.client_id}_webhook", urlparse(new_webhook_url).path)
204
+ except Exception as e:
205
+ self.logger.error(f"账号{account}修改webhook地址异常: {e}")
206
+
207
+ def get_webhook(self, account, count=None, timeout=GET_WEBHOOK_TIMEOUT, interval=GET_WEBHOOK_TRIES):
208
+ """
209
+ 获取 webhook 日志,若为空则重试
210
+
211
+ :param account: 账户名
212
+ :param count: 期望日志条数(None 表示只要非空即可)
213
+ :param timeout: 最大等待时间(秒)
214
+ :param interval: 重试间隔(秒)
215
+ :return: webhook 日志列表 or []
216
+ """
217
+ account_info = self.context.get(self.belong_app).get("accounts").get(account)
218
+ webhook_url = self.context_get(f"{account_info.client_id}_webhook")
219
+
220
+ if not webhook_url:
221
+ self.logger.warning(f"账号{account}未配置webhook地址")
222
+ return
223
+ webhook_url = webhook_url.replace("/webhook_vmock", "")
224
+ base64_webhook_url = b64_encode(webhook_url.encode("utf8"))
225
+ self.logger.info(f'原始webhook_url: {webhook_url}, base64格式webhook_url: {base64_webhook_url}')
226
+
227
+ redis_key = f"vmock_webhook_log:{base64_webhook_url}"
228
+ redis = self.redis_conn(db="database")
229
+ end_time = time.time() + timeout
230
+ webhook_list = []
231
+ while time.time() < end_time:
232
+ webhook_list = redis.lrange_all(redis_key)
233
+ if webhook_list:
234
+ webhook_list = [
235
+ json.loads(raw[raw.find(b"{"):].decode("utf-8"))
236
+ for raw in webhook_list
237
+ if isinstance(raw, (bytes, bytearray)) and raw.find(b"{") != -1
238
+ ]
239
+ current_count = len(webhook_list)
240
+ if count is None:
241
+ self.logger.info(f"成功获取webhook{current_count}条: {json.dumps(webhook_list)}")
242
+ redis.lpop_all(redis_key)
243
+ return webhook_list
244
+
245
+ if current_count == count:
246
+ self.logger.info(f"成功获取webhook{current_count}条: {json.dumps(webhook_list)}")
247
+ redis.lpop_all(redis_key)
248
+ return webhook_list
249
+
250
+ self.logger.info(f"已获取webhook {current_count} 条,未达到预期 {count} 条,5 秒后重试...")
251
+ else:
252
+ self.logger.info(f"暂未收到webhook,{interval} 秒后重试...")
253
+ time.sleep(interval)
254
+ current_count = len(webhook_list)
255
+ if current_count == 0:
256
+ self.logger.warning(f"等待webhook超时({GET_WEBHOOK_TIMEOUT} 秒),redis key: {redis_key}")
257
+ raise AssertionError('暂未收到webhook')
258
+ else:
259
+ self.logger.info(f"已获取部分webhook: {json.dumps(webhook_list)}")
260
+ if count:
261
+ raise AssertionError(f'webhook条数错误,当前条数: {current_count} 期望条数: {count}')
262
+ redis.lpop_all(redis_key)
263
+ return webhook_list
264
+
265
+ def webhook(self, webhook):
266
+ return AssertWebhook(webhook)
267
+
176
268
  def get_customized_kytmock(self, request_id):
177
269
  return get_customized_kytmock(self.context.get("env"), request_id)
178
270
 
@@ -222,7 +314,8 @@ class BaseTestCase(ExtendBaseTestCase):
222
314
  crm = self.mysql_conn(db=self.db.DB_CAMP_CRM)
223
315
  crm.execute(
224
316
  f"delete from tbl_crypto_wallet where id in (select wallet_id from tbl_participant_crypto_wallet where wallet_tag ='MY HOT WALLET' and participant_code='{participant_code}');")
225
- crm.execute(f"delete from tbl_participant_crypto_wallet where wallet_tag ='MY HOT WALLET' and participant_code='{participant_code}';")
317
+ crm.execute(
318
+ f"delete from tbl_participant_crypto_wallet where wallet_tag ='MY HOT WALLET' and participant_code='{participant_code}';")
226
319
 
227
320
  # 重新生成新钱包
228
321
  self.post(
@@ -254,7 +347,8 @@ class BaseTestCase(ExtendBaseTestCase):
254
347
  crm = self.mysql_conn(db=self.db.DB_CAMP_CRM)
255
348
  crm.execute(
256
349
  f"delete from tbl_crypto_wallet where id in (select crypto_wallet_id from tbl_buyer_crypto_wallet where wallet_tag ='MY HOT WALLET' and participant_code='{buyer_participant_code}');")
257
- crm.execute(f"delete from tbl_buyer_crypto_wallet where wallet_tag ='MY HOT WALLET' and participant_code='{buyer_participant_code}';")
350
+ crm.execute(
351
+ f"delete from tbl_buyer_crypto_wallet where wallet_tag ='MY HOT WALLET' and participant_code='{buyer_participant_code}';")
258
352
 
259
353
  # 重新生成新钱包
260
354
  self.post(
@@ -27,6 +27,7 @@ from framework.exit_code import ExitCode
27
27
  from framework.db.mysql_db import MysqlDB
28
28
  from framework.db.redis_db import RedisDB
29
29
  from framework.utils.log_util import logger
30
+ from framework.retry_assert import RetryAssert
30
31
  from framework.utils.lark_util import LarkUtil
31
32
  from framework.utils.yaml_util import CachedYamlLoader
32
33
  from framework.exceptions import MysqlDBError, RedisDBError
@@ -392,6 +393,7 @@ def pytest_runtest_setup(item):
392
393
  if item.funcargs.get("first"):
393
394
  test_object = item.instance
394
395
  test_object.context = CONTEXT
396
+ test_object.retry_assert = RetryAssert.eventually
395
397
  test_object.config = CONFIG
396
398
  test_object.http = _FRAMEWORK_CONTEXT.get(key="_http")
397
399
  data = item.callspec.params.get("data")
@@ -456,6 +458,7 @@ def pytest_runtest_call(item):
456
458
  item.funcargs["belong_app"] = item.instance.belong_app = _belong_app
457
459
  item.funcargs["config"] = item.instance.config = CONFIG
458
460
  item.funcargs["context"] = item.instance.context = CONTEXT
461
+ item.funcargs["retry_assert"] = item.instance.retry_assert = RetryAssert.eventually
459
462
  # 类式测试用例添加参数http,data, belong_app
460
463
  item.instance.http = http
461
464
 
@@ -477,6 +480,7 @@ def pytest_runtest_teardown(item):
477
480
  test_object = item.instance
478
481
  test_object.context = CONTEXT
479
482
  test_object.config = CONFIG
483
+ test_object.retry_assert = RetryAssert.eventually
480
484
  test_object.http = _FRAMEWORK_CONTEXT.get(key="_http")
481
485
  data = item.callspec.params.get("data")
482
486
  test_object.data = Box(data)
@@ -553,7 +557,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
553
557
  passed=passed,
554
558
  failed=failed,
555
559
  skipped=skipped,
556
- job_name=os.environ.get("JOB_NAME"),
560
+ job_name=CONTEXT.get("mark"),
557
561
  env=CONTEXT.get("env")
558
562
  )
559
563
 
@@ -101,6 +101,12 @@ class RedisDB:
101
101
  items.append(val)
102
102
  return items
103
103
 
104
+ @safe_redis_call
105
+ def lrange_all(self, name):
106
+ """弹出并返回队列中所有元素(不会清空队列)"""
107
+ res = self.conn.lrange(name, 0, -1)
108
+ return res if res else []
109
+
104
110
  # -------------------- meta --------------------
105
111
  @safe_redis_call
106
112
  def set_ttl(self, name, ttl, log=True):
@@ -139,4 +145,4 @@ class RedisDB:
139
145
 
140
146
  @safe_redis_call
141
147
  def rpop(self, name):
142
- return self.conn.rpop(name)
148
+ return self.conn.rpop(name)
@@ -0,0 +1,58 @@
1
+ import time
2
+ import traceback
3
+ from framework.utils.log_util import logger
4
+ from config.settings import ASYNC_ASSERT_TRIES, ASYNC_ASSERT_TIMEOUT
5
+
6
+
7
+ class RetryAssert:
8
+ @staticmethod
9
+ def eventually(
10
+ assert_func,
11
+ *args,
12
+ timeout=ASYNC_ASSERT_TIMEOUT,
13
+ interval=ASYNC_ASSERT_TRIES,
14
+ desc=None,
15
+ **kwargs
16
+ ):
17
+ """
18
+ 在超时时间内不断重试断言,直到成功或超时
19
+
20
+ :param assert_func: 断言函数
21
+ :param args: 传给断言函数的位置参数
22
+ :param kwargs: 传给断言函数的关键字参数
23
+ """
24
+ start = time.time()
25
+ last_exception = None
26
+ attempt = 0
27
+
28
+ while True:
29
+ attempt += 1
30
+ elapsed = time.time() - start
31
+
32
+ if elapsed > timeout:
33
+ break
34
+
35
+ try:
36
+ assert_func(*args, **kwargs)
37
+ return
38
+ except AssertionError as e:
39
+ last_exception = e
40
+ logger.warning(
41
+ f"第 {attempt} 次断言失败 "
42
+ f"(已等待 {elapsed:.1f}s / {timeout}s): {e}"
43
+ )
44
+ except Exception as e:
45
+ last_exception = e
46
+ logger.warning(
47
+ f"第 {attempt} 次执行异常 "
48
+ f"(已等待 {elapsed:.1f}s / {timeout}s): {e}"
49
+ )
50
+
51
+ time.sleep(interval)
52
+
53
+ error_msg = desc or "异步断言失败"
54
+ raise AssertionError(
55
+ f"{error_msg}\n"
56
+ f"重试次数: {attempt}\n"
57
+ f"最后异常: {last_exception}"
58
+ )
@@ -74,7 +74,7 @@ class LarkUtil:
74
74
  except ZeroDivisionError:
75
75
  pass_rate = 0
76
76
  markdown = f"""**执行环境:** {env}
77
- **执行命令:** {command}
77
+ **执行标签:** {command}
78
78
  **执行完成时间:** {datetime.now().strftime("%Y-%m-%d %X")}
79
79
  **执行用例总数:** {total}
80
80
  **通过用例数:** {passed}
@@ -115,7 +115,7 @@ class Validate(object):
115
115
 
116
116
  def assert_equal(self, expectant_expression, practical_result):
117
117
  expectant_result = self.parse_expectant_expression(expectant_expression)
118
- if isinstance(expectant_result, (int, float)) and isinstance(practical_result, str):
118
+ if isinstance(expectant_result, (int, float)) or isinstance(practical_result, str):
119
119
  if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith(
120
120
  "}"):
121
121
  practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
@@ -126,7 +126,7 @@ class Validate(object):
126
126
 
127
127
  def assert_not_equal(self, expectant_expression, practical_result):
128
128
  expectant_result = self.parse_expectant_expression(expectant_expression)
129
- if isinstance(expectant_result, (int, float)) and isinstance(practical_result, str):
129
+ if isinstance(expectant_result, (int, float)) or isinstance(practical_result, str):
130
130
  if isinstance(practical_result, str) and practical_result.startswith("${") and practical_result.endswith(
131
131
  "}"):
132
132
  practical_result = RenderData(self.data).get_attribute(practical_result[2:-1])
@@ -267,7 +267,7 @@ class Validate(object):
267
267
 
268
268
  def assert_is_null(self, expectant_expression, practical_result):
269
269
  expectant_result = self.parse_expectant_expression(expectant_expression)
270
- assert expectant_result is None
270
+ assert not expectant_result
271
271
  return expectant_result
272
272
 
273
273
  def assert_is_not_null(self, expectant_expression, practical_result):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-api-framework-alpha
3
- Version: 0.3.18
3
+ Version: 0.3.20
4
4
  Author: alpha
5
5
  Author-email:
6
6
  Requires-Python: >=3.6
@@ -1,5 +1,6 @@
1
1
  setup.py
2
2
  framework/__init__.py
3
+ framework/assert_webhook.py
3
4
  framework/base_class.py
4
5
  framework/conftest.py
5
6
  framework/exceptions.py
@@ -9,6 +10,7 @@ framework/global_attribute.py
9
10
  framework/http_client.py
10
11
  framework/render_data.py
11
12
  framework/report.py
13
+ framework/retry_assert.py
12
14
  framework/script.py
13
15
  framework/startapp.py
14
16
  framework/validate.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name="pytest-api-framework-alpha", # 包名(必须唯一)
5
- version="0.3.18",
5
+ version="0.3.20",
6
6
  packages=find_packages(),
7
7
  author="alpha",
8
8
  author_email="",