pytest-dsl 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. pytest_dsl/__init__.py +10 -0
  2. pytest_dsl/cli.py +44 -0
  3. pytest_dsl/conftest_adapter.py +4 -0
  4. pytest_dsl/core/__init__.py +0 -0
  5. pytest_dsl/core/auth_provider.py +409 -0
  6. pytest_dsl/core/auto_decorator.py +181 -0
  7. pytest_dsl/core/auto_directory.py +81 -0
  8. pytest_dsl/core/context.py +23 -0
  9. pytest_dsl/core/custom_auth_example.py +425 -0
  10. pytest_dsl/core/dsl_executor.py +329 -0
  11. pytest_dsl/core/dsl_executor_utils.py +84 -0
  12. pytest_dsl/core/global_context.py +103 -0
  13. pytest_dsl/core/http_client.py +411 -0
  14. pytest_dsl/core/http_request.py +810 -0
  15. pytest_dsl/core/keyword_manager.py +109 -0
  16. pytest_dsl/core/lexer.py +139 -0
  17. pytest_dsl/core/parser.py +197 -0
  18. pytest_dsl/core/parsetab.py +76 -0
  19. pytest_dsl/core/plugin_discovery.py +187 -0
  20. pytest_dsl/core/utils.py +146 -0
  21. pytest_dsl/core/variable_utils.py +267 -0
  22. pytest_dsl/core/yaml_loader.py +62 -0
  23. pytest_dsl/core/yaml_vars.py +75 -0
  24. pytest_dsl/docs/custom_keywords.md +140 -0
  25. pytest_dsl/examples/__init__.py +5 -0
  26. pytest_dsl/examples/assert/assertion_example.auto +44 -0
  27. pytest_dsl/examples/assert/boolean_test.auto +34 -0
  28. pytest_dsl/examples/assert/expression_test.auto +49 -0
  29. pytest_dsl/examples/http/__init__.py +3 -0
  30. pytest_dsl/examples/http/builtin_auth_test.auto +79 -0
  31. pytest_dsl/examples/http/csrf_auth_test.auto +64 -0
  32. pytest_dsl/examples/http/custom_auth_test.auto +76 -0
  33. pytest_dsl/examples/http/file_reference_test.auto +111 -0
  34. pytest_dsl/examples/http/http_advanced.auto +91 -0
  35. pytest_dsl/examples/http/http_example.auto +147 -0
  36. pytest_dsl/examples/http/http_length_test.auto +55 -0
  37. pytest_dsl/examples/http/http_retry_assertions.auto +91 -0
  38. pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +94 -0
  39. pytest_dsl/examples/http/http_with_yaml.auto +58 -0
  40. pytest_dsl/examples/http/new_retry_test.auto +22 -0
  41. pytest_dsl/examples/http/retry_assertions_only.auto +52 -0
  42. pytest_dsl/examples/http/retry_config_only.auto +49 -0
  43. pytest_dsl/examples/http/retry_debug.auto +22 -0
  44. pytest_dsl/examples/http/retry_with_fix.auto +21 -0
  45. pytest_dsl/examples/http/simple_retry.auto +20 -0
  46. pytest_dsl/examples/http/vars.yaml +55 -0
  47. pytest_dsl/examples/http_clients.yaml +48 -0
  48. pytest_dsl/examples/keyword_example.py +70 -0
  49. pytest_dsl/examples/test_assert.py +16 -0
  50. pytest_dsl/examples/test_http.py +168 -0
  51. pytest_dsl/keywords/__init__.py +10 -0
  52. pytest_dsl/keywords/assertion_keywords.py +610 -0
  53. pytest_dsl/keywords/global_keywords.py +51 -0
  54. pytest_dsl/keywords/http_keywords.py +430 -0
  55. pytest_dsl/keywords/system_keywords.py +17 -0
  56. pytest_dsl/main_adapter.py +7 -0
  57. pytest_dsl/plugin.py +44 -0
  58. pytest_dsl-0.1.0.dist-info/METADATA +537 -0
  59. pytest_dsl-0.1.0.dist-info/RECORD +63 -0
  60. pytest_dsl-0.1.0.dist-info/WHEEL +5 -0
  61. pytest_dsl-0.1.0.dist-info/entry_points.txt +5 -0
  62. pytest_dsl-0.1.0.dist-info/licenses/LICENSE +21 -0
  63. pytest_dsl-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,411 @@
