CloudApiRequest 1.0.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.

Potentially problematic release.


This version of CloudApiRequest might be problematic. Click here for more details.

@@ -0,0 +1,426 @@
1
+ """
2
+ cloud接口测试核心类
3
+ """
4
+ import json
5
+ import os
6
+ import time
7
+ from urllib.parse import urlparse
8
+
9
+ import pytest
10
+ import allure
11
+ import jsonpath
12
+ from requests_toolbelt import MultipartEncoder
13
+ from cloud.CloudLogUtil import log
14
+ from cloud.CloudRequestUtil import CloudHttpClient
15
+ from cloud.CloudSignUtil import CloudSignUtil
16
+ from cloud.CloudYamlUtil import CloudReadYaml, CloudPlaceholderYaml
17
+ from cloud.CloudApiConfig import config
18
+
19
+
20
+ class CloudAPIRequest:
21
+ """
22
+ cloud接口测试核心类
23
+ """
24
+
25
+ def __init__(self, commonCase=None):
26
+ """
27
+ :param commonCase: 公共用例文件
28
+ """
29
+ self.commonCase = commonCase
30
+ # 延迟初始化配置,避免循环导入
31
+ self._config = None
32
+ self.baseUrl = None
33
+ self.token = None
34
+ self.globalBean = None
35
+ self.assertFail = 'stop'
36
+ self.tenv = 'base'
37
+
38
+ def _get_config(self):
39
+ """延迟获取配置实例"""
40
+ if self._config is None:
41
+ from cloud import config
42
+ self._config = config
43
+ # 正确获取配置值,而不是property对象
44
+ self.baseUrl = str(config._baseUrl) if hasattr(config, '_baseUrl') and config._baseUrl else None
45
+ self.token = str(config._token) if hasattr(config, '_token') and config._token else None
46
+ self.globalBean = config
47
+ self.assertFail = str(config._assertFail) if hasattr(config, '_assertFail') and config._assertFail else 'stop'
48
+ self.tenv = str(config._tEnv) if hasattr(config, '_tEnv') and config._tEnv else 'base'
49
+ return self._config
50
+
51
+ def doRequest(self, file, bean):
52
+ """
53
+ 执行API请求测试
54
+ """
55
+ requestParameter = None
56
+ dataSaveBean = bean
57
+ yaml = CloudReadYaml(file).load_yaml()
58
+ yamlId = yaml.get('id')
59
+ yamlName = yaml.get('name')
60
+ yamlTestcase = yaml.get('testcases')
61
+
62
+ log.info(f"开始执行测试用例name: {yamlName}, id: {yamlId}")
63
+ config_instance = self._get_config()
64
+ clientSession = config_instance.Session
65
+
66
+ for testcase in yamlTestcase:
67
+ with allure.step(testcase.get('name')):
68
+ if testcase.get('skip'):
69
+ log.info(f"用例: {testcase.get('name')}跳过")
70
+ continue
71
+
72
+ sleeps = testcase.get('sleep')
73
+ if sleeps:
74
+ time.sleep(int(sleeps))
75
+ log.info(f"当前用例: {testcase.get('name')}执行前等待{sleeps}秒")
76
+
77
+ # 处理公共用例
78
+ config_instance = self._get_config()
79
+ if testcase.get('kind') and testcase.get('kind').lower() == 'common' and hasattr(config_instance, '_commonTestCasePath') and config_instance._commonTestCasePath is not None:
80
+ testcase = self.getCommonTestCase(testcase, config_instance._commonTestCasePath, testcase.get('id'))
81
+ elif testcase.get('kind') and testcase.get('kind').lower() == 'common' and (not hasattr(config_instance, '_commonTestCasePath') or config_instance._commonTestCasePath is None):
82
+ log.error(f"commonPath路径未配置,请检查配置文件")
83
+ raise Exception(f"commonPath路径未配置,请检查配置文件")
84
+
85
+ # 参数替换
86
+ if testcase.get('requestType') is None:
87
+ requestType = 'json'
88
+ else:
89
+ requestType = testcase.get('requestType')
90
+ repParameter = self.replaceParameterAttr(dataSaveBean, testcase.get('parameter'), requestType)
91
+ repApi = self.replaceParameterAttr(dataSaveBean, testcase.get('api'))
92
+ headers = self.replaceParameterAttr(dataSaveBean, testcase.get('headers'))
93
+
94
+ # 鉴权处理
95
+ requestParameter, requestUrl, authHeaders = self.authType(testcase.get('authType'), repApi, testcase.get('method'), repParameter)
96
+
97
+ # 请求类型处理
98
+ dataRequestParameter, jsonRequestParameter, paramsData, ModelData, requestType = self.requestType(requestType, requestParameter)
99
+
100
+ # 合并请求头
101
+ if headers:
102
+ headers.update(authHeaders)
103
+ else:
104
+ headers = authHeaders
105
+
106
+ # 执行请求
107
+ log.info(f"开始请求地址: {requestUrl}")
108
+ log.info(f"开始请求方式: {testcase.get('method')}")
109
+
110
+ if dataRequestParameter is not None and requestType.lower() in ['form-data', 'form-file']:
111
+ headers['Content-Type'] = dataRequestParameter.content_type
112
+
113
+ if testcase.get('stream_check'):
114
+ response = self.handle_stream_response(clientSession, testcase.get('method'), requestUrl, dataRequestParameter, jsonRequestParameter, paramsData, ModelData, headers)
115
+ else:
116
+ response = clientSession.request(method=testcase.get('method'), url=requestUrl, data=dataRequestParameter, json=jsonRequestParameter, params=paramsData, files=ModelData, headers=headers)
117
+
118
+ # 记录响应
119
+ try:
120
+ log.info(f"当前用例response: {json.dumps(response.json(), indent=4, ensure_ascii=False)}")
121
+ except:
122
+ log.info(f"当前用例response: {response.text}")
123
+
124
+ # 处理断言
125
+ if testcase.get('assertFail'):
126
+ failtype = testcase.get('assertFail')
127
+ else:
128
+ failtype = self.assertFail
129
+ self.assertType(testcase.get('assert'), response, dataSaveBean, failtype)
130
+
131
+ # 保存数据
132
+ try:
133
+ self.addAttrSaveBean(dataSaveBean, self.globalBean, testcase.get('saveData'), response.json())
134
+ except:
135
+ self.addAttrSaveBean(dataSaveBean, self.globalBean, testcase.get('saveData'), response.text)
136
+
137
+ return clientSession
138
+
139
+ def assertType(self, assertType, response, bean, failType):
140
+ """
141
+ 处理断言
142
+ """
143
+ if assertType is None:
144
+ log.info(f"断言为空,跳过断言")
145
+ return None
146
+
147
+ for ass in assertType:
148
+ key = list(ass.keys())[0]
149
+ log.info(f"开始判断{key}断言: {ass.get(key)}")
150
+
151
+ if 'status_code' in ass:
152
+ if ass.get('status_code'):
153
+ self.assertChoose(str(response.status_code) == str(ass.get('status_code')),
154
+ f"status_code断言失败: {ass.get('status_code')} ,response结果: {response.status_code}", failType)
155
+ continue
156
+
157
+ jsonpathResults = jsonpath.jsonpath(response.json(), ass.get(key)[0])
158
+ if jsonpathResults is False and 'not_found' not in ass:
159
+ self.assertChoose(1 > 2, f"提取{ass.get(key)[0]}失败,断言失败", failType)
160
+ continue
161
+
162
+
163
+
164
+ if 'eq' in ass:
165
+ expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('eq')[1]).replace().replaced_str
166
+ assResults = str(expectedResults) in [str(item) for item in jsonpathResults]
167
+ self.assertChoose(assResults is True, f"eq断言失败: {jsonpathResults} 不等于 {expectedResults}", failType)
168
+ elif 'sge' in ass:
169
+ expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('sge')[1]).replace().replaced_str
170
+ self.assertChoose(len(jsonpathResults) >= int(expectedResults), f"sge断言失败: {jsonpathResults} 小于 {expectedResults}", failType)
171
+ elif 'nn' in ass:
172
+ self.assertChoose(jsonpathResults is not None, f"not none断言失败: {ass.get('nn')[0]}", failType)
173
+ elif 'none' in ass:
174
+ self.assertChoose(jsonpathResults is True, f"none断言失败: {ass.get('none')[0]}", failType)
175
+ elif 'not_found' in ass:
176
+ self.assertChoose(jsonpathResults is False, f"not_found断言失败,字段存在", failType)
177
+ elif 'in' in ass:
178
+ expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('in')[1]).replace().replaced_str
179
+ self.assertChoose(str(expectedResults) in str(jsonpathResults), f"断言in失败: {expectedResults} 不在 {jsonpathResults} 内", failType)
180
+ elif 'len' in ass:
181
+ expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('in')[1]).replace().replaced_str
182
+ jsonpathResults_len = len(jsonpathResults[0])
183
+ self.assertChoose(jsonpathResults_len == int(expectedResults), f"断言len失败: {jsonpathResults_len} 长度不等于 {expectedResults}", failType)
184
+
185
+ def addAttrSaveBean(self, bean, globalBean, data: list, response):
186
+ """
187
+ 保存响应数据到Bean
188
+ """
189
+ if data is None:
190
+ return
191
+ for d in data:
192
+ if 'json' in d:
193
+ jsonPath = d.get('json')[1]
194
+ value = jsonpath.jsonpath(response, jsonPath)
195
+ if value is False:
196
+ value = None
197
+
198
+ saveBean = bean
199
+ if d.get('json').__len__() == 3 and d.get('json')[2].lower() == 'global':
200
+ saveBean = globalBean
201
+
202
+ key_parts = d.get('json')[0].split(':')
203
+ d.get('json')[0] = key_parts[0]
204
+
205
+ if value is not None and len(value) > 1:
206
+ setattr(saveBean, d.get('json')[0], list(value))
207
+ elif value is not None and len(value) == 1:
208
+ if len(key_parts) > 1 and key_parts[1].lower() == 'str':
209
+ value[0] = str(value[0])
210
+ setattr(saveBean, d.get('json')[0], value[0])
211
+
212
+ def replaceParameterAttr(self, bean, parameter, requestType='json'):
213
+ """
214
+ 替换参数中的占位符
215
+ """
216
+ if parameter is None:
217
+ return None
218
+
219
+ # 获取配置实例
220
+ config_instance = self._get_config()
221
+
222
+ if requestType.lower() == 'json-text':
223
+ repParameter = CloudPlaceholderYaml(yaml_str=parameter, attrObj=bean, methObj=config_instance.methObj).replace().textLoad()
224
+ else:
225
+ repParameter = CloudPlaceholderYaml(yaml_str=parameter, attrObj=bean, methObj=config_instance.methObj).replace().jsonLoad()
226
+
227
+ return repParameter
228
+
229
+ def requestType(self, requestType, data):
230
+ """
231
+ 处理请求类型
232
+ """
233
+ jsonRequestParameter = None
234
+ dataRequestParameter = None
235
+ paramsData = None
236
+ ModelData = None
237
+
238
+ if isinstance(data, dict) and data.get('MIME'):
239
+ MIME = data.get('MIME')
240
+ else:
241
+ MIME = 'application/octet-stream'
242
+
243
+ if requestType is None:
244
+ jsonRequestParameter = data
245
+ elif requestType.lower() in ["json", "json-text"]:
246
+ jsonRequestParameter = data
247
+ elif requestType.lower() == "form-data":
248
+ dataRequestParameter = MultipartEncoder(fields=data)
249
+ elif requestType.lower() == "form-model":
250
+ filename = data['filename']
251
+ file_name = data[filename].split('\\')
252
+ data[filename] = (file_name[-1], open(data[filename], 'rb'), MIME)
253
+ for k, v in data.items():
254
+ if type(v) == dict:
255
+ data[k] = (None, json.dumps(data[k]))
256
+ ModelData = data
257
+ elif requestType.lower() == "form-file":
258
+ filename = data['filename']
259
+ data[filename] = (os.path.basename(data[filename]), open(data[filename], 'rb'), MIME)
260
+ dataRequestParameter = MultipartEncoder(fields=data)
261
+ elif requestType == "PARAMS":
262
+ paramsData = data
263
+ elif requestType == "DATA":
264
+ dataRequestParameter = data
265
+ else:
266
+ log.error("请求方式不支持")
267
+
268
+ return dataRequestParameter, jsonRequestParameter, paramsData, ModelData, requestType
269
+
270
+ def authType(self, authType, url, method, parameter):
271
+ """
272
+ 处理鉴权方式
273
+ """
274
+ if self.baseUrl is None or self.isValidUrl(url):
275
+ requestUrl = url
276
+ else:
277
+ requestUrl = self.baseUrl + url
278
+ requestParameter = None
279
+ authHeaders = {}
280
+
281
+ if authType == "SIGN":
282
+ # 云服务MD5签名鉴权
283
+ config_instance = self._get_config()
284
+ # 从config中获取企业ID
285
+ enterprise_id = config_instance._enterpriseId if hasattr(config_instance, '_enterpriseId') else None
286
+ if not enterprise_id:
287
+ log.error("企业ID未配置,请在环境配置中设置enterpriseId")
288
+ raise ValueError("企业ID未配置,请在环境配置中设置enterpriseId")
289
+
290
+ if method.upper() == "GET":
291
+ # GET请求:将签名参数添加到URL参数中
292
+ # 添加默认的validateType参数
293
+ if parameter is None:
294
+ parameter = {}
295
+ if 'validateType' not in parameter:
296
+ parameter['validateType'] = config_instance._validateType if hasattr(config_instance, '_validateType') else '2' # 从配置中获取验证类型
297
+
298
+ signed_params = CloudSignUtil.generate_params_with_signature(
299
+ enterprise_id=enterprise_id,
300
+ token=config_instance._token if hasattr(config_instance, '_token') else None,
301
+ additional_params=parameter
302
+ )
303
+ # 构建带签名的URL
304
+ from urllib.parse import urlencode
305
+ query_string = urlencode(signed_params)
306
+ if '?' in requestUrl:
307
+ requestUrl += '&' + query_string
308
+ else:
309
+ requestUrl += '?' + query_string
310
+ requestParameter = None
311
+ log.info(f"GET请求URL: {requestUrl}")
312
+ else:
313
+ # POST/PUT/PATCH请求:签名信息放在请求头中
314
+ authHeaders = CloudSignUtil.generate_headers(
315
+ method=method,
316
+ url=requestUrl,
317
+ params=None,
318
+ enterprise_id=enterprise_id,
319
+ token=config_instance._token if hasattr(config_instance, '_token') else None,
320
+ body=parameter
321
+ )
322
+ requestParameter = parameter
323
+
324
+ return requestParameter, requestUrl, authHeaders
325
+ elif authType == "COOKIE" or authType is None:
326
+ requestParameter = parameter
327
+ return requestParameter, requestUrl, authHeaders
328
+ else:
329
+ log.error("鉴权方式不支持")
330
+ return requestParameter, requestUrl, authHeaders
331
+
332
+ def getCommonTestCase(self, testcase, commonFile, caseId):
333
+ """
334
+ 获取公共测试用例
335
+ """
336
+ if self.commonCase is None:
337
+ commonFile = commonFile
338
+ else:
339
+ commonFile = os.path.join(commonFile.split('common')[0], f'common/{self.commonCase}')
340
+
341
+ yaml = CloudReadYaml(commonFile).load_yaml()
342
+ commonCase = yaml.get('testcases')
343
+
344
+ for case in commonCase:
345
+ if case.get('id') == caseId:
346
+ case['assert'] = [item for item in (case.get('assert') or []) + (testcase.get('assert') or []) if item is not None]
347
+ case['saveData'] = [item for item in (case.get('saveData') or []) + (testcase.get('saveData') or []) if item is not None]
348
+ return case
349
+
350
+ raise ValueError("Case with id {} not found".format(caseId))
351
+
352
+ def assertChoose(self, ass, tips, type):
353
+ """
354
+ 断言选择器
355
+ """
356
+ if type == 'stop':
357
+ assert ass, tips
358
+ elif type == 'continue':
359
+ pytest.assume(ass, tips)
360
+
361
+ def isValidUrl(self, url):
362
+ """
363
+ 验证URL是否有效
364
+ """
365
+ try:
366
+ result = urlparse(url)
367
+ return all([result.scheme, result.netloc])
368
+ except ValueError:
369
+ return False
370
+
371
+ def handle_stream_response(self, clientSession, method, url, data, json_data, params, files, headers):
372
+ """
373
+ 处理流式响应
374
+ """
375
+ try:
376
+ response_dict = {}
377
+ answer_count = 0
378
+ complete_message = ""
379
+
380
+ with clientSession.request(
381
+ method=method,
382
+ url=url,
383
+ data=data,
384
+ json=json_data,
385
+ params=params,
386
+ files=files,
387
+ headers=headers,
388
+ stream=True
389
+ ) as response:
390
+ for chunk in response.iter_lines():
391
+ if chunk:
392
+ data = chunk.decode('utf-8')
393
+ if data.startswith('data: '):
394
+ try:
395
+ json_str = data[6:]
396
+ if json_str.strip() == '[DONE]':
397
+ if complete_message:
398
+ response_dict["complete_message"] = complete_message
399
+ continue
400
+
401
+ json_data = json.loads(json_str)
402
+
403
+ if 'answer' in json_data:
404
+ answer_count += 1
405
+ key = f"answer{answer_count}"
406
+ response_dict[key] = json_data
407
+
408
+ answer_content = json_data.get('answer', '')
409
+ if isinstance(answer_content, list):
410
+ answer_content = ''.join(str(item) for item in answer_content)
411
+ elif not isinstance(answer_content, str):
412
+ answer_content = str(answer_content)
413
+
414
+ complete_message += answer_content
415
+
416
+ except json.JSONDecodeError as e:
417
+ log.error(f"JSON解析错误: {e}")
418
+ continue
419
+
420
+ response.json = lambda: response_dict
421
+ response._content = json.dumps(response_dict).encode()
422
+ return response
423
+
424
+ except Exception as e:
425
+ log.error(f"流式处理错误: {e}")
426
+ raise
@@ -0,0 +1,311 @@
1
+ from faker import Faker
2
+ import string
3
+ import time
4
+ import random
5
+ import datetime
6
+ import pytz
7
+ import hashlib
8
+ import base64
9
+ import hmac
10
+ import json
11
+ from urllib.parse import urlencode, urlparse
12
+ from Crypto.Cipher import AES
13
+ from Crypto.Util.Padding import pad, unpad
14
+
15
+ from cloud.CloudRequestUtil import CloudHttpClient
16
+
17
+
18
+ class CloudTimes:
19
+ def __init__(self, dt=None, tz=pytz.timezone('Asia/Shanghai')):
20
+ """
21
+ 初始化时间工具类
22
+
23
+ :param dt: datetime对象,默认使用当前时间
24
+ :param tz: 时区,默认使用本地时区
25
+ """
26
+ self.tz = tz
27
+ self.dt = dt or datetime.datetime.now(self.tz)
28
+
29
+ def to_datetime(self):
30
+ """
31
+ 返回datetime对象
32
+
33
+ :return: datetime对象
34
+ """
35
+ return self.dt
36
+
37
+ def to_str(self, fmt='%Y-%m-%dT%H:%M:%SZ'):
38
+ """
39
+ 将datetime对象转换为指定格式的字符串
40
+
41
+ :param fmt: 时间格式,默认为'%Y-%m-%d %H:%M:%S'
42
+ :return: 格式化后的时间字符串
43
+ """
44
+ return self.dt.strftime(fmt)
45
+
46
+
47
+ class CloudDataGenerator:
48
+ """
49
+ 生成中文数据的工具类
50
+ """
51
+
52
+ def __init__(self):
53
+ self.fake = Faker(locale='zh_CN')
54
+
55
+ def generate_name(self):
56
+ """
57
+ 生成中文姓名,返回字符串。
58
+ """
59
+ return self.fake.name()
60
+
61
+ def generate_address(self):
62
+ """
63
+ 生成中文地址,返回字符串。
64
+ """
65
+ return self.fake.address()
66
+
67
+ def generate_phone_number(self):
68
+ """
69
+ 生成中文手机号,返回字符串。
70
+ """
71
+ return self.fake.phone_number()
72
+
73
+ def generate_id_number(self):
74
+ """
75
+ 生成中文身份证号码,返回字符串。
76
+ """
77
+ return self.fake.ssn()
78
+
79
+ def random_number(self, digits=4):
80
+ """
81
+ 生成一个4位随机整数并转换为字符串类型
82
+ 如果生成的整数不足4位,则在左侧用0进行填充
83
+ """
84
+ digits = int(digits)
85
+ return "{:04d}".format(self.fake.random_number(digits=digits))
86
+
87
+ def get_cloud_password(self):
88
+ """
89
+ 获取初始化的SK 加密后的密码
90
+ :return:
91
+ """
92
+ # 这里不能直接引用config,因为config类还没有定义
93
+ # 在实际使用时,会通过实例方法调用
94
+ return "default_cloud_password"
95
+
96
+ @staticmethod
97
+ def start_of_day():
98
+ """
99
+ 获取当前时间开始时间戳:eg:2023-06-01 00:00:00
100
+ """
101
+ now = datetime.datetime.now()
102
+ return datetime.datetime(now.year, now.month, now.day)
103
+
104
+ @staticmethod
105
+ def end_of_day():
106
+ """
107
+ 获取当前时间开始时间戳:eg:2023-06-01 23:59:59
108
+ """
109
+ return CloudDataGenerator.start_of_day() + datetime.timedelta(days=1) - datetime.timedelta(seconds=1)
110
+
111
+ def start_of_day_s(self):
112
+ """
113
+ 获取当前时间开始时间戳:eg:1685548800 秒级
114
+ """
115
+ return int(time.mktime(CloudDataGenerator.start_of_day().timetuple()))
116
+
117
+ def end_of_day_s(self):
118
+ """
119
+ 获取当前时间结束时间戳:eg:1685635199 秒级
120
+ """
121
+ return int(time.mktime(CloudDataGenerator.end_of_day().timetuple()))
122
+
123
+ def random_externalId(self):
124
+ """
125
+ 生成唯一性数据,crm用于 外部企业客户id
126
+ """
127
+ num = str(random.randint(1000, 9999))
128
+ src_uppercase = string.ascii_uppercase # string_大写字母
129
+ src_lowercase = string.ascii_lowercase # string_小写字母
130
+ chrs = random.sample(src_lowercase + src_uppercase, 3)
131
+ for i in chrs:
132
+ num += i
133
+ return num
134
+
135
+ def encryptPassword(self, plain_text='Aa112233'):
136
+ """
137
+ 加密 - 使用默认密钥
138
+ """
139
+ # 使用默认密钥进行加密
140
+ password = "default_encryption_key"
141
+ # 设置随机数生成器的种子
142
+ secure_random = hashlib.sha1(password.encode()).digest()
143
+ # 创建对称加密密钥生成器
144
+ kgen = hashlib.sha1(secure_random).digest()[:16]
145
+ # 创建密码器并初始化
146
+ cipher = AES.new(kgen, AES.MODE_ECB)
147
+ # 加密明文(使用PKCS7填充)
148
+ padded_plain_text = pad(plain_text.encode(), AES.block_size)
149
+ encrypted_bytes = cipher.encrypt(padded_plain_text)
150
+ # 将加密结果转换为16进制字符串
151
+ encrypted_text = base64.b16encode(encrypted_bytes).decode().lower()
152
+ return encrypted_text
153
+
154
+ def decrypt(self, encrypted_text, password):
155
+ """
156
+ # 解密
157
+ """
158
+ # 设置随机数生成器的种子
159
+ secure_random = hashlib.sha1(password.encode()).digest()
160
+ # 创建对称加密密钥生成器
161
+ kgen = hashlib.sha1(secure_random).digest()[:16]
162
+ # 创建密码器并初始化
163
+ cipher = AES.new(kgen, AES.MODE_ECB)
164
+ # 解密密文(parseHexStr2Byte方法为将16进制字符串转为二进制字节数组)
165
+ encrypted_bytes = base64.b16decode(encrypted_text)
166
+ decrypted_bytes = cipher.decrypt(encrypted_bytes)
167
+ decrypted_text = unpad(decrypted_bytes, AES.block_size).decode()
168
+ return decrypted_text
169
+
170
+ def generate_cloud_signature(self, method, url, params, access_key_id, access_key_secret):
171
+ """
172
+ 生成云服务签名
173
+ """
174
+ # 获取当前时间戳
175
+ timestamp = int(time.time())
176
+
177
+ # 构建签名字符串
178
+ # 1. 请求方法
179
+ string_to_sign = method.upper() + "\n"
180
+
181
+ # 2. 请求路径
182
+ parsed_url = urlparse(url)
183
+ string_to_sign += parsed_url.path + "\n"
184
+
185
+ # 3. 查询参数(按字典序排序)
186
+ if params:
187
+ sorted_params = sorted(params.items())
188
+ query_string = "&".join([f"{k}={v}" for k, v in sorted_params])
189
+ string_to_sign += query_string + "\n"
190
+ else:
191
+ string_to_sign += "\n"
192
+
193
+ # 4. 时间戳
194
+ string_to_sign += str(timestamp)
195
+
196
+ # 使用HMAC-SHA256进行签名
197
+ signature = hmac.new(
198
+ access_key_secret.encode('utf-8'),
199
+ string_to_sign.encode('utf-8'),
200
+ hashlib.sha256
201
+ ).hexdigest()
202
+
203
+ # 构建Authorization头
204
+ auth_header = f"Cloud {access_key_id}:{signature}"
205
+
206
+ return auth_header, timestamp
207
+
208
+
209
+ class config:
210
+ """
211
+ 配置文件
212
+ """
213
+
214
+ def __init__(self, baseUrl=None, token=None, CloudPassword=None,
215
+ commonTestCasePath=None, methObj=None, Session: CloudHttpClient = CloudHttpClient(),
216
+ assertFail='stop', tEnv='base', enterpriseId=None, validateType='2'):
217
+ """
218
+ 初始化配置文件
219
+ """
220
+ self._baseUrl = baseUrl
221
+ self._token = token
222
+ self._enterpriseId = enterpriseId
223
+ self._validateType = validateType
224
+ # 加密后的密码
225
+ self._CloudPassword = CloudPassword
226
+ self._commonTestCasePath = commonTestCasePath
227
+ self._methObj = methObj
228
+ self._assertFail = assertFail
229
+ self._tEnv = tEnv
230
+ # 构建全局session
231
+ self._Session = Session
232
+
233
+ @property
234
+ def Session(self):
235
+ return self._Session
236
+
237
+ @Session.setter
238
+ def Session(self, value):
239
+ self._Session = value
240
+
241
+ @property
242
+ def methObj(self):
243
+ return self._methObj
244
+
245
+ @methObj.setter
246
+ def methObj(self, value):
247
+ self._methObj = value
248
+
249
+ @property
250
+ def CloudPassword(self):
251
+ return self._CloudPassword
252
+
253
+ @CloudPassword.setter
254
+ def CloudPassword(self, value):
255
+ self._CloudPassword = value
256
+
257
+ @property
258
+ def commonTestCasePath(self):
259
+ return self._commonTestCasePath
260
+
261
+ @commonTestCasePath.setter
262
+ def commonTestCasePath(self, value):
263
+ self._commonTestCasePath = value
264
+
265
+ @property
266
+ def baseUrl(self):
267
+ return self._baseUrl
268
+
269
+ @baseUrl.setter
270
+ def baseUrl(self, value):
271
+ self._baseUrl = value
272
+
273
+ @property
274
+ def assertFail(self):
275
+ return self._assertFail
276
+
277
+ @assertFail.setter
278
+ def assertFail(self, value):
279
+ self._assertFail = value
280
+
281
+ @property
282
+ def enterpriseId(self):
283
+ return self._enterpriseId
284
+
285
+ @enterpriseId.setter
286
+ def enterpriseId(self, value):
287
+ self._enterpriseId = value
288
+
289
+ @property
290
+ def tEnv(self):
291
+ return self._tEnv
292
+
293
+ @tEnv.setter
294
+ def tEnv(self, value):
295
+ self._tEnv = value
296
+
297
+ @property
298
+ def validateType(self):
299
+ return self._validateType
300
+
301
+ @validateType.setter
302
+ def validateType(self, value):
303
+ self._validateType = value
304
+
305
+ @property
306
+ def token(self):
307
+ return self._token
308
+
309
+ @token.setter
310
+ def token(self, value):
311
+ self._token = value
cloud/CloudLogUtil.py ADDED
@@ -0,0 +1,36 @@
1
+ import logging
2
+ import time
3
+
4
+ """
5
+ cloud日志工具类:
6
+ 定义日志输出
7
+ """
8
+
9
+
10
+ class CloudLogConfig:
11
+
12
+ @staticmethod
13
+ def logger_set():
14
+ # 第一步:创建一个日志收集器
15
+ log = logging.getLogger()
16
+
17
+ # 第二步:设置收集器收集的等级
18
+ log.setLevel(logging.INFO)
19
+
20
+ # 第三步:设置输出渠道以及输出渠道的等级
21
+ curTime = time.strftime("%Y-%m-%d %H-%M-%S", time.localtime())
22
+
23
+ sh = logging.StreamHandler()
24
+ sh.setLevel(logging.INFO)
25
+ log.addHandler(sh)
26
+
27
+ # 创建一个输出格式对象
28
+ formats = '%(thread)d: %(asctime)s -- [%(filename)s-->line:%(lineno)d] - %(levelname)s: %(message)s'
29
+ form = logging.Formatter(formats)
30
+ # 将输出格式添加到输出渠道
31
+ sh.setFormatter(form)
32
+
33
+ return log
34
+
35
+
36
+ log = CloudLogConfig.logger_set()
@@ -0,0 +1,107 @@
1
+ import jsonpath
2
+ import requests
3
+ from urllib.parse import urlparse
4
+ from requests import Session
5
+
6
+
7
+ class CloudHttpClient:
8
+ """
9
+ 对 requests 库进行简单封装的类,可以方便地进行 HTTP 请求和处理响应。
10
+ """
11
+
12
+ def __init__(self, base_url=None):
13
+ """
14
+ 初始化方法,接受一个基础URL参数,并创建一个Session对象
15
+ :param base_url: str 基础URL,例如 https://api.cloud.com
16
+ """
17
+ if base_url is not None:
18
+ self.base_url = base_url
19
+
20
+ self.session: Session = requests.Session()
21
+
22
+ def request(self, method, url, **kwargs):
23
+ """
24
+ 发送HTTP请求,并返回响应对象
25
+ :param method: str 请求方法,例如 GET、POST、PUT等
26
+ :param url: str 请求URL,会和base_url拼接成完整的URL
27
+ :param kwargs: dict 其他requests.request方法支持的参数
28
+ :return: requests.Response 响应对象
29
+ """
30
+ response = self.session.request(method, url, **kwargs)
31
+ # 判断请求url 是否包含 openapi_login 用于处理 单点登录到系统后进行系统的接口请求鉴权处理
32
+ # if 'openapi_login' in url:
33
+ # # 需要请求接口 获取 Tsessionid
34
+ # # 先从url 中解析出请求域名
35
+ # parsed_url = urlparse(url)
36
+ # domain = parsed_url.scheme + '://' + parsed_url.netloc
37
+ # # 进行接口请求 拿到响应
38
+ # personalResponse = self.session.request(method='GET', url=domain + '/api/personal/info/get')
39
+ # # 拿响应的$.result.authToken
40
+ # authToken = jsonpath.jsonpath(personalResponse.json(), '$.result.authToken')[0]
41
+ # # 后续的请求头上都需要带上 Tsessionid
42
+ # self.session.headers.update({'Tsessionid': authToken})
43
+
44
+ # 处理HTTP错误
45
+ http_error_msg = None
46
+ if 400 <= response.status_code < 500:
47
+ http_error_msg = (
48
+ f"{response.status_code} 客户端错误 Error: {response.text} for url: {response.url}"
49
+ )
50
+ elif 500 <= response.status_code < 600:
51
+ http_error_msg = (
52
+ f"{response.status_code} 服务端错误 Error: {response.text} for url: {response.url}"
53
+ )
54
+
55
+ return response
56
+
57
+ def get(self, url, params=None, **kwargs):
58
+ """
59
+ 发送GET请求,并返回响应对象
60
+ :param url: str 请求URL,会和base_url拼接成完整的URL
61
+ :param params: dict 查询参数,会被转换为?key=value&key=value的形式拼接到URL后面
62
+ :param kwargs: dict 其他requests.get方法支持的参数
63
+ :return: requests.Response 响应对象
64
+ """
65
+ return self.request('GET', url, params=params, **kwargs)
66
+
67
+ def post(self, url, json=None, data=None, **kwargs):
68
+ """
69
+ 发送POST请求,并返回响应对象
70
+ :param url: str 请求URL,会和base_url拼接成完整的URL
71
+ :param data: dict 请求体参数,以表单形式提交
72
+ :param json: dict 请求体参数,以JSON格式提交
73
+ :param kwargs: dict 其他requests.post方法支持的参数
74
+ :return: requests.Response 响应对象
75
+ """
76
+ return self.request('POST', url, data=data, json=json, **kwargs)
77
+
78
+ def put(self, url, json=None, data=None, **kwargs):
79
+ """
80
+ 发送PUT请求,并返回响应对象
81
+ :param url: str 请求URL,会和base_url拼接成完整的URL
82
+ :param data: dict 请求体参数,以表单形式提交
83
+ :param json: dict 请求体参数,以JSON格式提交
84
+ :param kwargs: dict 其他requests.put方法支持的参数
85
+ :return: requests.Response 响应对象
86
+ """
87
+ return self.request('PUT', url, data=data, json=json, **kwargs)
88
+
89
+ def delete(self, url, **kwargs):
90
+ """
91
+ 发送DELETE请求,并返回响应对象
92
+ :param url: str 请求URL,会和base_url拼接成完整的URL
93
+ :param kwargs: dict 其他requests.delete方法支持的参数
94
+ :return: requests.Response 响应对象
95
+ """
96
+ return self.request('DELETE', url, **kwargs)
97
+
98
+ def patch(self, url, json=None, data=None, **kwargs):
99
+ """
100
+ 发送PATCH请求,并返回响应对象
101
+ :param url: str 请求URL,会和base_url拼接成完整的URL
102
+ :param data: dict 请求体参数,以表单形式提交
103
+ :param json: dict 请求体参数,以JSON格式提交
104
+ :param kwargs: dict 其他requests.patch方法支持的参数
105
+ :return: requests.Response 响应对象
106
+ """
107
+ return self.request('PATCH', url, data=data, json=json, **kwargs)
cloud/CloudSignUtil.py ADDED
@@ -0,0 +1,117 @@
1
+ import hashlib
2
+ import json
3
+ import time
4
+ from urllib.parse import urlparse
5
+
6
+ from cloud.CloudLogUtil import log
7
+
8
+
9
+ class CloudSignUtil:
10
+ """
11
+ cloud签名工具类 - 基于MD5的签名算法
12
+ 鉴权方式:MD5({enterpriseId}+{timestamp}+{token})
13
+ """
14
+
15
+ @staticmethod
16
+ def generate_md5_signature(enterprise_id, timestamp, token):
17
+ """
18
+ 生成MD5签名
19
+ :param enterprise_id: 企业ID
20
+ :param timestamp: 时间戳
21
+ :param token: 访问token
22
+ :return: 32位小写MD5签名
23
+ """
24
+ # 构建签名字符串:{enterpriseId}+{timestamp}+{token}
25
+ sign_string = f"{enterprise_id}{timestamp}{token}"
26
+ log.info(f"签名字符串: {sign_string}")
27
+
28
+ # 生成MD5签名
29
+ signature = hashlib.md5(sign_string.encode('utf-8')).hexdigest()
30
+ log.info(f"生成的MD5签名: {signature}")
31
+
32
+ return signature
33
+
34
+ @staticmethod
35
+ def generate_headers(method, url, params, enterprise_id, token, body=None):
36
+ """
37
+ 生成云服务请求头 - 基于MD5签名
38
+ :param method: 请求方法
39
+ :param url: 请求URL
40
+ :param params: 查询参数
41
+ :param enterprise_id: 企业ID
42
+ :param token: 访问token
43
+ :param body: 请求体(用于POST/PUT请求)
44
+ :return: 请求头字典
45
+ """
46
+ # 获取当前时间戳(秒级)
47
+ timestamp = int(time.time())
48
+
49
+ # 生成MD5签名
50
+ signature = CloudSignUtil.generate_md5_signature(enterprise_id, timestamp, token)
51
+
52
+ # 构建请求头
53
+ headers = {
54
+ 'Authorization': f"Cloud {token}:{signature}",
55
+ 'X-Timestamp': str(timestamp),
56
+ 'Content-Type': 'application/json',
57
+ 'User-Agent': 'CloudAPISDK/1.0'
58
+ }
59
+
60
+ # 添加企业ID到请求头(如果需要)
61
+ headers['X-Enterprise-Id'] = str(enterprise_id)
62
+
63
+ log.info(f"生成的请求头: {headers}")
64
+
65
+ return headers
66
+
67
+ @staticmethod
68
+ def generate_params_with_signature(enterprise_id, token, additional_params=None):
69
+ """
70
+ 生成带签名的请求参数
71
+ :param enterprise_id: 企业ID
72
+ :param token: 访问token
73
+ :param additional_params: 额外参数
74
+ :return: 包含签名的参数字典
75
+ """
76
+ # 获取当前时间戳(秒级)
77
+ timestamp = int(time.time())
78
+
79
+ # 生成MD5签名
80
+ signature = CloudSignUtil.generate_md5_signature(enterprise_id, timestamp, token)
81
+
82
+ # 构建基础参数
83
+ params = {
84
+ 'enterpriseId': enterprise_id,
85
+ 'timestamp': timestamp,
86
+ 'sign': signature
87
+ }
88
+
89
+ # 添加额外参数
90
+ if additional_params:
91
+ params.update(additional_params)
92
+
93
+ log.info(f"生成的请求参数: {params}")
94
+
95
+ return params
96
+
97
+ @staticmethod
98
+ def validate_signature(enterprise_id, timestamp, token, received_signature):
99
+ """
100
+ 验证签名
101
+ :param enterprise_id: 企业ID
102
+ :param timestamp: 时间戳
103
+ :param token: 访问token
104
+ :param received_signature: 接收到的签名
105
+ :return: 验证结果
106
+ """
107
+ # 生成期望的签名
108
+ expected_signature = CloudSignUtil.generate_md5_signature(enterprise_id, timestamp, token)
109
+
110
+ # 比较签名
111
+ is_valid = expected_signature == received_signature
112
+
113
+ log.info(f"签名验证结果: {is_valid}")
114
+ log.info(f"期望签名: {expected_signature}")
115
+ log.info(f"接收签名: {received_signature}")
116
+
117
+ return is_valid
cloud/CloudYamlUtil.py ADDED
@@ -0,0 +1,197 @@
1
+ import re
2
+ import yaml
3
+ import json
4
+ from cloud.CloudLogUtil import log
5
+ from cloud.CloudApiConfig import config
6
+
7
+
8
+ class CloudPlaceholderYaml:
9
+ """
10
+ 用于替换yaml文件中的占位符
11
+ """
12
+
13
+ def __init__(self, yaml_str=None, reString=None, attrObj=config, methObj=None, gloObj=config):
14
+ if yaml_str:
15
+ self.yaml_str = json.dumps(yaml_str)
16
+ else:
17
+ self.yaml_str = str(reString)
18
+
19
+ self.attrObj = attrObj
20
+
21
+ # 修复methObj的初始化逻辑
22
+ if methObj is not None:
23
+ self.methObj = methObj
24
+ elif hasattr(config, 'methObj') and config.methObj is not None:
25
+ self.methObj = config.methObj
26
+ else:
27
+ # 如果都没有,创建一个默认的CloudDataGenerator实例
28
+ from cloud.CloudApiConfig import CloudDataGenerator
29
+ self.methObj = CloudDataGenerator()
30
+ log.warning("methObj未配置,使用默认的CloudDataGenerator实例")
31
+
32
+ self.gloObj = gloObj
33
+
34
+ def replace(self):
35
+ # 定义正则表达式模式
36
+ # 用于匹配 ${attr} 和 #{method} 这样的占位符
37
+ # $() #() 如果不匹配则报错
38
+ pattern_attr = re.compile(r'\$\{(\w+)\}')
39
+ pattern_method = re.compile(r'\#\{(.*?)\}')
40
+ pattern_glo = re.compile(r'\$\$\{(\w+)\}')
41
+
42
+ # 定义全局替换函数
43
+ def replace_glo(match):
44
+ # 获取占位符中的属性名
45
+ attr_name = match.group(1)
46
+ # 如果对象中有该属性,则返回该属性的值
47
+ if hasattr(self.gloObj, attr_name):
48
+ # 获取属性的值
49
+ attr_value = getattr(self.gloObj, attr_name)
50
+ # 如果属性的值是字符串,则返回该字符串
51
+ if isinstance(attr_value, str):
52
+ return str(attr_value)
53
+ # 如果属性的值是可调用对象,则返回方法名
54
+ elif callable(attr_value):
55
+ return match.group(0)
56
+ # 如果属性的值是字典,则返回该字典的字符串表示
57
+ elif isinstance(attr_value, dict):
58
+ return str(attr_value)
59
+ # 如果属性的值是列表,则返回该列表的字符串表示
60
+ elif isinstance(attr_value, list):
61
+ return str(",".join(str(x) for x in attr_value))
62
+ # 否则,返回属性的值(将其转换为字符串)
63
+ else:
64
+ return str(attr_value)
65
+ # 否则返回原字符串
66
+ return match.group(0)
67
+
68
+ # 定义替换函数
69
+ def replace_attr(match):
70
+ # 获取占位符中的属性名
71
+ attr_name = match.group(1)
72
+ # 如果对象中有该属性,则返回该属性的值
73
+ if hasattr(self.attrObj, attr_name):
74
+ # 获取属性的值
75
+ attr_value = getattr(self.attrObj, attr_name)
76
+ # 如果属性的值是字符串,则返回该字符串
77
+ if isinstance(attr_value, str):
78
+ return str(attr_value)
79
+ # 如果属性的值是可调用对象,则返回方法名
80
+ elif callable(attr_value):
81
+ return match.group(0)
82
+ # 如果属性的值是字典,则返回该字典的字符串表示
83
+ elif isinstance(attr_value, dict):
84
+ return str(attr_value)
85
+ # 如果属性的值是列表,则返回该列表的字符串表示
86
+ elif isinstance(attr_value, list):
87
+ return str(",".join(str(x) for x in attr_value))
88
+ # 否则,返回属性的值(将其转换为字符串)
89
+ else:
90
+ return str(attr_value)
91
+ # 否则返回原字符串
92
+ return match.group(0)
93
+
94
+ # 定义替换函数
95
+ def replace_method(match):
96
+ # 获取占位符中的方法名
97
+ method_name = match.group(1)
98
+ args = None
99
+ if '(' in match.group(1):
100
+ # 获取占位符中的方法名
101
+ method_name = match.group(1).split('(')[0]
102
+ # 获取参数列表
103
+ args_str = match.group(1).split('(')[1][:-1]
104
+ args = [arg.strip() for arg in args_str.split(',')]
105
+
106
+ # 如果对象中有该方法,并且该方法是可调用的,则返回该方法的返回值
107
+ if hasattr(self.methObj, method_name):
108
+ # 获取方法
109
+ method = getattr(self.methObj, method_name)
110
+ # 如果方法是可调用对象,则调用该方法并返回其返回值的字符串表示
111
+ if callable(method):
112
+ try:
113
+ if args:
114
+ method_value = method(*args)
115
+ else:
116
+ method_value = method()
117
+ if isinstance(method_value, str):
118
+ return str(method_value)
119
+ else:
120
+ return str(method_value)
121
+ except Exception as e:
122
+ log.error(f"调用方法 {method_name} 时出错: {e}")
123
+ return match.group(0)
124
+ # 否则,返回方法的字符串表示
125
+ else:
126
+ return str(method)
127
+ else:
128
+ log.warning(f"方法 {method_name} 在 methObj 中不存在,methObj类型: {type(self.methObj)}")
129
+ log.warning(f"methObj 可用方法: {[attr for attr in dir(self.methObj) if not attr.startswith('_')]}")
130
+ # 否则返回原字符串
131
+ return match.group(0)
132
+
133
+ # 判断是否有需要替换的再进行替换
134
+ log.info(f"开始替换str中的占位符: {self.yaml_str}")
135
+ log.info(f"使用的methObj: {type(self.methObj)}")
136
+ # 先进行全局替换
137
+ replaced_str = pattern_glo.sub(replace_glo, self.yaml_str)
138
+ # 替换占位符中的属性
139
+ replaced_str = pattern_attr.sub(replace_attr, replaced_str)
140
+ # 替换占位符中的方法
141
+ replaced_str = pattern_method.sub(replace_method, replaced_str)
142
+ self.replaced_str = replaced_str
143
+ log.info("替换后的str内容为:{}".format(replaced_str))
144
+ return self
145
+
146
+ def jsonLoad(self):
147
+ # 把replaced_str中的"[]"替换为[] "{}"替换为{} "'"替换为"/"" 'None换为 null'
148
+ replaced_str = self.replaced_str.replace('"[', '[').replace(']"', ']').replace('"{', '{').replace('}"', '}').replace("'", "\"").replace('None','null')
149
+ try:
150
+ replaced_str = json.loads(replaced_str)
151
+ log.info("替换后jsonLoad的str内容为:{}".format(replaced_str))
152
+ return replaced_str
153
+ except:
154
+ log.info(f'*************替换失败-YAML,请检查格式{replaced_str}******************')
155
+
156
+ def textLoad(self):
157
+ return json.loads(self.replaced_str)
158
+
159
+
160
+ class CloudReadYaml:
161
+ """
162
+ 用于读取yaml文件的工具类
163
+ """
164
+
165
+ def __init__(self, yaml_file):
166
+ self.yaml_file = yaml_file
167
+
168
+ def load_yaml(self):
169
+ """
170
+ 读取yaml文件,并返回其中的数据
171
+ :return: dict
172
+ """
173
+ with open(self.yaml_file, encoding='utf-8') as f:
174
+ data = yaml.safe_load(f)
175
+ return data if data is not None else {} # 如果data为None,则返回空字典
176
+
177
+ def get(self, key, default=None):
178
+ """
179
+ 获取yaml文件中的数据
180
+ :param key: 数据的键
181
+ :param default: 如果获取失败,则返回该默认值
182
+ :return: dict
183
+ """
184
+ # 读取yaml文件
185
+ data = self.load_yaml()
186
+ # 获取数据
187
+ return data.get(key, default) if data is not None else default
188
+
189
+ def get_all(self):
190
+ """
191
+ 获取yaml文件中的所有数据
192
+ :return: dict
193
+ """
194
+ # 读取yaml文件
195
+ data = self.load_yaml()
196
+ # 如果data为None,则返回空字典
197
+ return data if data is not None else {}
cloud/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ from cloud.CloudAPIRequest import CloudAPIRequest
2
+ from cloud.CloudApiConfig import config, CloudDataGenerator
3
+ from cloud.CloudRequestUtil import CloudHttpClient
4
+
5
+ # 创建config实例
6
+ config = config()
7
+ # 初始化配置实例
8
+ config.methObj = CloudDataGenerator()
9
+ config.Session = CloudHttpClient()
cloud/cli.py ADDED
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Cloud API SDK 命令行工具
5
+ """
6
+
7
+ import argparse
8
+ import sys
9
+ import os
10
+ from cloud_api_sdk import config, CloudAPIRequest
11
+
12
+
13
+ def main():
14
+ """
15
+ 主函数
16
+ """
17
+ parser = argparse.ArgumentParser(description='Cloud API SDK 测试工具')
18
+ parser.add_argument('testcase', help='测试用例文件路径')
19
+ parser.add_argument('--base-url', help='基础URL')
20
+ parser.add_argument('--access-key-id', help='访问密钥ID')
21
+ parser.add_argument('--access-key-secret', help='访问密钥Secret')
22
+ parser.add_argument('--cloud-password', help='云服务密码')
23
+ parser.add_argument('--env', default='test', help='运行环境')
24
+ parser.add_argument('--common-path', help='公共用例路径')
25
+ parser.add_argument('--assert-fail', default='stop', choices=['stop', 'continue'], help='断言失败处理模式')
26
+
27
+ args = parser.parse_args()
28
+
29
+ # 配置环境
30
+ if args.base_url:
31
+ config.baseUrl = args.base_url
32
+ if args.access_key_id:
33
+ config.accessKeyId = args.access_key_id
34
+ if args.access_key_secret:
35
+ config.accessKeySecret = args.access_key_secret
36
+ if args.cloud_password:
37
+ config.CloudPassword = args.cloud_password
38
+ if args.common_path:
39
+ config.commonTestCasePath = args.common_path
40
+ config.assertFail = args.assert_fail
41
+ config.tEnv = args.env
42
+
43
+ # 检查测试用例文件是否存在
44
+ if not os.path.exists(args.testcase):
45
+ print(f"错误: 测试用例文件 {args.testcase} 不存在")
46
+ sys.exit(1)
47
+
48
+ # 执行测试
49
+ try:
50
+ class TestRunner:
51
+ pass
52
+
53
+ test_runner = TestRunner()
54
+ CloudAPIRequest().doRequest(args.testcase, test_runner)
55
+ print("测试执行完成")
56
+ except Exception as e:
57
+ print(f"测试执行失败: {e}")
58
+ sys.exit(1)
59
+
60
+
61
+ if __name__ == "__main__":
62
+ main()
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) [2025] [天润-测试]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,21 @@
1
+ Metadata-Version: 2.1
2
+ Name: CloudApiRequest
3
+ Version: 1.0.0
4
+ Summary: 天润cloud接口测试库
5
+ Home-page: https://www.ti-net.com.cn/
6
+ Author: 天润-测试
7
+ Author-email: wangwd@ti-net.com.cn
8
+ License: MIT
9
+ Requires-Python: >=3.7
10
+ License-File: LICENSE
11
+ Requires-Dist: Faker==18.4.0
12
+ Requires-Dist: jsonpath==0.82
13
+ Requires-Dist: pytz==2023.3
14
+ Requires-Dist: PyYAML==6.0
15
+ Requires-Dist: requests==2.28.2
16
+ Requires-Dist: allure-pytest==2.13.2
17
+ Requires-Dist: allure-python-commons==2.13.2
18
+ Requires-Dist: requests-toolbelt==1.0.0
19
+ Requires-Dist: pycryptodome==3.17
20
+ Requires-Dist: pytest-assume==2.4.3
21
+
@@ -0,0 +1,13 @@
1
+ cloud/CloudAPIRequest.py,sha256=bji5WJJ4UQPO7EhQ2IsFMAOcG1aHtxjRU-0zwzNek2U,19441
2
+ cloud/CloudApiConfig.py,sha256=6mF-guPi92UyjE0o7pXVd5cZKZAJHmohv16CvziVkGs,9138
3
+ cloud/CloudLogUtil.py,sha256=EXnU6QXFxl_npCcJgQvhtlG8uR57NmbOovFjVMHPOpQ,935
4
+ cloud/CloudRequestUtil.py,sha256=bsbI74p1iu4weXbDKqwhXvH1KwezvxYXdrXBMc9L3sE,4827
5
+ cloud/CloudSignUtil.py,sha256=3D2etwvk1LYRE3GzoU6qXfTIM4FhEFjmT-i458Q2uCA,3849
6
+ cloud/CloudYamlUtil.py,sha256=_WmtzLOBznwYNY02JOQxOB3B8WvgJONk-PV7grw2C_U,8392
7
+ cloud/__init__.py,sha256=RgNAN_vKhCHnwcBy0hc1ttrr5x58bO_u6TNICZDSfFw,306
8
+ cloud/cli.py,sha256=VId1s29djJu7QrkXJc2je4fRV030vqD3nwuI23AN8Ig,1986
9
+ cloudapirequest-1.0.0.dist-info/LICENSE,sha256=hMjjUYpALQY0E8hzysmZszKwYy2mt8tUvAFQfIIbwV4,1099
10
+ cloudapirequest-1.0.0.dist-info/METADATA,sha256=3uFgDtx9S2odQlKmBxEKJULLQKq-rxcix41RpnET33I,608
11
+ cloudapirequest-1.0.0.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
12
+ cloudapirequest-1.0.0.dist-info/top_level.txt,sha256=lBiHHi76sKOBY-DmE0iDypxZxv-TTCnvHeiFleC_PuA,6
13
+ cloudapirequest-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.3.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ cloud