CloudApiRequest 1.0.0__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.

Potentially problematic release.


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

@@ -0,0 +1,20 @@
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
@@ -0,0 +1,16 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ CloudApiRequest.egg-info/PKG-INFO
5
+ CloudApiRequest.egg-info/SOURCES.txt
6
+ CloudApiRequest.egg-info/dependency_links.txt
7
+ CloudApiRequest.egg-info/requires.txt
8
+ CloudApiRequest.egg-info/top_level.txt
9
+ cloud/CloudAPIRequest.py
10
+ cloud/CloudApiConfig.py
11
+ cloud/CloudLogUtil.py
12
+ cloud/CloudRequestUtil.py
13
+ cloud/CloudSignUtil.py
14
+ cloud/CloudYamlUtil.py
15
+ cloud/__init__.py
16
+ cloud/cli.py
@@ -0,0 +1,10 @@
1
+ Faker==18.4.0
2
+ jsonpath==0.82
3
+ pytz==2023.3
4
+ PyYAML==6.0
5
+ requests==2.28.2
6
+ allure-pytest==2.13.2
7
+ allure-python-commons==2.13.2
8
+ requests_toolbelt==1.0.0
9
+ pycryptodome==3.17
10
+ pytest-assume==2.4.3
@@ -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,20 @@
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
@@ -0,0 +1,134 @@
1
+ 项目名称
2
+
3
+ Cloud API自动化测试框架,封装requests库.使用Yaml文件管理测试用例,支持数据驱动,支持多环境配置,支持多线程执行,支持测试报告生成.
4
+
5
+ 当前版本 https://pypi.org/project/CloudApiRequest/
6
+
7
+ ## 项目结构
8
+
9
+ ```
10
+ ├── dist # 打包目录 版本发布
11
+ ├── test # 测试目录 测试类
12
+ ├── cloud # 源码目录 框架源码
13
+ ```
14
+
15
+ ## 运行条件
16
+
17
+ Python 3.7+
18
+
19
+ ```
20
+ pip install -r requirements.txt
21
+ ```
22
+
23
+ 完成依赖安装
24
+
25
+ ## 快速开始
26
+
27
+ ```
28
+ 进行测试
29
+ pytest -s
30
+ pytest -s ./testcase --env=bj --tenv=gray -n=2 --alluredir ./result
31
+
32
+ --env 环境配置
33
+ -n 多线程执行
34
+ --alluredir allure报告生成目录
35
+
36
+ 查看报告
37
+ allure serve ./result
38
+ .result 为allure报告生成目录
39
+ ```
40
+
41
+ ## 开发方式
42
+
43
+ 导入cloudrequest库
44
+ ```
45
+ pip install CloudApiRequest-v*.whl
46
+ ```
47
+
48
+ 更新版本
49
+ ```
50
+ pip uninstall CloudApiRequest
51
+ pip install CloudApiRequest-v*.whl
52
+ ```
53
+
54
+ 引用CloudApiRequest
55
+ ```
56
+ from cloud import config
57
+ # 平台与运行环境选择
58
+ def pytest_addoption(parser):
59
+ parser.addoption(
60
+ "--env", action="store", default="test", help="test:表示测试环境,默认测试环境"
61
+ )
62
+ parser.addoption(
63
+ "--tenv", action="store", default="gray", help="tenv:表示蓝绿环境,默认gray灰度环境"
64
+ )
65
+ # 配置环境
66
+ config.baseUrl = envConfig.urlPath # 基础请求地址
67
+ config.token = envConfig.token # token
68
+ config.commonTestCasePath = os.path.join(DATA_JSON_PATH, "common.yaml") # common 公共用例路径
69
+ config.methObj=method() # 用于自定义"#{fun}"方法调用的函数 ,不想使用sdk自带的方法时,可以自定义方法,在yaml中使用
70
+ config.session=request.Session() # 用于传递请求session实体类,可以传递cookies
71
+ config.tEnv = request.config.getoption("tenv") # 运行环境选择
72
+
73
+ ```
74
+
75
+ ```
76
+ from cloud import CloudAPIRequest
77
+ # 执行测试 传入yamlFile文件路径 传入Test_pyt测试类 也可以使用自定义commonfile文件,不是用全局配置的commonfile。直接写yaml文件即可
78
+ CloudAPIRequest('cloud_api.yaml').doRequest(yamlFile,Test_pyt)
79
+ ```
80
+
81
+ ## 用例示例
82
+
83
+ ```yaml
84
+ id: create_client # 用例id
85
+ name: 示例测试用例 # 用例名称
86
+ testcases: # 测试用例步骤 #
87
+ - sleep: 1 # 等待时间
88
+ - skip: 1 # 跳过步骤
89
+ - kind: common #用例类型 common去取公共用例,需要公共用例路径
90
+ id: getQueues
91
+ name: 获取队列信息
92
+ - name: 查找队列信息 # 步骤名称
93
+ id: list_queues # 步骤id
94
+ api: /list_queues # 请求地址 # todo: 支持变量地址 ${args}
95
+ headers: # 请求头 # todo: 支持变量
96
+ Content-Type: application/json
97
+ User-Agent: Chrome$${browser_version}
98
+ authType: SIGN # 鉴权方式 SIGN COOKIE # todo AUTH bear xxx
99
+ method: GET # GET POST
100
+ requestType: JSON # JSON FORM FORM-DATA FORM-FILE FROM-MODEL JSON-TEXT # todo PARAMS
101
+ stream_check: true # 启用流式检查 默认为false或者不写 启用流式检查后,会将response中的answer字段进行拼接,返回完整的answer字段。
102
+ parameter: # 请求参数 '#{}'取FAKER模拟数据方法 '${}' 取缓存变量 # todo '$${}' 取global变量 todo 方法传参数
103
+ offset: 0
104
+ limit: '#{get_sk_password}' # 获取环境变量里 SK 加密密码
105
+ cno: '#{random_number(args)}' # 生成随机数
106
+ name: '[${queues}]'
107
+ names: '$${names}'
108
+ file: '..\..\..\data\files\知识图谱导出文件-物价编码.xlsx' # 该字段填写 上传文件的相对路径 配合form-data使用
109
+ MIME: 'image/gif' # 该字段填写 文件的MIME类型 配合form-data使用
110
+ model: # 该字段 工单模块 使用多。上传文件使用
111
+ "operator": 1223
112
+ "content": "213213"
113
+ "type": 1
114
+ filename: file # 该字段仅限于 FORM-FILE FROM-MODEL 使用 值为 文件入参字段
115
+ assertFail: stop、continue # 断言失败处理模式 stop程序直接终止 continue断言失败仍运行。默认为stop
116
+ assert: # 断言 eq sge # todo 支持多个断言 eq ne gt lt ge le seq sne sgt slt sge sle in
117
+ - status_code: 201
118
+ - eq: [ '$.pageSize', '${size}' ]
119
+ - eq: [ '$.pageSize', '$${size}' ]
120
+ - sge: [ '$.queues[:2].id', 2 ]
121
+ - none : ['$.error'] # 判断 返回值 是否为空
122
+ - nn: ['$.error'] # 判断 返回值 是否不为空
123
+ - in: ['$.pageSize', '$${size}'] # 判断 返回值 是否包含 预期
124
+ - not_found: [ '$.pageSize', ] # 判断 指定数据是否不存在
125
+ - len: [ '$.pageSize', 4 ] # 判断 指定数据的长度
126
+ saveData: # 缓存变量 global变量 #todo cookie变量 header变量 request变量
127
+ - json: [ 'queues:str','$.queues[:2].id'] #:str 指定获取的值 type为str,暂不支持其他类型,有需要可提出优化。
128
+ - json: [ 'names','$.name' ,'global']
129
+
130
+
131
+ - name: 创建座席
132
+ id: create_client
133
+ api: ```````
134
+ ```
@@ -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