pytest-api-framework-alpha 0.1.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.
@@ -0,0 +1,387 @@
1
+ import json
2
+ import base64
3
+ import hashlib
4
+
5
+ from Crypto.PublicKey import RSA
6
+ from Crypto.Util.Padding import pad, unpad
7
+ from Crypto.Cipher import AES, DES, DES3, PKCS1_v1_5 as PKCS1_cipher
8
+ from framework.utils.common import valid_hex_format, valid_b64_format, hex_to_bytes, bytes_to_hex
9
+
10
+
11
+ def md5(string, byte=False, encoding="utf-8"):
12
+ """
13
+
14
+ :param string: 加密内容
15
+ :param byte: 返回字节类型,默认返回16进制
16
+ :param encoding: 编码
17
+ :return: 返回字符串
18
+ """
19
+ md_obj = hashlib.md5()
20
+ if isinstance(string, str):
21
+ string = string.encode(encoding=encoding)
22
+ md_obj.update(string)
23
+ if byte:
24
+ return md_obj.digest()
25
+ return md_obj.hexdigest()
26
+
27
+
28
+ def sha(str_list, mode, byte=False, length=None, encoding="utf-8"):
29
+ """
30
+
31
+ :param str_list: 加密内容,字节或字符串
32
+ :param mode: sha系列加密方式
33
+ :param byte: 返回字节类型,默认返回16进制
34
+ :param length: 'shake_128', 'shake_256'需要指定返回的长度
35
+ :param encoding: 编码
36
+ :return: 返回字符串
37
+ """
38
+ sha_obj = hashlib.new(mode)
39
+ if not isinstance(str_list, list):
40
+ raise Exception(f"{str_list} 必须是list类型")
41
+ for i in str_list:
42
+ if not isinstance(i, (str, bytes)):
43
+ raise Exception(f"{i} 必须是str或bytes类型")
44
+ if isinstance(i, str):
45
+ i = i.encode(encoding=encoding)
46
+ sha_obj.update(i)
47
+
48
+ if mode in ['shake_128', 'shake_256']:
49
+ if byte:
50
+ return sha_obj.digest(length)
51
+ return sha_obj.hexdigest(length)
52
+ else:
53
+ if byte:
54
+ return sha_obj.digest()
55
+ return sha_obj.hexdigest()
56
+
57
+
58
+ def b64_encode(bs, byte=False, encoding="utf-8"):
59
+ """
60
+ bytes -> b64字符串
61
+ :param bs: 二进制字节串
62
+ :param byte: 返回二进制字节或b64字符串,默认b64字符串
63
+ :param encoding:
64
+ :return:
65
+ """
66
+ if not isinstance(bs, bytes):
67
+ raise Exception(f"{bs} 必须是bytes类型")
68
+ if byte:
69
+ return base64.b64encode(bs)
70
+ return base64.b64encode(bs).decode(encoding)
71
+
72
+
73
+ def b64_decode(string, byte=True, encoding="utf-8"):
74
+ """
75
+ b64字符串 -> bytes
76
+ :param string: b64字符串
77
+ :param byte: 返回二进制字节或字符串,默认返回二进制字节
78
+ :param encoding:
79
+ :return:
80
+ """
81
+ if not isinstance(string, str):
82
+ raise Exception(f"{string} 必须是b64字符串")
83
+
84
+ missing_padding = 4 - len(string) % 4
85
+ if missing_padding:
86
+ string += '=' * missing_padding
87
+ if byte:
88
+ return base64.b64decode(string)
89
+ return base64.b64decode(string).decode(encoding=encoding)
90
+
91
+
92
+ class RsaByPubKey(object):
93
+
94
+ def __init__(self, pub_key):
95
+ """
96
+ :param pub_key: 公钥
97
+ 1.可以直接传公钥的base64字符串;
98
+ 2.通过save_publish_key方法先将公钥的base64字符串存到文件,然后使用get_publish_key方法读取文件获取公钥,然后传入
99
+ """
100
+ self.pub_key = pub_key
101
+
102
+ def encrypt(self, data, b64=False, encoding="utf-8"):
103
+ """
104
+
105
+ :param data: 要加密的数据
106
+ :param b64: 默认返回16进制字符串,True返回base64字符
107
+ :param encoding:
108
+ :return:
109
+ """
110
+
111
+ if valid_hex_format(self.pub_key):
112
+ self.pub_key = hex_to_bytes(self.pub_key)
113
+
114
+ elif valid_b64_format(self.pub_key):
115
+ self.pub_key = b64_decode(self.pub_key)
116
+
117
+ else:
118
+ raise Exception("pub_key参数必须是16进制字符串或b64字符串")
119
+
120
+ if not isinstance(data, bytes):
121
+ data = data.encode(encoding=encoding)
122
+ cipher = PKCS1_cipher.new(RSA.import_key(self.pub_key))
123
+ encrypt_text = cipher.encrypt(data)
124
+ if b64:
125
+ return b64_encode(cipher.encrypt(data)) # 转成base64字符串
126
+ return bytes_to_hex(encrypt_text).decode(encoding=encoding) # 转成base64字符
127
+
128
+
129
+ class Aes(object):
130
+ AES_MODE_MAP = {
131
+ "CBC": AES.MODE_CBC,
132
+ "ECB": AES.MODE_ECB,
133
+ "CFB": AES.MODE_CFB,
134
+ "OFB": AES.MODE_OFB,
135
+ "CTR": AES.MODE_CTR,
136
+ "OPENPGP": AES.MODE_OPENPGP,
137
+ "CCM": AES.MODE_CCM,
138
+ "EAX": AES.MODE_EAX,
139
+ "SIV": AES.MODE_SIV,
140
+ "GCM": AES.MODE_GCM,
141
+ "OCB": AES.MODE_OCB
142
+ }
143
+
144
+ def __init__(self, key, mode, iv=None, encoding="utf-8"):
145
+ """
146
+
147
+ :param key: 必须是16 or 24 or 32字节长度
148
+ :param mode:
149
+ :param iv: 必须是16位字节
150
+ :param encoding:
151
+ """
152
+ self.iv = iv
153
+ self.key = key
154
+ self.mode = mode.upper()
155
+ self.encoding = encoding
156
+ if self.mode in ["CBC", "CFB", "OFB"]:
157
+ if not self.iv or len(self.iv) != 16:
158
+ raise Exception(f"{iv} 必须是16字节长度")
159
+
160
+ if len(self.key) not in [16, 24, 32]:
161
+ raise Exception(f"{self.key} 必须是16、24或32字节长度")
162
+
163
+ if not isinstance(self.key, bytes):
164
+ self.key = self.key.encode(encoding=self.encoding)
165
+
166
+ if self.iv:
167
+ if not isinstance(self.iv, bytes):
168
+ self.iv = self.iv.encode(encoding=self.encoding)
169
+ self.mode = self.AES_MODE_MAP.get(mode)
170
+
171
+ def new_aes(self):
172
+ if self.iv:
173
+ return AES.new(key=self.key, mode=self.mode, IV=self.iv)
174
+ return AES.new(key=self.key, mode=self.mode)
175
+
176
+ def encrypt(self, data, byte=False, separate=False):
177
+ """
178
+ 字符串/字节->加密->b64字符串
179
+ :param data: 加密内容,字节或者字符串,如果是字符串,默认转为字节
180
+ :param byte: 返回b64字符或字节,默认返回b64字符
181
+ :param separate: 序列化时是否需要:,,后面的空格,默认不需要
182
+ :return:b64字符串
183
+ """
184
+
185
+ aes_obj = self.new_aes()
186
+ if isinstance(data, dict):
187
+ if separate:
188
+ data = json.dumps(data, separators=(',', ':')).encode(encoding=self.encoding)
189
+ else:
190
+ data = json.dumps(data).encode(encoding=self.encoding)
191
+ else:
192
+ data = str(data).encode(encoding=self.encoding)
193
+ data = pad(data, 16)
194
+ if byte:
195
+ return aes_obj.encrypt(data)
196
+ return b64_encode(aes_obj.encrypt(data))
197
+
198
+ def decrypt(self, data, byte=False):
199
+ """
200
+ b64字符串->解密->字符串/字节
201
+ :param data: 解密内容,字节或者字符串,如果是字符串,转为b64字节
202
+ :param byte: 返回字符串或字节,默认字符串
203
+ :return:
204
+ """
205
+ aes_obj = self.new_aes()
206
+ if not isinstance(data, bytes):
207
+ data = b64_decode(data)
208
+ if byte:
209
+ return unpad(aes_obj.decrypt(data), 16)
210
+ try:
211
+ return json.loads(unpad(aes_obj.decrypt(data), 16).decode(encoding=self.encoding))
212
+ except Exception:
213
+ return unpad(aes_obj.decrypt(data), 16).decode(encoding=self.encoding)
214
+
215
+
216
+ class Des(object):
217
+ DES_MODE_MAP = {
218
+ "CBC": DES.MODE_CBC,
219
+ "ECB": DES.MODE_ECB,
220
+ "CFB": DES.MODE_CFB,
221
+ "OFB": DES.MODE_OFB,
222
+ "CTR": DES.MODE_CTR,
223
+ "OPENPGP": DES.MODE_OPENPGP,
224
+ "EAX": DES.MODE_EAX
225
+ }
226
+
227
+ def __init__(self, key, mode, iv=None, encoding="utf-8"):
228
+ """
229
+
230
+ :param key: 必须是8位字节
231
+ :param mode:
232
+ :param iv: 必须是8位字节
233
+ :param encoding:
234
+ """
235
+ self.iv = iv
236
+ self.key = key
237
+ self.mode = mode.upper()
238
+ self.encoding = encoding
239
+ if self.mode in ["CBC", "CFB", "OFB"]:
240
+ if not self.iv or len(self.iv) != 8:
241
+ raise Exception(f"{self.iv} 必须是8字节长度")
242
+
243
+ if len(self.key) != 8:
244
+ raise Exception(f"{self.key} 必须是8字节长度")
245
+
246
+ if not isinstance(self.key, bytes):
247
+ self.key = self.key.encode(encoding=self.encoding)
248
+
249
+ if self.iv:
250
+ if not isinstance(self.iv, bytes):
251
+ self.iv = self.iv.encode(encoding=self.encoding)
252
+
253
+ self.mode = self.DES_MODE_MAP.get(mode)
254
+
255
+ def new_des(self):
256
+ if self.iv:
257
+ return DES.new(key=self.key, mode=self.mode, IV=self.iv)
258
+ return DES.new(key=self.key, mode=self.mode)
259
+
260
+ def encrypt(self, data, byte=False, separate=False):
261
+ """
262
+ :param data: 加密内容,字节或者字符串,如果是字符串,默认转为字节
263
+ :param byte: 返回base64字符或字节,默认返回base64字符
264
+ :param separate: 序列化时是否需要:,,后面的空格,默认不需要
265
+ :return:
266
+ """
267
+ des_obj = self.new_des()
268
+ if isinstance(data, dict):
269
+ if separate:
270
+ data = json.dumps(data, separators=(',', ':')).encode(encoding=self.encoding)
271
+ else:
272
+ data = json.dumps(data).encode(encoding=self.encoding)
273
+ else:
274
+ data = str(data).encode(encoding=self.encoding)
275
+ data = pad(data, 8)
276
+ if byte:
277
+ return des_obj.encrypt(data)
278
+ return b64_encode(des_obj.encrypt(data))
279
+
280
+ def decrypt(self, data, byte=False):
281
+ """
282
+
283
+ :param data: 解密内容,字节或者字符串,如果是字符串,默认转为base64字符
284
+ :param byte: 返回字符串或字节,默认字符串
285
+ :return:
286
+ """
287
+ des_obj = self.new_des()
288
+ if not isinstance(data, bytes):
289
+ data = b64_decode(data)
290
+ if byte:
291
+ return unpad(des_obj.decrypt(data), 8)
292
+ try:
293
+ return json.loads(unpad(des_obj.decrypt(data), 8).decode(encoding=self.encoding))
294
+ except Exception:
295
+ return unpad(des_obj.decrypt(data), 8).decode(encoding=self.encoding)
296
+
297
+
298
+ class Des3(object):
299
+ DES3_MODE_MAP = {
300
+ "CBC": DES3.MODE_CBC,
301
+ "ECB": DES3.MODE_ECB,
302
+ "CFB": DES3.MODE_CFB,
303
+ "OFB": DES3.MODE_OFB,
304
+ "CTR": DES3.MODE_CTR,
305
+ "OPENPGP": DES3.MODE_OPENPGP,
306
+ "EAX": DES3.MODE_EAX
307
+ }
308
+
309
+ def __init__(self, key, mode, iv=None, encoding="utf-8"):
310
+ """
311
+
312
+ :param key: 必须是16,24位字节
313
+ :param mode:
314
+ :param iv: 必须是8位字节
315
+ :param encoding:
316
+ """
317
+ self.iv = iv
318
+ self.key = key
319
+ self.mode = mode.upper()
320
+ self.encoding = encoding
321
+
322
+ if self.mode in ["CBC", "CFB", "OFB"]:
323
+ if not self.iv or len(self.iv) != 8:
324
+ raise Exception(f"{self.iv} 必须是8字节长度")
325
+
326
+ if len(self.key) not in [16, 24]:
327
+ raise Exception(f"{self.key} 必须是16或24字节长度")
328
+
329
+ if not isinstance(self.key, bytes):
330
+ self.key = self.key.encode(encoding=self.encoding)
331
+
332
+ if self.iv:
333
+ if not isinstance(iv, bytes):
334
+ self.iv = self.iv.encode(encoding=self.encoding)
335
+
336
+ self.mode = self.DES3_MODE_MAP.get(mode)
337
+
338
+ def new_des3(self):
339
+ if self.iv:
340
+ return DES3.new(key=self.key, mode=self.mode, IV=self.iv)
341
+ return DES3.new(key=self.key, mode=self.mode)
342
+
343
+ def encrypt(self, data, byte=False, separate=False):
344
+ """
345
+ :param data: 加密内容,字节或者字符串,如果是字符串,默认转为字节
346
+ :param byte: 返回base64字符或字节,默认返回base64字符
347
+ :param separate: 序列化时是否需要:,,后面的空格,默认不需要
348
+ :return:
349
+ """
350
+ des3_obj = self.new_des3()
351
+ if isinstance(data, dict):
352
+ if separate:
353
+ data = json.dumps(data, separators=(',', ':')).encode(encoding=self.encoding)
354
+ else:
355
+ data = json.dumps(data).encode(encoding=self.encoding)
356
+ else:
357
+ data = str(data).encode(encoding=self.encoding)
358
+ data = pad(data, 8)
359
+ if byte:
360
+ return des3_obj.encrypt(data)
361
+ else:
362
+ return b64_encode(des3_obj.encrypt(data))
363
+
364
+ def decrypt(self, data, byte=False):
365
+ """
366
+
367
+ :param data: 解密内容,字节或者字符串,如果是字符串,默认转为base64字符
368
+ :param byte: 返回字符串或字节,默认字符串
369
+ :return:
370
+ """
371
+ des3_obj = self.new_des3()
372
+ if not isinstance(data, bytes):
373
+ data = b64_decode(data)
374
+ if byte:
375
+ return unpad(des3_obj.decrypt(data), 8)
376
+ else:
377
+ try:
378
+ return json.loads(unpad(des3_obj.decrypt(data), 8).decode(encoding=self.encoding))
379
+ except Exception:
380
+ return unpad(des3_obj.decrypt(data), 8).decode(encoding=self.encoding)
381
+
382
+
383
+ if __name__ == '__main__':
384
+ key = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqJA8yliEYgp9aorMNzayIyJex4ukgwEXi+Us2xVJlttB2Uy9Bsh9ugTqNcc1bf7R5WW/QIN/EbA+yJC1FCqZHzZdYw54O+IN9oV9I+pE2ziK6vlOjUYmKbi2NO84xAYW83uaWee6MkH8m87qn5hrd7JzksPJS3HdHNZCcOOOemwIDAQAB"
385
+ rsa = RsaByPubKey(pub_key=key)
386
+ a = rsa.encrypt(test_data="password", b64=True)
387
+ print(a)
@@ -0,0 +1,3 @@
1
+ import logging
2
+
3
+ logger = logging.getLogger(__name__)
@@ -0,0 +1,48 @@
1
+ import requests
2
+
3
+ from framework.utils.common import get_current_datetime
4
+
5
+
6
+ class TeamsUtil(object):
7
+
8
+ def __init__(self, webhook_url):
9
+ self.webhook_url = webhook_url
10
+
11
+ def send_teams_card(self, title, content_list):
12
+ headers = {
13
+ "Content-Type": "application/json"
14
+ }
15
+
16
+ payload = {
17
+ "type": "message",
18
+ "attachments": [
19
+ {
20
+ "contentType": "application/vnd.microsoft.card.adaptive",
21
+ "content": {
22
+ "type": "AdaptiveCard",
23
+ "body": [
24
+ {
25
+ "type": "TextBlock",
26
+ "size": "Large",
27
+ "weight": "Bolder",
28
+ "text": title
29
+ },
30
+ ],
31
+ "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
32
+ "version": "1.0"
33
+ }
34
+ }
35
+ ]
36
+ }
37
+ content_list.append(f"发送时间: {get_current_datetime()}")
38
+ for item in content_list:
39
+ payload["attachments"][0]["content"]["body"].append({
40
+ "type": "TextBlock",
41
+ "text": item,
42
+ "wrap": True
43
+ })
44
+
45
+ response = requests.post(self.webhook_url, headers=headers, json=payload)
46
+
47
+ if not response.status_code == 200:
48
+ raise Exception(f"Failed to send card. Status code: {response.status_code}, Response: {response.text}")
@@ -0,0 +1,25 @@
1
+ import os
2
+ import yaml
3
+ from box import Box
4
+
5
+ from config.settings import ROOT_DIR
6
+
7
+
8
+ class YamlUtil(object):
9
+ def __init__(self, file_path):
10
+ self.file_path = file_path
11
+
12
+ def load_yml(self, key=None, is_box=False):
13
+ """
14
+
15
+ @param key:
16
+ @param is_box:
17
+ @return:
18
+ """
19
+ with open(os.path.join(ROOT_DIR, self.file_path), mode="r", encoding="utf-8") as f:
20
+ result = yaml.safe_load(stream=f) or dict()
21
+ if key:
22
+ result = result.get(key)
23
+ if is_box:
24
+ result = Box(result) if isinstance(result, dict) else result
25
+ return result
framework/validate.py ADDED
@@ -0,0 +1,207 @@
1
+ import re
2
+
3
+ import allure
4
+ from utils.log_util import logger
5
+ from jsonpath import jsonpath
6
+ from utils.common import an2cn, is_digit
7
+
8
+
9
+ class Validate(object):
10
+ def __init__(self, response):
11
+ self.response = response
12
+
13
+ self.mapping = {
14
+ "eq": "assert_equal",
15
+ "not_eq": "assert_not_equal",
16
+ "lt": "assert_less_than",
17
+ "le": "assert_less_than_or_equals",
18
+ "gt": "assert_greater_than",
19
+ "ge": "assert_greater_than_or_equals",
20
+ "len_eq": "assert_length_equals",
21
+ "len_lt": "assert_length_less_than",
22
+ "len_le": "assert_length_less_than_or_equals",
23
+ "len_gt": "assert_length_greater_than",
24
+ "len_ge": "assert_length_greater_than_or_equals",
25
+ "contains": "assert_contains",
26
+ "contained_by": "assert_contained_by",
27
+ "startswith": "assert_startswith",
28
+ "endswith": "assert_endswith"
29
+ }
30
+
31
+ def valid(self, validates):
32
+ for valid_item in validates:
33
+ key = list(valid_item.keys())[0]
34
+ valid_list = [i.strip() for i in valid_item.get(key).split(",")]
35
+ expression = valid_list[0]
36
+ expectant_result = valid_list[1]
37
+ func = self.mapping.get(key)
38
+ try:
39
+ if func:
40
+ getattr(self, func)(expression, expectant_result)
41
+ with allure.step(
42
+ f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言通过"):
43
+ logger.info(
44
+ f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言通过")
45
+ else:
46
+ raise Exception(f"不支持的断言方式: {key}")
47
+ except AssertionError as e:
48
+ with allure.step(
49
+ f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言失败。 失败原因: {e}"):
50
+ logger.error(
51
+ f"断言({an2cn(validates.index(valid_item) + 1)}): 断言类型: {self.mapping.get(key)}, 断言内容: {valid_list}, 断言结果: 断言失败。 失败原因: {e}")
52
+ raise AssertionError(e)
53
+
54
+ def parse_expectant_expression(self, expectant_expression):
55
+
56
+ try:
57
+ # jsonpath表达式取值
58
+ if expectant_expression.lower().startswith("$."):
59
+ return self.exec_jsonpath(expectant_expression)
60
+ # 正则表达式取值
61
+ elif expectant_expression.startswith("/") and expectant_expression.endswith("/"):
62
+ return self.exec_reg(expectant_expression[1: -1])
63
+ else:
64
+ # response对象取值
65
+ return self.exec_response(expectant_expression)
66
+ except Exception as e:
67
+ with allure.step(f"不支持的变量提取方式{expectant_expression}: {e}"):
68
+ logger.error(f"不支持的变量提取方式{expectant_expression}: {e}")
69
+
70
+ def exec_jsonpath(self, expression):
71
+ try:
72
+ return jsonpath(self.response.json(), expression)[0]
73
+ except Exception:
74
+ raise Exception(f"jsonpath表达式错误或非预期响应内容。表达式: {expression};响应内容: {self.response.json()}")
75
+
76
+ def exec_reg(self, reg_expression):
77
+ try:
78
+ result = re.search(reg_expression, self.response.text, flags=re.S).group()
79
+ if is_digit(result):
80
+ return eval(result)
81
+ else:
82
+ return result
83
+ except AttributeError:
84
+ raise Exception(f"正则表达式或非预期响应内容。表达式: {reg_expression}; 响应内容: {self.response.text}")
85
+
86
+ def exec_response(self, attr):
87
+ if attr in ["status_code", "url", "ok", "encoding"]:
88
+ return getattr(self.response, attr)
89
+ else:
90
+ raise Exception(f"不支持提取的Response对象属性{attr}")
91
+
92
+ def assert_equal(self, expectant_expression, practical_result):
93
+ expectant_result = self.parse_expectant_expression(expectant_expression)
94
+ if isinstance(expectant_result, (int, float)) and isinstance(practical_result, str):
95
+ practical_result = eval(practical_result)
96
+ assert practical_result == expectant_result, f'{expectant_result} == {practical_result}'
97
+
98
+ def assert_not_equal(self, expectant_expression, practical_result):
99
+ expectant_result = self.parse_expectant_expression(expectant_expression)
100
+ if isinstance(expectant_result, (int, float)) and isinstance(practical_result, str):
101
+ practical_result = eval(practical_result)
102
+ assert practical_result != expectant_result, f'{expectant_result} != {practical_result}'
103
+
104
+ def assert_less_than(self, expectant_expression, practical_result):
105
+ expectant_result, practical_result = self.__to_digit(
106
+ self.parse_expectant_expression(expectant_expression),
107
+ practical_result
108
+ )
109
+ assert expectant_result < practical_result, f'{expectant_result} < {practical_result}'
110
+
111
+ def assert_less_than_or_equals(self, expectant_expression, practical_result):
112
+ expectant_result, practical_result = self.__to_digit(
113
+ self.parse_expectant_expression(expectant_expression),
114
+ practical_result
115
+ )
116
+ assert expectant_result <= practical_result, f'{expectant_result} <= {practical_result}'
117
+
118
+ def assert_greater_than(self, expectant_expression, practical_result):
119
+ expectant_result, practical_result = self.__to_digit(
120
+ self.parse_expectant_expression(expectant_expression),
121
+ practical_result
122
+ )
123
+ assert expectant_result > practical_result, f'{expectant_result} > {practical_result}'
124
+
125
+ def assert_greater_than_or_equals(self, expectant_expression, practical_result):
126
+ expectant_result, practical_result = self.__to_digit(
127
+ self.parse_expectant_expression(expectant_expression),
128
+ practical_result
129
+ )
130
+ assert expectant_result >= practical_result, f'{expectant_result} >= {practical_result}'
131
+
132
+ def assert_contains(self, expectant_expression, practical_result):
133
+ expectant_result, practical_result = self.__to_str(
134
+ self.parse_expectant_expression(expectant_expression),
135
+ practical_result
136
+ )
137
+ assert practical_result in expectant_result, f'{practical_result} in {expectant_result}'
138
+
139
+ def assert_contained_by(self, expectant_expression, practical_result):
140
+ expectant_result, practical_result = self.__to_str(
141
+ self.parse_expectant_expression(expectant_expression),
142
+ practical_result
143
+ )
144
+ assert expectant_result in practical_result, f'{expectant_result} in {practical_result}'
145
+
146
+ def assert_startswith(self, expectant_expression, practical_result):
147
+ expectant_result, practical_result = self.__to_str(
148
+ self.parse_expectant_expression(expectant_expression),
149
+ practical_result
150
+ )
151
+ assert expectant_result.startswith(practical_result), f'{expectant_result} 以 {practical_result} 开头'
152
+
153
+ def assert_endswith(self, expectant_expression, practical_result):
154
+ expectant_result, practical_result = self.__to_str(
155
+ self.parse_expectant_expression(expectant_expression),
156
+ practical_result
157
+ )
158
+ assert expectant_result.endswith(practical_result), f'{expectant_result} 以 {practical_result} 结尾'
159
+
160
+ def assert_length_equals(self, expectant_expression, practical_result):
161
+ expectant_result, practical_result = self.__to_str(
162
+ self.parse_expectant_expression(expectant_expression),
163
+ practical_result
164
+ )
165
+ assert len(expectant_result) == int(practical_result), f'{len(expectant_result)} == {practical_result}'
166
+
167
+ def assert_length_less_than(self, expectant_expression, practical_result):
168
+ expectant_result, practical_result = self.__to_str(
169
+ self.parse_expectant_expression(expectant_expression),
170
+ practical_result
171
+ )
172
+ assert len(expectant_result) < int(practical_result), f'{len(expectant_result)} < {practical_result}'
173
+
174
+ def assert_length_less_than_or_equals(self, expectant_expression, practical_result):
175
+ expectant_result, practical_result = self.__to_str(
176
+ self.parse_expectant_expression(expectant_expression),
177
+ practical_result
178
+ )
179
+ assert len(expectant_result) <= int(practical_result), f'{len(expectant_result)} <= {practical_result}'
180
+
181
+ def assert_length_greater_than(self, expectant_expression, practical_result):
182
+ expectant_result, practical_result = self.__to_str(
183
+ self.parse_expectant_expression(expectant_expression),
184
+ practical_result
185
+ )
186
+ assert len(expectant_result) > int(practical_result), f'{len(expectant_result)} > {practical_result}'
187
+
188
+ def assert_length_greater_than_or_equals(self, expectant_expression, practical_result):
189
+ expectant_result, practical_result = self.__to_str(
190
+ self.parse_expectant_expression(expectant_expression),
191
+ practical_result
192
+ )
193
+ assert len(expectant_result) >= int(practical_result), f'{len(expectant_result)} >= {practical_result}'
194
+
195
+ def __to_str(self, expectant_expression, practical_result):
196
+ if isinstance(expectant_expression, (int, float)):
197
+ expectant_expression = str(expectant_expression)
198
+ if isinstance(practical_result, (int, float)):
199
+ practical_result = str(practical_result)
200
+ return expectant_expression, practical_result
201
+
202
+ def __to_digit(self, expectant_expression, practical_result):
203
+ if isinstance(expectant_expression, str) and is_digit(expectant_expression):
204
+ expectant_expression = eval(expectant_expression)
205
+ if isinstance(practical_result, str) and is_digit(practical_result):
206
+ practical_result = eval(practical_result)
207
+ return expectant_expression, practical_result