1
+ import os
2
+ import json
3
+ import time
4
+ import logging
5
+ from typing import Dict, List, Any, Optional, Union, Tuple
6
+ import requests
7
+ from requests.exceptions import RequestException
8
+ from urllib.parse import urljoin
9
+ from pytest_dsl.core.yaml_vars import yaml_vars
10
+ from pytest_dsl.core.auth_provider import AuthProvider, create_auth_provider
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class HTTPClient:
16
+ """HTTP客户端类
17
+
18
+ 负责管理HTTP请求会话和发送请求
19
+ """
20
+
21
+ def __init__(self,
22
+ name: str = "default",
23
+ base_url: str = "",
24
+ headers: Dict[str, str] = None,
25
+ timeout: int = 30,
26
+ verify_ssl: bool = True,
27
+ session: bool = True,
28
+ retry: Dict[str, Any] = None,
29
+ proxies: Dict[str, str] = None,
30
+ auth_config: Dict[str, Any] = None):
31
+ """初始化HTTP客户端
32
+
33
+ Args:
34
+ name: 客户端名称
35
+ base_url: 基础URL
36
+ headers: 默认请求头
37
+ timeout: 默认超时时间(秒)
38
+ verify_ssl: 是否验证SSL证书
39
+ session: 是否启用会话
40
+ retry: 重试配置
41
+ proxies: 代理配置
42
+ auth_config: 认证配置
43
+ """
44
+ self.name = name
45
+ self.base_url = base_url
46
+ self.default_headers = headers or {}
47
+ self.timeout = timeout
48
+ self.verify_ssl = verify_ssl
49
+ self.use_session = session
50
+ self.retry_config = retry or {
51
+ "max_retries": 0,
52
+ "retry_interval": 1,
53
+ "retry_on_status": [500, 502, 503, 504]
54
+ }
55
+ self.proxies = proxies or {}
56
+
57
+ # 处理认证配置
58
+ self.auth_provider = None
59
+ if auth_config:
60
+ self.auth_provider = create_auth_provider(auth_config)
61
+ if not self.auth_provider:
62
+ logger.warning(f"无法创建认证提供者: {auth_config}")
63
+
64
+ # 创建会话
65
+ self._session = requests.Session() if self.use_session else None
66
+ if self.use_session and self.default_headers:
67
+ self._session.headers.update(self.default_headers)
68
+
69
+ def reset_session(self):
70
+ """完全重置会话对象,创建一个新的会话实例
71
+
72
+ 当需要彻底清除所有会话状态(例如认证信息、cookies等)时使用
73
+ """
74
+ if self.use_session:
75
+ # 关闭当前会话
76
+ if self._session:
77
+ self._session.close()
78
+
79
+ # 创建新会话
80
+ self._session = requests.Session()
81
+
82
+ # 重新应用默认头
83
+ if self.default_headers:
84
+ self._session.headers.update(self.default_headers)
85
+
86
+ logger.debug(f"会话已完全重置: {self.name}")
87
+
88
+ def make_request(self, method: str, url: str, **request_kwargs) -> requests.Response:
89
+ """发送HTTP请求
90
+
91
+ Args:
92
+ method: HTTP方法
93
+ url: 请求URL
94
+ **request_kwargs: 请求参数
95
+
96
+ Returns:
97
+ requests.Response: 响应对象
98
+ """
99
+ # 构建完整URL
100
+ if not url.startswith(('http://', 'https://')):
101
+ url = urljoin(self.base_url, url.lstrip('/'))
102
+
103
+ # 处理认证
104
+ disable_auth = request_kwargs.pop('disable_auth', False)
105
+ if disable_auth:
106
+ # 如果有认证提供者,使用其清理逻辑
107
+ if self.auth_provider:
108
+ request_kwargs = self.auth_provider.clean_auth_state(request_kwargs)
109
+ else:
110
+ # 默认清理逻辑:移除所有认证相关的头
111
+ auth_headers = [
112
+ 'Authorization', 'X-API-Key', 'X-Api-Key', 'api-key', 'Api-Key',
113
+ 'X-Csrf-Token', 'X-CSRF-Token', 'csrf-token', 'CSRF-Token', # CSRF相关头
114
+ 'X-WX-OPENID', 'X-WX-SESSION-KEY' # 微信相关认证头
115
+ ]
116
+ if 'headers' in request_kwargs:
117
+ for header in auth_headers:
118
+ request_kwargs['headers'].pop(header, None)
119
+ # 移除认证参数
120
+ request_kwargs.pop('auth', None)
121
+
122
+ # 如果使用会话,并且认证提供者没有自己的会话管理,则使用默认的会话重置
123
+ if self.use_session and not hasattr(self.auth_provider, 'manage_session'):
124
+ self.reset_session()
125
+
126
+ elif self.auth_provider and 'auth' not in request_kwargs:
127
+ # 应用认证提供者
128
+ request_kwargs = self.auth_provider.apply_auth(request_kwargs)
129
+ # 如果使用会话,更新会话头
130
+ if self.use_session and 'headers' in request_kwargs:
131
+ self._session.headers.update(request_kwargs['headers'])
132
+
133
+ # 记录请求详情
134
+ logger.debug(f"=== HTTP请求详情 ===")
135
+ logger.debug(f"方法: {method}")
136
+ logger.debug(f"URL: {url}")
137
+ if 'headers' in request_kwargs:
138
+ safe_headers = {k: '***' if k.lower() in ['authorization', 'x-api-key', 'token'] else v
139
+ for k, v in request_kwargs['headers'].items()}
140
+ logger.debug(f"请求头: {json.dumps(safe_headers, indent=2, ensure_ascii=False)}")
141
+ if 'params' in request_kwargs:
142
+ logger.debug(f"查询参数: {json.dumps(request_kwargs['params'], indent=2, ensure_ascii=False)}")
143
+ if 'json' in request_kwargs:
144
+ logger.debug(f"JSON请求体: {json.dumps(request_kwargs['json'], indent=2, ensure_ascii=False)}")
145
+ if 'data' in request_kwargs:
146
+ logger.debug(f"表单数据: {request_kwargs['data']}")
147
+
148
+ # 为超时设置默认值
149
+ if 'timeout' not in request_kwargs:
150
+ request_kwargs['timeout'] = self.timeout
151
+
152
+ # 为SSL验证设置默认值
153
+ if 'verify' not in request_kwargs:
154
+ request_kwargs['verify'] = self.verify_ssl
155
+
156
+ # 应用代理配置
157
+ if self.proxies and 'proxies' not in request_kwargs:
158
+ request_kwargs['proxies'] = self.proxies
159
+
160
+ try:
161
+ # 发送请求
162
+ if self.use_session:
163
+ if self._session is None:
164
+ logger.warning("会话对象为空,创建新会话")
165
+ self._session = requests.Session()
166
+ if self.default_headers:
167
+ self._session.headers.update(self.default_headers)
168
+ response = self._session.request(method, url, **request_kwargs)
169
+ else:
170
+ response = requests.request(method, url, **request_kwargs)
171
+
172
+ # 记录响应详情
173
+ logger.debug(f"\n=== HTTP响应详情 ===")
174
+ logger.debug(f"状态码: {response.status_code}")
175
+ logger.debug(f"响应头: {json.dumps(dict(response.headers), indent=2, ensure_ascii=False)}")
176
+ try:
177
+ if 'application/json' in response.headers.get('Content-Type', ''):
178
+ logger.debug(f"响应体 (JSON): {json.dumps(response.json(), indent=2, ensure_ascii=False)}")
179
+ else:
180
+ logger.debug(f"响应体: {response.text}")
181
+ except Exception as e:
182
+ logger.debug(f"解析响应体失败: {str(e)}")
183
+ logger.debug(f"原始响应体: {response.text}")
184
+
185
+ # 添加响应时间
186
+ if not hasattr(response, 'elapsed_ms'):
187
+ response.elapsed_ms = response.elapsed.total_seconds() * 1000
188
+
189
+ return response
190
+ except requests.exceptions.RequestException as e:
191
+ logger.error(f"HTTP请求异常: {type(e).__name__}: {str(e)}")
192
+ raise
193
+ except Exception as e:
194
+ logger.error(f"未预期的异常: {type(e).__name__}: {str(e)}")
195
+ # 将非请求异常包装为请求异常
196
+ raised_exception = requests.exceptions.RequestException(f"HTTP请求过程中发生错误: {str(e)}")
197
+ raised_exception.__cause__ = e
198
+ raise raised_exception
199
+
200
+ def _log_request(self, method: str, url: str, request_kwargs: Dict[str, Any]) -> None:
201
+ """记录请求信息
202
+
203
+ Args:
204
+ method: HTTP方法
205
+ url: 请求URL
206
+ request_kwargs: 请求参数
207
+ """
208
+ logger.info(f"发送 {method} 请求到 {url}")
209
+
210
+ # 打印请求头 (排除敏感信息)
211
+ headers = request_kwargs.get("headers", {})
212
+ safe_headers = headers.copy()
213
+
214
+ for key in headers:
215
+ if key.lower() in ["authorization", "x-api-key", "token", "api-key"]:
216
+ safe_headers[key] = "***REDACTED***"
217
+
218
+ logger.debug(f"请求头: {safe_headers}")
219
+
220
+ # 打印查询参数
221
+ if request_kwargs.get("params"):
222
+ logger.debug(f"查询参数: {request_kwargs['params']}")
223
+
224
+ # 打印请求体
225
+ if request_kwargs.get("json"):
226
+ logger.debug(f"JSON请求体: {json.dumps(request_kwargs['json'], ensure_ascii=False)}")
227
+ elif request_kwargs.get("data"):
228
+ logger.debug(f"表单数据: {request_kwargs['data']}")
229
+
230
+ # 打印文件信息
231
+ if request_kwargs.get("files"):
232
+ file_info = {k: f"<文件: {getattr(v, 'name', '未知文件')}>" for k, v in request_kwargs["files"].items()}
233
+ logger.debug(f"上传文件: {file_info}")
234
+
235
+ def _log_response(self, response: requests.Response) -> None:
236
+ """记录响应信息
237
+
238
+ Args:
239
+ response: 响应对象
240
+ """
241
+ logger.info(f"收到响应: {response.status_code} {response.reason} ({response.elapsed_ms:.2f}ms)")
242
+
243
+ # 打印响应头
244
+ logger.debug(f"响应头: {dict(response.headers)}")
245
+
246
+ # 尝试打印响应体
247
+ try:
248
+ if 'application/json' in response.headers.get('Content-Type', ''):
249
+ logger.debug(f"响应体 (JSON): {json.dumps(response.json(), ensure_ascii=False)}")
250
+ elif len(response.content) < 1024: # 只打印小响应
251
+ logger.debug(f"响应体: {response.text}")
252
+ else:
253
+ logger.debug(f"响应体: <{len(response.content)} 字节>")
254
+ except Exception as e:
255
+ logger.debug(f"无法打印响应体: {str(e)}")
256
+
257
+ def close(self) -> None:
258
+ """关闭客户端会话"""
259
+ if self._session:
260
+ self._session.close()
261
+ self._session = None
262
+
263
+
264
+ class HTTPClientManager:
265
+ """HTTP客户端管理器
266
+
267
+ 负责管理多个HTTP客户端实例和会话
268
+ """
269
+
270
+ def __init__(self):
271
+ """初始化客户端管理器"""
272
+ self._clients: Dict[str, HTTPClient] = {}
273
+ self._sessions: Dict[str, HTTPClient] = {}
274
+
275
+ def create_client(self, config: Dict[str, Any]) -> HTTPClient:
276
+ """从配置创建客户端
277
+
278
+ Args:
279
+ config: 客户端配置
280
+
281
+ Returns:
282
+ HTTPClient实例
283
+ """
284
+ name = config.get("name", "default")
285
+ client = HTTPClient(
286
+ name=name,
287
+ base_url=config.get("base_url", ""),
288
+ headers=config.get("headers", {}),
289
+ timeout=config.get("timeout", 30),
290
+ verify_ssl=config.get("verify_ssl", True),
291
+ session=config.get("session", True),
292
+ retry=config.get("retry", None),
293
+ proxies=config.get("proxies", None),
294
+ auth_config=config.get("auth", None) # 获取认证配置
295
+ )
296
+ return client
297
+
298
+ def get_client(self, name: str = "default") -> HTTPClient:
299
+ """获取或创建客户端
300
+
301
+ Args:
302
+ name: 客户端名称
303
+
304
+ Returns:
305
+ HTTPClient实例
306
+ """
307
+ # 如果客户端已存在,直接返回
308
+ if name in self._clients:
309
+ return self._clients[name]
310
+
311
+ # 从YAML变量中读取客户端配置
312
+ http_clients = yaml_vars.get_variable("http_clients") or {}
313
+ client_config = http_clients.get(name)
314
+
315
+ if not client_config:
316
+ # 如果请求的是default但配置中没有,创建一个默认客户端
317
+ if name == "default":
318
+ logger.warning("使用默认HTTP客户端配置")
319
+ client = HTTPClient(name="default")
320
+ self._clients[name] = client
321
+ return client
322
+ else:
323
+ raise ValueError(f"未找到名为 '{name}' 的HTTP客户端配置")
324
+
325
+ # 创建新客户端
326
+ client_config["name"] = name
327
+ client = self.create_client(client_config)
328
+ self._clients[name] = client
329
+ return client
330
+
331
+ def get_session(self, name: str = "default", client_name: str = None) -> HTTPClient:
332
+ """获取或创建命名会话
333
+
334
+ Args:
335
+ name: 会话名称
336
+ client_name: 用于创建会话的客户端名称
337
+
338
+ Returns:
339
+ HTTPClient实例
340
+ """
341
+ session_key = name
342
+
343
+ # 如果会话已存在,直接返回
344
+ if session_key in self._sessions:
345
+ return self._sessions[session_key]
346
+
347
+ # 使用指定的客户端配置创建新会话
348
+ client_name = client_name or name
349
+ client_config = self._get_client_config(client_name)
350
+
351
+ if not client_config:
352
+ raise ValueError(f"未找到名为 '{client_name}' 的HTTP客户端配置,无法创建会话")
353
+
354
+ # 创建新会话
355
+ client_config["name"] = f"session_{name}"
356
+ client_config["session"] = True # 确保启用会话
357
+ session = self.create_client(client_config)
358
+ self._sessions[session_key] = session
359
+ return session
360
+
361
+ def _get_client_config(self, name: str) -> Dict[str, Any]:
362
+ """从YAML变量获取客户端配置
363
+
364
+ Args:
365
+ name: 客户端名称
366
+
367
+ Returns:
368
+ 客户端配置
369
+ """
370
+ http_clients = yaml_vars.get_variable("http_clients") or {}
371
+ client_config = http_clients.get(name)
372
+
373
+ if not client_config and name == "default":
374
+ # 如果没有默认配置,返回空配置
375
+ return {"name": "default"}
376
+
377
+ return client_config
378
+
379
+ def close_client(self, name: str) -> None:
380
+ """关闭指定的客户端
381
+
382
+ Args:
383
+ name: 客户端名称
384
+ """
385
+ if name in self._clients:
386
+ self._clients[name].close()
387
+ del self._clients[name]
388
+
389
+ def close_session(self, name: str) -> None:
390
+ """关闭指定的会话
391
+
392
+ Args:
393
+ name: 会话名称
394
+ """
395
+ if name in self._sessions:
396
+ self._sessions[name].close()
397
+ del self._sessions[name]
398
+
399
+ def close_all(self) -> None:
400
+ """关闭所有客户端和会话"""
401
+ for client in self._clients.values():
402
+ client.close()
403
+ self._clients.clear()
404
+
405
+ for session in self._sessions.values():
406
+ session.close()
407
+ self._sessions.clear()
408
+
409
+
410
+ # 创建全局HTTP客户端管理器实例
411
+ http_client_manager = HTTPClientManager()