pytest-dsl 0.15.3__py3-none-any.whl → 0.15.5__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,333 @@
1
+ """
2
+ 远程服务器注册模块
3
+
4
+ 提供独立的远程服务器注册功能,方便其他系统集成pytest-dsl时使用自己的变量系统。
5
+ 这个模块不依赖于YAML配置,完全通过编程方式进行服务器注册。
6
+ """
7
+
8
+ from typing import Dict, List, Optional, Any, Callable
9
+ import logging
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class RemoteServerRegistry:
15
+ """远程服务器注册器
16
+
17
+ 提供灵活的API用于注册和管理远程关键字服务器,
18
+ 支持自定义变量获取方式,方便第三方系统集成。
19
+ """
20
+
21
+ def __init__(self):
22
+ self._variable_providers = [] # 变量提供者列表
23
+ self._server_configs = [] # 服务器配置列表
24
+ self._connection_callbacks = [] # 连接成功后的回调
25
+
26
+ def add_variable_provider(self, provider: Callable[[], Dict[str, Any]]):
27
+ """添加变量提供者
28
+
29
+ 变量提供者是一个无参数的可调用对象,返回字典形式的变量。
30
+ 这允许第三方系统提供自己的变量获取逻辑。
31
+
32
+ Args:
33
+ provider: 返回变量字典的可调用对象
34
+
35
+ Examples:
36
+ >>> def my_vars():
37
+ ... return {'api_key': 'secret', 'env': 'prod'}
38
+ >>> registry.add_variable_provider(my_vars)
39
+ """
40
+ if callable(provider):
41
+ self._variable_providers.append(provider)
42
+ else:
43
+ raise ValueError("变量提供者必须是可调用对象")
44
+
45
+ def add_connection_callback(self, callback: Callable[[str, bool], None]):
46
+ """添加连接回调
47
+
48
+ 连接回调会在每次连接远程服务器后被调用,
49
+ 无论连接成功还是失败。
50
+
51
+ Args:
52
+ callback: 接受(alias, success)参数的回调函数
53
+ """
54
+ if callable(callback):
55
+ self._connection_callbacks.append(callback)
56
+ else:
57
+ raise ValueError("连接回调必须是可调用对象")
58
+
59
+ def register_server(self,
60
+ url: str,
61
+ alias: str,
62
+ api_key: Optional[str] = None,
63
+ sync_global_vars: bool = True,
64
+ sync_custom_vars: bool = True,
65
+ exclude_patterns: Optional[List[str]] = None) -> bool:
66
+ """注册单个远程服务器
67
+
68
+ Args:
69
+ url: 服务器URL
70
+ alias: 服务器别名
71
+ api_key: API密钥
72
+ sync_global_vars: 是否同步全局变量
73
+ sync_custom_vars: 是否同步自定义变量(通过变量提供者)
74
+ exclude_patterns: 要排除的变量名模式列表
75
+
76
+ Returns:
77
+ bool: 是否连接成功
78
+ """
79
+ # 构建同步配置
80
+ sync_config = {
81
+ 'sync_global_vars': sync_global_vars,
82
+ 'sync_yaml_vars': False, # 不使用YAML变量
83
+ 'sync_custom_vars': sync_custom_vars,
84
+ 'exclude_patterns': exclude_patterns or ['password', 'secret', 'token']
85
+ }
86
+
87
+ # 收集要同步的变量
88
+ variables_to_sync = {}
89
+
90
+ if sync_custom_vars:
91
+ variables_to_sync.update(self._collect_custom_variables())
92
+
93
+ # 尝试连接
94
+ success = self._connect_to_server(
95
+ url, alias, api_key, sync_config, variables_to_sync)
96
+
97
+ # 调用连接回调
98
+ for callback in self._connection_callbacks:
99
+ try:
100
+ callback(alias, success)
101
+ except Exception as e:
102
+ logger.warning(f"连接回调执行失败: {e}")
103
+
104
+ if success:
105
+ # 保存配置以便后续使用
106
+ self._server_configs.append({
107
+ 'url': url,
108
+ 'alias': alias,
109
+ 'api_key': api_key,
110
+ 'sync_config': sync_config
111
+ })
112
+
113
+ return success
114
+
115
+ def register_servers_from_config(self, servers: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]:
116
+ """从配置列表批量注册服务器
117
+
118
+ Args:
119
+ servers: 服务器配置列表,每个配置包含url、alias等字段
120
+
121
+ Returns:
122
+ dict: 注册结果,键为alias,值为结果详情字典
123
+
124
+ Examples:
125
+ >>> servers = [
126
+ ... {'url': 'http://server1:8270', 'alias': 'server1'},
127
+ ... {'url': 'http://server2:8270', 'alias': 'server2', 'api_key': 'secret'}
128
+ ... ]
129
+ >>> results = registry.register_servers_from_config(servers)
130
+ """
131
+ results = {}
132
+
133
+ for server_config in servers:
134
+ if not isinstance(server_config, dict):
135
+ continue
136
+
137
+ url = server_config.get('url')
138
+ alias = server_config.get('alias')
139
+
140
+ if not url or not alias:
141
+ logger.warning(f"服务器配置缺少必要字段: {server_config}")
142
+ results[alias or 'unknown'] = {
143
+ 'success': False,
144
+ 'url': url or 'unknown',
145
+ 'alias': alias or 'unknown',
146
+ 'error': '缺少必要字段'
147
+ }
148
+ continue
149
+
150
+ api_key = server_config.get('api_key')
151
+ sync_global_vars = server_config.get('sync_global_vars', True)
152
+ sync_custom_vars = server_config.get('sync_custom_vars', True)
153
+ exclude_patterns = server_config.get('exclude_patterns')
154
+
155
+ success = self.register_server(
156
+ url=url,
157
+ alias=alias,
158
+ api_key=api_key,
159
+ sync_global_vars=sync_global_vars,
160
+ sync_custom_vars=sync_custom_vars,
161
+ exclude_patterns=exclude_patterns
162
+ )
163
+
164
+ results[alias] = {
165
+ 'success': success,
166
+ 'url': url,
167
+ 'alias': alias
168
+ }
169
+
170
+ return results
171
+
172
+ def _collect_custom_variables(self) -> Dict[str, Any]:
173
+ """收集自定义变量"""
174
+ variables = {}
175
+
176
+ for provider in self._variable_providers:
177
+ try:
178
+ provider_vars = provider()
179
+ if isinstance(provider_vars, dict):
180
+ variables.update(provider_vars)
181
+ else:
182
+ logger.warning(f"变量提供者返回了非字典类型: {type(provider_vars)}")
183
+ except Exception as e:
184
+ logger.warning(f"变量提供者执行失败: {e}")
185
+
186
+ return variables
187
+
188
+ def _connect_to_server(self,
189
+ url: str,
190
+ alias: str,
191
+ api_key: Optional[str],
192
+ sync_config: Dict[str, Any],
193
+ variables: Dict[str, Any]) -> bool:
194
+ """连接到远程服务器"""
195
+ try:
196
+ # 导入远程关键字管理器
197
+ from pytest_dsl.remote import remote_keyword_manager
198
+
199
+ # 创建扩展的同步配置
200
+ extended_sync_config = sync_config.copy()
201
+ extended_sync_config['custom_variables'] = variables
202
+
203
+ # 注册服务器
204
+ success = remote_keyword_manager.register_remote_server(
205
+ url=url,
206
+ alias=alias,
207
+ api_key=api_key,
208
+ sync_config=extended_sync_config
209
+ )
210
+
211
+ if success:
212
+ logger.info(f"成功连接到远程服务器: {alias} ({url})")
213
+ else:
214
+ logger.error(f"连接远程服务器失败: {alias} ({url})")
215
+
216
+ return success
217
+
218
+ except ImportError:
219
+ logger.error("远程功能不可用,请检查依赖安装")
220
+ return False
221
+ except Exception as e:
222
+ logger.error(f"连接远程服务器时发生错误: {e}")
223
+ return False
224
+
225
+ def get_registered_servers(self) -> List[Dict[str, Any]]:
226
+ """获取已注册的服务器列表"""
227
+ return self._server_configs.copy()
228
+
229
+ def clear_variable_providers(self):
230
+ """清空所有变量提供者"""
231
+ self._variable_providers.clear()
232
+
233
+ def clear_connection_callbacks(self):
234
+ """清空所有连接回调"""
235
+ self._connection_callbacks.clear()
236
+
237
+
238
+ # 创建全局注册器实例
239
+ remote_server_registry = RemoteServerRegistry()
240
+
241
+
242
+ # 便捷函数
243
+ def register_remote_server_with_variables(url: str,
244
+ alias: str,
245
+ variables: Dict[str, Any],
246
+ api_key: Optional[str] = None) -> bool:
247
+ """使用指定变量注册远程服务器的便捷函数
248
+
249
+ Args:
250
+ url: 服务器URL
251
+ alias: 服务器别名
252
+ variables: 要同步的变量字典
253
+ api_key: API密钥
254
+
255
+ Returns:
256
+ bool: 是否连接成功
257
+ """
258
+ # 创建临时变量提供者
259
+ def temp_provider():
260
+ return variables
261
+
262
+ # 临时添加变量提供者
263
+ original_providers = remote_server_registry._variable_providers.copy()
264
+ remote_server_registry._variable_providers = [temp_provider]
265
+
266
+ try:
267
+ return remote_server_registry.register_server(url, alias, api_key)
268
+ finally:
269
+ # 恢复原来的变量提供者
270
+ remote_server_registry._variable_providers = original_providers
271
+
272
+
273
+ def create_database_variable_provider(connection_string: str):
274
+ """创建数据库变量提供者示例
275
+
276
+ 这是一个示例函数,展示如何创建从数据库获取变量的提供者。
277
+ 实际使用时需要根据具体的数据库类型进行调整。
278
+
279
+ Args:
280
+ connection_string: 数据库连接字符串
281
+
282
+ Returns:
283
+ callable: 变量提供者函数
284
+ """
285
+ def database_provider():
286
+ # 这里是示例代码,实际需要根据数据库类型实现
287
+ # import sqlite3
288
+ # conn = sqlite3.connect(connection_string)
289
+ # cursor = conn.cursor()
290
+ # cursor.execute("SELECT key, value FROM variables")
291
+ # variables = dict(cursor.fetchall())
292
+ # conn.close()
293
+ # return variables
294
+
295
+ return {
296
+ 'db_host': 'localhost',
297
+ 'db_port': '5432',
298
+ 'db_name': 'test_db'
299
+ }
300
+
301
+ return database_provider
302
+
303
+
304
+ def create_config_file_variable_provider(config_file_path: str):
305
+ """创建配置文件变量提供者
306
+
307
+ 从JSON或其他配置文件读取变量。
308
+
309
+ Args:
310
+ config_file_path: 配置文件路径
311
+
312
+ Returns:
313
+ callable: 变量提供者函数
314
+ """
315
+ import json
316
+ import os
317
+
318
+ def config_file_provider():
319
+ if not os.path.exists(config_file_path):
320
+ return {}
321
+
322
+ try:
323
+ with open(config_file_path, 'r', encoding='utf-8') as f:
324
+ if config_file_path.endswith('.json'):
325
+ return json.load(f)
326
+ else:
327
+ # 可以扩展支持其他格式
328
+ return {}
329
+ except Exception as e:
330
+ logger.warning(f"读取配置文件失败: {e}")
331
+ return {}
332
+
333
+ return config_file_provider
@@ -0,0 +1,231 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 序列化工具模块
5
+
6
+ 提供统一的XML-RPC序列化检查和转换功能,避免代码重复。
7
+ """
8
+
9
+ import datetime
10
+ from typing import Any, Dict, List, Optional
11
+
12
+
13
+ class XMLRPCSerializer:
14
+ """XML-RPC序列化工具类
15
+
16
+ 统一处理XML-RPC序列化检查、转换和过滤逻辑,
17
+ 避免在多个类中重复实现相同的序列化代码。
18
+ """
19
+
20
+ # 敏感信息过滤模式
21
+ DEFAULT_EXCLUDE_PATTERNS = [
22
+ 'password', 'secret', 'token', 'credential', 'auth', 'private'
23
+ ]
24
+
25
+ @staticmethod
26
+ def is_serializable(value: Any) -> bool:
27
+ """检查值是否可以被XML-RPC序列化
28
+
29
+ XML-RPC支持的类型:
30
+ - None (需要allow_none=True)
31
+ - bool, int, float, str, bytes
32
+ - datetime.datetime
33
+ - list (元素也必须可序列化)
34
+ - dict (键必须是字符串,值必须可序列化)
35
+
36
+ Args:
37
+ value: 要检查的值
38
+
39
+ Returns:
40
+ bool: 是否可序列化
41
+ """
42
+ # 基本类型
43
+ if value is None:
44
+ return True
45
+ if isinstance(value, (bool, int, float, str, bytes)):
46
+ return True
47
+ if isinstance(value, datetime.datetime):
48
+ return True
49
+
50
+ # 严格检查:只允许内置的list和dict类型,不允许自定义类
51
+ value_type = type(value)
52
+
53
+ # 检查是否为内置list类型(不是子类)
54
+ if value_type is list:
55
+ try:
56
+ for item in value:
57
+ if not XMLRPCSerializer.is_serializable(item):
58
+ return False
59
+ return True
60
+ except Exception:
61
+ return False
62
+
63
+ # 检查是否为内置tuple类型
64
+ if value_type is tuple:
65
+ try:
66
+ for item in value:
67
+ if not XMLRPCSerializer.is_serializable(item):
68
+ return False
69
+ return True
70
+ except Exception:
71
+ return False
72
+
73
+ # 检查是否为内置dict类型(不是子类,如DotAccessDict)
74
+ if value_type is dict:
75
+ try:
76
+ for k, v in value.items():
77
+ # XML-RPC要求字典的键必须是字符串
78
+ if not isinstance(k, str):
79
+ return False
80
+ if not XMLRPCSerializer.is_serializable(v):
81
+ return False
82
+ return True
83
+ except Exception:
84
+ return False
85
+
86
+ # 其他类型都不可序列化
87
+ return False
88
+
89
+ @staticmethod
90
+ def convert_to_serializable(value: Any) -> Optional[Any]:
91
+ """尝试将值转换为XML-RPC可序列化的格式
92
+
93
+ Args:
94
+ value: 要转换的值
95
+
96
+ Returns:
97
+ 转换后的值,如果无法转换则返回None
98
+ """
99
+ # 如果已经可序列化,直接返回
100
+ if XMLRPCSerializer.is_serializable(value):
101
+ return value
102
+
103
+ # 尝试转换类字典对象为标准字典
104
+ if hasattr(value, 'keys') and hasattr(value, 'items'):
105
+ try:
106
+ converted_dict = {}
107
+ for k, v in value.items():
108
+ # 键必须是字符串
109
+ if not isinstance(k, str):
110
+ k = str(k)
111
+
112
+ # 递归转换值
113
+ converted_value = XMLRPCSerializer.convert_to_serializable(v)
114
+ if converted_value is not None or v is None:
115
+ converted_dict[k] = converted_value
116
+ else:
117
+ # 如果无法转换子值,跳过这个键值对
118
+ print(f"跳过无法转换的字典项: {k} "
119
+ f"(类型: {type(v).__name__})")
120
+ continue
121
+
122
+ return converted_dict
123
+ except Exception as e:
124
+ print(f"转换类字典对象失败: {e}")
125
+ return None
126
+
127
+ # 尝试转换类列表对象为标准列表
128
+ if hasattr(value, '__iter__') and not isinstance(value, (str, bytes)):
129
+ try:
130
+ converted_list = []
131
+ for item in value:
132
+ converted_item = XMLRPCSerializer.convert_to_serializable(item)
133
+ if converted_item is not None or item is None:
134
+ converted_list.append(converted_item)
135
+ else:
136
+ # 如果无法转换子项,跳过
137
+ print(f"跳过无法转换的列表项: "
138
+ f"(类型: {type(item).__name__})")
139
+ continue
140
+
141
+ return converted_list
142
+ except Exception as e:
143
+ print(f"转换类列表对象失败: {e}")
144
+ return None
145
+
146
+ # 尝试转换为字符串表示
147
+ try:
148
+ str_value = str(value)
149
+ # 避免转换过长的字符串或包含敏感信息的对象
150
+ if (len(str_value) < 1000 and
151
+ not any(pattern in str_value.lower()
152
+ for pattern in XMLRPCSerializer.DEFAULT_EXCLUDE_PATTERNS)):
153
+ return str_value
154
+ except Exception:
155
+ pass
156
+
157
+ # 无法转换
158
+ return None
159
+
160
+ @staticmethod
161
+ def filter_variables(variables: Dict[str, Any],
162
+ exclude_patterns: Optional[List[str]] = None) -> Dict[str, Any]:
163
+ """过滤变量字典,移除敏感变量和不可序列化的变量
164
+
165
+ Args:
166
+ variables: 原始变量字典
167
+ exclude_patterns: 排除模式列表,如果为None则使用默认模式
168
+
169
+ Returns:
170
+ Dict[str, Any]: 过滤后的变量字典
171
+ """
172
+ if exclude_patterns is None:
173
+ exclude_patterns = XMLRPCSerializer.DEFAULT_EXCLUDE_PATTERNS
174
+
175
+ filtered_variables = {}
176
+
177
+ for var_name, var_value in variables.items():
178
+ # 检查是否需要排除
179
+ should_exclude = False
180
+ var_name_lower = var_name.lower()
181
+
182
+ # 检查变量名
183
+ for pattern in exclude_patterns:
184
+ if pattern.lower() in var_name_lower:
185
+ should_exclude = True
186
+ break
187
+
188
+ # 如果值是字符串,也检查是否包含敏感信息
189
+ if not should_exclude and isinstance(var_value, str):
190
+ value_lower = var_value.lower()
191
+ for pattern in exclude_patterns:
192
+ if (pattern.lower() in value_lower and
193
+ len(var_value) < 100): # 只检查短字符串
194
+ should_exclude = True
195
+ break
196
+
197
+ if not should_exclude:
198
+ # 尝试转换为可序列化的格式
199
+ serializable_value = XMLRPCSerializer.convert_to_serializable(var_value)
200
+ # 注意:None值转换后仍然是None,但这是有效的结果
201
+ if serializable_value is not None or var_value is None:
202
+ filtered_variables[var_name] = serializable_value
203
+ else:
204
+ print(f"跳过不可序列化的变量: {var_name} "
205
+ f"(类型: {type(var_value).__name__})")
206
+ else:
207
+ print(f"跳过敏感变量: {var_name}")
208
+
209
+ return filtered_variables
210
+
211
+ @staticmethod
212
+ def validate_xmlrpc_data(data: Any) -> bool:
213
+ """验证数据是否可以通过XML-RPC传输
214
+
215
+ Args:
216
+ data: 要验证的数据
217
+
218
+ Returns:
219
+ bool: 是否可以传输
220
+ """
221
+ try:
222
+ import xmlrpc.client
223
+ # 尝试序列化数据
224
+ xmlrpc.client.dumps((data,), allow_none=True)
225
+ return True
226
+ except Exception:
227
+ return False
228
+
229
+
230
+ # 创建全局序列化器实例,方便直接使用
231
+ xmlrpc_serializer = XMLRPCSerializer()
pytest_dsl/core/utils.py CHANGED
@@ -69,7 +69,8 @@ def _legacy_replace_variables_in_string(value: str) -> str:
69
69
  break
70
70
 
71
71
  # 替换变量引用
72
- value = value[:match.start()] + str(var_value) + value[match.end():]
72
+ value = value[:match.start()] + str(var_value) + \
73
+ value[match.end():]
73
74
 
74
75
  # 再处理基本引用
75
76
  matches = list(re.finditer(basic_pattern, value))
@@ -77,7 +78,8 @@ def _legacy_replace_variables_in_string(value: str) -> str:
77
78
  var_name = match.group(1)
78
79
  if context_has_variable(var_name):
79
80
  var_value = get_variable(var_name)
80
- value = value[:match.start()] + str(var_value) + value[match.end():]
81
+ value = value[:match.start()] + str(var_value) + \
82
+ value[match.end():]
81
83
 
82
84
  return value
83
85
 
@@ -102,37 +104,42 @@ def replace_variables_in_dict(data: Union[Dict, List, str]) -> Union[Dict, List,
102
104
 
103
105
 
104
106
  def context_has_variable(var_name: str) -> bool:
105
- """检查变量是否存在于全局上下文"""
106
- # 先检查YAML变量
107
- from pytest_dsl.core.yaml_vars import yaml_vars
108
- if yaml_vars.get_variable(var_name) is not None:
109
- return True
107
+ """检查变量是否存在于上下文中
110
108
 
