CloudApiRequest 1.2.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.
cloud/CloudSignUtil.py ADDED
@@ -0,0 +1,152 @@
1
+ import hashlib
2
+ import time
3
+
4
+ from cloud.CloudLogUtil import log
5
+
6
+
7
+ class CloudSignUtil:
8
+ """
9
+ cloud签名工具类 - 基于MD5的签名算法
10
+ 鉴权方式:MD5({enterpriseId}+{timestamp}+{token})
11
+ """
12
+
13
+ @staticmethod
14
+ def _generate_md5_hash(sign_string):
15
+ """
16
+ 生成MD5签名的内部方法
17
+ :param sign_string: 签名字符串
18
+ :return: 32位小写MD5签名
19
+ """
20
+ return hashlib.md5(sign_string.encode('utf-8')).hexdigest()
21
+
22
+ @staticmethod
23
+ def generate_md5_signature(enterprise_id, timestamp, token):
24
+ """
25
+ 生成MD5签名(按企业编号)
26
+ :param enterprise_id: 企业ID
27
+ :param timestamp: 时间戳
28
+ :param token: 访问token
29
+ :return: 32位小写MD5签名
30
+ """
31
+ # 构建签名字符串:{enterpriseId}+{timestamp}+{token}
32
+ sign_string = f"{enterprise_id}{timestamp}{token}"
33
+ return CloudSignUtil._generate_md5_hash(sign_string)
34
+
35
+ @staticmethod
36
+ def generate_md5_signature_by_validate_type(validate_type, enterprise_id, department_id, timestamp, token):
37
+ """
38
+ 根据验证类型生成MD5签名
39
+ :param validate_type: 验证类型 (1=部门编号, 2=企业编号)
40
+ :param enterprise_id: 企业ID
41
+ :param department_id: 部门编号
42
+ :param timestamp: 时间戳
43
+ :param token: 访问token
44
+ :return: 32位小写MD5签名
45
+ """
46
+ if validate_type == 1:
47
+ # validateType=1时,sign=MD5({departmentId}+{timestamp}+{部门token值})
48
+ sign_string = f"{department_id}{timestamp}{token}"
49
+ else:
50
+ # validateType=2时,sign=MD5({enterpriseId}+{timestamp}+{企业token值})
51
+ sign_string = f"{enterprise_id}{timestamp}{token}"
52
+
53
+ signature = CloudSignUtil._generate_md5_hash(sign_string)
54
+ log.info(f"签名计算: validateType={validate_type}, sign_string={sign_string}, signature={signature}")
55
+
56
+ return signature
57
+
58
+ @staticmethod
59
+ def generate_headers(method, url, params, enterprise_id, token, body=None):
60
+ """
61
+ 生成云服务请求头 - 基于MD5签名
62
+ :param method: 请求方法
63
+ :param url: 请求URL
64
+ :param params: 查询参数
65
+ :param enterprise_id: 企业ID
66
+ :param token: 访问token
67
+ :param body: 请求体(用于POST/PUT请求)
68
+ :return: 请求头字典
69
+ """
70
+ # 获取当前时间戳(秒级)
71
+ timestamp = int(time.time())
72
+
73
+ # 生成MD5签名
74
+ signature = CloudSignUtil.generate_md5_signature(enterprise_id, timestamp, token)
75
+
76
+ # 构建请求头
77
+ headers = {
78
+ 'Authorization': f"Cloud {token}:{signature}",
79
+ 'X-Timestamp': str(timestamp),
80
+ 'Content-Type': 'application/json',
81
+ 'User-Agent': 'CloudAPISDK/1.0'
82
+ }
83
+
84
+ # 添加企业ID到请求头(如果需要)
85
+ headers['X-Enterprise-Id'] = str(enterprise_id)
86
+
87
+ return headers
88
+
89
+ @staticmethod
90
+ def generate_params_with_signature(enterprise_id, token, additional_params=None):
91
+ """
92
+ 生成带签名的请求参数
93
+ :param enterprise_id: 企业ID
94
+ :param token: 访问token
95
+ :param additional_params: 额外参数
96
+ :return: 包含签名的参数字典
97
+ """
98
+ # 获取当前时间戳(秒级)
99
+ timestamp = int(time.time())
100
+
101
+ # 检查验证类型
102
+ validate_type = 2 # 默认按企业编号验证
103
+ department_id = None
104
+
105
+ if additional_params:
106
+ validate_type = int(additional_params.get('validateType', 2))
107
+ department_id = additional_params.get('departmentId')
108
+
109
+ # 根据验证类型生成签名
110
+ if validate_type == 1 and department_id:
111
+ # 按部门编号验证
112
+ signature = CloudSignUtil.generate_md5_signature_by_validate_type(
113
+ validate_type, enterprise_id, department_id, timestamp, token
114
+ )
115
+ else:
116
+ # 按企业编号验证
117
+ signature = CloudSignUtil.generate_md5_signature(enterprise_id, timestamp, token)
118
+
119
+ # 构建基础参数
120
+ params = {
121
+ 'enterpriseId': enterprise_id,
122
+ 'timestamp': timestamp,
123
+ 'sign': signature
124
+ }
125
+
126
+ # 添加额外参数
127
+ if additional_params:
128
+ params.update(additional_params)
129
+
130
+ return params
131
+
132
+ @staticmethod
133
+ def validate_signature(enterprise_id, timestamp, token, received_signature):
134
+ """
135
+ 验证签名
136
+ :param enterprise_id: 企业ID
137
+ :param timestamp: 时间戳
138
+ :param token: 访问token
139
+ :param received_signature: 接收到的签名
140
+ :return: 验证结果
141
+ """
142
+ # 生成期望的签名
143
+ expected_signature = CloudSignUtil.generate_md5_signature(enterprise_id, timestamp, token)
144
+
145
+ # 比较签名
146
+ is_valid = expected_signature == received_signature
147
+
148
+ log.info(f"签名验证结果: {is_valid}")
149
+ log.info(f"期望签名: {expected_signature}")
150
+ log.info(f"接收签名: {received_signature}")
151
+
152
+ return is_valid
cloud/CloudYamlUtil.py ADDED
@@ -0,0 +1,224 @@
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
+ # 先将单引号替换为双引号,None替换为null
148
+ temp_str = self.replaced_str.replace("'", "\"").replace('None', 'null')
149
+
150
+ # 在解析之前,检测并保护JSON字符串参数
151
+ # 查找模式:"key": "{...}" 或 "key": "[...]"
152
+ # 将内部的引号转义,使其成为合法的JSON字符串
153
+ def escape_json_string_values(match):
154
+ key = match.group(1)
155
+ json_str = match.group(2)
156
+ # 转义内部的双引号
157
+ escaped_json_str = json_str.replace('"', '\\"')
158
+ return f'"{key}": "{escaped_json_str}"'
159
+
160
+ # 匹配 "key": "{...}" 或 "key": "[...]" 的模式
161
+ # 使用非贪婪匹配,并且只匹配value部分被双引号包裹的情况
162
+ # .*? 可以匹配包含双引号的内容
163
+ pattern = r'"(\w+)"\s*:\s*"(\{.*?\}|\[.*?\])"'
164
+ temp_str = re.sub(pattern, escape_json_string_values, temp_str)
165
+ log.info(f"转义后的字符串: {temp_str[:300]}")
166
+
167
+ try:
168
+ # 尝试直接解析
169
+ parsed_result = json.loads(temp_str)
170
+ log.info("替换后jsonLoad的str内容为:{}".format(parsed_result))
171
+ return parsed_result
172
+ except json.JSONDecodeError as e:
173
+ # 如果直接解析失败,尝试旧的替换逻辑(兼容旧用例)
174
+ log.warning(f"JSON解析失败: {e}, 尝试旧的替换逻辑")
175
+ try:
176
+ replaced_str = self.replaced_str.replace('"[', '[').replace(']"', ']').replace('"{', '{').replace('}"', '}').replace("'", "\"").replace('None','null')
177
+ replaced_str = json.loads(replaced_str)
178
+ log.info("替换后jsonLoad的str内容为:{}".format(replaced_str))
179
+ return replaced_str
180
+ except:
181
+ log.info(f'*************替换失败-YAML,请检查格式{replaced_str}******************')
182
+
183
+ def textLoad(self):
184
+ return json.loads(self.replaced_str)
185
+
186
+
187
+ class CloudReadYaml:
188
+ """
189
+ 用于读取yaml文件的工具类
190
+ """
191
+
192
+ def __init__(self, yaml_file):
193
+ self.yaml_file = yaml_file
194
+
195
+ def load_yaml(self):
196
+ """
197
+ 读取yaml文件,并返回其中的数据
198
+ :return: dict
199
+ """
200
+ with open(self.yaml_file, encoding='utf-8') as f:
201
+ data = yaml.safe_load(f)
202
+ return data if data is not None else {} # 如果data为None,则返回空字典
203
+
204
+ def get(self, key, default=None):
205
+ """
206
+ 获取yaml文件中的数据
207
+ :param key: 数据的键
208
+ :param default: 如果获取失败,则返回该默认值
209
+ :return: dict
210
+ """
211
+ # 读取yaml文件
212
+ data = self.load_yaml()
213
+ # 获取数据
214
+ return data.get(key, default) if data is not None else default
215
+
216
+ def get_all(self):
217
+ """
218
+ 获取yaml文件中的所有数据
219
+ :return: dict
220
+ """
221
+ # 读取yaml文件
222
+ data = self.load_yaml()
223
+ # 如果data为None,则返回空字典
224
+ 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.2.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=J0DyxwvoW2ZeDmcD3mxRLEMt_enMHVgNANUYi0nOup4,24528
2
+ cloud/CloudApiConfig.py,sha256=s4VjgJT8cwCU2IZdZm5u5pgRxs8gp9ia5kW6P9txUWI,13027
3
+ cloud/CloudLogUtil.py,sha256=EXnU6QXFxl_npCcJgQvhtlG8uR57NmbOovFjVMHPOpQ,935
4
+ cloud/CloudRequestUtil.py,sha256=bsbI74p1iu4weXbDKqwhXvH1KwezvxYXdrXBMc9L3sE,4827
5
+ cloud/CloudSignUtil.py,sha256=dyUESxWYgcvTt-nOnI3mEboVispEF0cl_vX-1d3kUOU,5493
6
+ cloud/CloudYamlUtil.py,sha256=-WoOBSVsTQG9nbeEPocwh4eFV0Iilj1IcqXGYw5DYxs,9787
7
+ cloud/__init__.py,sha256=RgNAN_vKhCHnwcBy0hc1ttrr5x58bO_u6TNICZDSfFw,306
8
+ cloud/cli.py,sha256=VId1s29djJu7QrkXJc2je4fRV030vqD3nwuI23AN8Ig,1986
9
+ cloudapirequest-1.2.0.dist-info/LICENSE,sha256=hMjjUYpALQY0E8hzysmZszKwYy2mt8tUvAFQfIIbwV4,1099
10
+ cloudapirequest-1.2.0.dist-info/METADATA,sha256=pc5iBoOAtKXd2kRJeKZqISUUvcGYuXTs1yj06T0LwTQ,608
11
+ cloudapirequest-1.2.0.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
12
+ cloudapirequest-1.2.0.dist-info/top_level.txt,sha256=lBiHHi76sKOBY-DmE0iDypxZxv-TTCnvHeiFleC_PuA,6
13
+ cloudapirequest-1.2.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