109
+ 检查顺序:
110
+ 1. 测试上下文
111
+ 2. 全局上下文(包含YAML变量)
112
+ """
111
113
  # 检查测试上下文
112
- from pytest_dsl.core.keyword_manager import keyword_manager
113
- current_context = getattr(keyword_manager, 'current_context', None)
114
- if current_context and current_context.has(var_name):
115
- return True
116
-
117
- # 再检查全局上下文
114
+ try:
115
+ from pytest_dsl.core.keyword_manager import keyword_manager
116
+ current_context = getattr(keyword_manager, 'current_context', None)
117
+ if current_context and current_context.has(var_name):
118
+ return True
119
+ except ImportError:
120
+ pass
121
+
122
+ # 检查全局上下文(包含YAML变量的统一访问)
118
123
  return global_context.has_variable(var_name)
119
124
 
120
125
 
121
126
  def get_variable(var_name: str) -> Any:
122
- """获取变量值,先从YAML变量中获取,再从全局上下文获取"""
123
- # 先从YAML变量中获取
124
- from pytest_dsl.core.yaml_vars import yaml_vars
125
- yaml_value = yaml_vars.get_variable(var_name)
126
- if yaml_value is not None:
127
- return yaml_value
127
+ """获取变量值
128
128
 
129
- # 检查测试上下文
130
- from pytest_dsl.core.keyword_manager import keyword_manager
131
- current_context = getattr(keyword_manager, 'current_context', None)
132
- if current_context and current_context.has(var_name):
133
- return current_context.get(var_name)
134
-
135
- # 再从全局上下文获取
129
+ 获取顺序:
130
+ 1. 测试上下文
131
+ 2. 全局上下文(包含YAML变量)
132
+ """
133
+ # 先从测试上下文获取
134
+ try:
135
+ from pytest_dsl.core.keyword_manager import keyword_manager
136
+ current_context = getattr(keyword_manager, 'current_context', None)
137
+ if current_context and current_context.has(var_name):
138
+ return current_context.get(var_name)
139
+ except ImportError:
140
+ pass
141
+
142
+ # 再从全局上下文获取(包含对YAML变量的统一访问)
136
143
  if global_context.has_variable(var_name):
137
144
  return global_context.get_variable(var_name)
138
145
 
@@ -163,4 +170,4 @@ def deep_merge(base: Dict, override: Dict) -> Dict:
163
170
  # 覆盖或添加值
164
171
  result[key] = value
165
172
 
166
- return result
173
+ return result