pytest-dsl 0.11.1__py3-none-any.whl → 0.12.1__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.
- pytest_dsl/cli.py +89 -83
- pytest_dsl/core/http_client.py +91 -78
- pytest_dsl/core/http_request.py +135 -61
- pytest_dsl/examples/http/http_example.auto +1 -1
- pytest_dsl/examples/http/http_retry_assertions.auto +4 -4
- pytest_dsl/examples/http/http_retry_assertions_enhanced.auto +2 -2
- pytest_dsl/examples/http/new_retry_test.auto +1 -1
- pytest_dsl/examples/http/retry_assertions_only.auto +3 -3
- pytest_dsl/examples/http/retry_config_only.auto +3 -3
- pytest_dsl/examples/http/retry_debug.auto +19 -8
- pytest_dsl/examples/http/retry_with_fix.auto +1 -1
- pytest_dsl/keywords/http_keywords.py +42 -14
- pytest_dsl/keywords/system_keywords.py +420 -1
- pytest_dsl/plugin.py +3 -2
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/METADATA +3 -3
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/RECORD +20 -20
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/WHEEL +0 -0
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.11.1.dist-info → pytest_dsl-0.12.1.dist-info}/top_level.txt +0 -0
pytest_dsl/core/http_client.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
|
-
from typing import Dict, Any
|
3
|
+
from typing import Dict, Any
|
4
4
|
import requests
|
5
5
|
from urllib.parse import urljoin
|
6
6
|
from pytest_dsl.core.yaml_vars import yaml_vars
|
@@ -11,11 +11,11 @@ logger = logging.getLogger(__name__)
|
|
11
11
|
|
12
12
|
class HTTPClient:
|
13
13
|
"""HTTP客户端类
|
14
|
-
|
14
|
+
|
15
15
|
负责管理HTTP请求会话和发送请求
|
16
16
|
"""
|
17
|
-
|
18
|
-
def __init__(self,
|
17
|
+
|
18
|
+
def __init__(self,
|
19
19
|
name: str = "default",
|
20
20
|
base_url: str = "",
|
21
21
|
headers: Dict[str, str] = None,
|
@@ -26,7 +26,7 @@ class HTTPClient:
|
|
26
26
|
proxies: Dict[str, str] = None,
|
27
27
|
auth_config: Dict[str, Any] = None):
|
28
28
|
"""初始化HTTP客户端
|
29
|
-
|
29
|
+
|
30
30
|
Args:
|
31
31
|
name: 客户端名称
|
32
32
|
base_url: 基础URL
|
@@ -50,38 +50,38 @@ class HTTPClient:
|
|
50
50
|
"retry_on_status": [500, 502, 503, 504]
|
51
51
|
}
|
52
52
|
self.proxies = proxies or {}
|
53
|
-
|
53
|
+
|
54
54
|
# 处理认证配置
|
55
55
|
self.auth_provider = None
|
56
56
|
if auth_config:
|
57
57
|
self.auth_provider = create_auth_provider(auth_config)
|
58
58
|
if not self.auth_provider:
|
59
59
|
logger.warning(f"无法创建认证提供者: {auth_config}")
|
60
|
-
|
60
|
+
|
61
61
|
# 创建会话
|
62
62
|
self._session = requests.Session() if self.use_session else None
|
63
63
|
if self.use_session and self.default_headers:
|
64
64
|
self._session.headers.update(self.default_headers)
|
65
|
-
|
65
|
+
|
66
66
|
def reset_session(self):
|
67
67
|
"""完全重置会话对象,创建一个新的会话实例
|
68
|
-
|
68
|
+
|
69
69
|
当需要彻底清除所有会话状态(例如认证信息、cookies等)时使用
|
70
70
|
"""
|
71
71
|
if self.use_session:
|
72
72
|
# 关闭当前会话
|
73
73
|
if self._session:
|
74
74
|
self._session.close()
|
75
|
-
|
75
|
+
|
76
76
|
# 创建新会话
|
77
77
|
self._session = requests.Session()
|
78
|
-
|
78
|
+
|
79
79
|
# 重新应用默认头
|
80
80
|
if self.default_headers:
|
81
81
|
self._session.headers.update(self.default_headers)
|
82
|
-
|
82
|
+
|
83
83
|
logger.debug(f"会话已完全重置: {self.name}")
|
84
|
-
|
84
|
+
|
85
85
|
def make_request(self, method: str, url: str, **request_kwargs) -> requests.Response:
|
86
86
|
"""发送HTTP请求
|
87
87
|
|
@@ -102,7 +102,8 @@ class HTTPClient:
|
|
102
102
|
if disable_auth:
|
103
103
|
# 如果有认证提供者,使用其清理逻辑
|
104
104
|
if self.auth_provider:
|
105
|
-
request_kwargs = self.auth_provider.clean_auth_state(
|
105
|
+
request_kwargs = self.auth_provider.clean_auth_state(
|
106
|
+
request_kwargs)
|
106
107
|
else:
|
107
108
|
# 默认清理逻辑:移除所有认证相关的头
|
108
109
|
auth_headers = [
|
@@ -115,49 +116,54 @@ class HTTPClient:
|
|
115
116
|
request_kwargs['headers'].pop(header, None)
|
116
117
|
# 移除认证参数
|
117
118
|
request_kwargs.pop('auth', None)
|
118
|
-
|
119
|
+
|
119
120
|
# 如果使用会话,并且认证提供者没有自己的会话管理,则使用默认的会话重置
|
120
121
|
if self.use_session and not hasattr(self.auth_provider, 'manage_session'):
|
121
122
|
self.reset_session()
|
122
|
-
|
123
|
+
|
123
124
|
elif self.auth_provider and 'auth' not in request_kwargs:
|
124
125
|
# 应用认证提供者
|
125
|
-
request_kwargs = self.auth_provider.apply_auth(
|
126
|
+
request_kwargs = self.auth_provider.apply_auth(
|
127
|
+
self.base_url, request_kwargs)
|
126
128
|
# 如果使用会话,更新会话头
|
127
129
|
if self.use_session and 'headers' in request_kwargs:
|
128
130
|
self._session.headers.update(request_kwargs['headers'])
|
129
131
|
|
130
132
|
# 调用认证提供者的请求前钩子
|
131
133
|
if self.auth_provider and not disable_auth:
|
132
|
-
request_kwargs = self.auth_provider.pre_request_hook(
|
134
|
+
request_kwargs = self.auth_provider.pre_request_hook(
|
135
|
+
method, url, request_kwargs)
|
133
136
|
|
134
137
|
# 记录请求详情
|
135
138
|
logger.debug("=== HTTP请求详情 ===")
|
136
139
|
logger.debug(f"方法: {method}")
|
137
140
|
logger.debug(f"URL: {url}")
|
138
141
|
if 'headers' in request_kwargs:
|
139
|
-
safe_headers = {k: '***' if k.lower() in ['authorization', 'x-api-key', 'token'] else v
|
140
|
-
|
141
|
-
logger.debug(
|
142
|
+
safe_headers = {k: '***' if k.lower() in ['authorization', 'x-api-key', 'token'] else v
|
143
|
+
for k, v in request_kwargs['headers'].items()}
|
144
|
+
logger.debug(
|
145
|
+
f"请求头: {json.dumps(safe_headers, indent=2, ensure_ascii=False)}")
|
142
146
|
if 'params' in request_kwargs:
|
143
|
-
logger.debug(
|
147
|
+
logger.debug(
|
148
|
+
f"查询参数: {json.dumps(request_kwargs['params'], indent=2, ensure_ascii=False)}")
|
144
149
|
if 'json' in request_kwargs:
|
145
|
-
logger.debug(
|
150
|
+
logger.debug(
|
151
|
+
f"JSON请求体: {json.dumps(request_kwargs['json'], indent=2, ensure_ascii=False)}")
|
146
152
|
if 'data' in request_kwargs:
|
147
153
|
logger.debug(f"表单数据: {request_kwargs['data']}")
|
148
154
|
|
149
155
|
# 为超时设置默认值
|
150
156
|
if 'timeout' not in request_kwargs:
|
151
157
|
request_kwargs['timeout'] = self.timeout
|
152
|
-
|
158
|
+
|
153
159
|
# 为SSL验证设置默认值
|
154
160
|
if 'verify' not in request_kwargs:
|
155
161
|
request_kwargs['verify'] = self.verify_ssl
|
156
|
-
|
162
|
+
|
157
163
|
# 应用代理配置
|
158
164
|
if self.proxies and 'proxies' not in request_kwargs:
|
159
165
|
request_kwargs['proxies'] = self.proxies
|
160
|
-
|
166
|
+
|
161
167
|
try:
|
162
168
|
# 发送请求
|
163
169
|
if self.use_session:
|
@@ -173,10 +179,12 @@ class HTTPClient:
|
|
173
179
|
# 记录响应详情
|
174
180
|
logger.debug("\n=== HTTP响应详情 ===")
|
175
181
|
logger.debug(f"状态码: {response.status_code}")
|
176
|
-
logger.debug(
|
182
|
+
logger.debug(
|
183
|
+
f"响应头: {json.dumps(dict(response.headers), indent=2, ensure_ascii=False)}")
|
177
184
|
try:
|
178
185
|
if 'application/json' in response.headers.get('Content-Type', ''):
|
179
|
-
logger.debug(
|
186
|
+
logger.debug(
|
187
|
+
f"响应体 (JSON): {json.dumps(response.json(), indent=2, ensure_ascii=False)}")
|
180
188
|
else:
|
181
189
|
logger.debug(f"响应体: {response.text}")
|
182
190
|
except Exception as e:
|
@@ -186,11 +194,11 @@ class HTTPClient:
|
|
186
194
|
# 添加响应时间
|
187
195
|
if not hasattr(response, 'elapsed_ms'):
|
188
196
|
response.elapsed_ms = response.elapsed.total_seconds() * 1000
|
189
|
-
|
197
|
+
|
190
198
|
# 调用认证提供者的响应处理钩子
|
191
199
|
if self.auth_provider and not disable_auth:
|
192
200
|
self.auth_provider.post_response_hook(response, request_kwargs)
|
193
|
-
|
201
|
+
|
194
202
|
return response
|
195
203
|
except requests.exceptions.RequestException as e:
|
196
204
|
logger.error(f"HTTP请求异常: {type(e).__name__}: {str(e)}")
|
@@ -198,67 +206,72 @@ class HTTPClient:
|
|
198
206
|
except Exception as e:
|
199
207
|
logger.error(f"未预期的异常: {type(e).__name__}: {str(e)}")
|
200
208
|
# 将非请求异常包装为请求异常
|
201
|
-
raised_exception = requests.exceptions.RequestException(
|
209
|
+
raised_exception = requests.exceptions.RequestException(
|
210
|
+
f"HTTP请求过程中发生错误: {str(e)}")
|
202
211
|
raised_exception.__cause__ = e
|
203
212
|
raise raised_exception
|
204
|
-
|
213
|
+
|
205
214
|
def _log_request(self, method: str, url: str, request_kwargs: Dict[str, Any]) -> None:
|
206
215
|
"""记录请求信息
|
207
|
-
|
216
|
+
|
208
217
|
Args:
|
209
218
|
method: HTTP方法
|
210
219
|
url: 请求URL
|
211
220
|
request_kwargs: 请求参数
|
212
221
|
"""
|
213
222
|
logger.info(f"发送 {method} 请求到 {url}")
|
214
|
-
|
223
|
+
|
215
224
|
# 打印请求头 (排除敏感信息)
|
216
225
|
headers = request_kwargs.get("headers", {})
|
217
226
|
safe_headers = headers.copy()
|
218
|
-
|
227
|
+
|
219
228
|
for key in headers:
|
220
229
|
if key.lower() in ["authorization", "x-api-key", "token", "api-key"]:
|
221
230
|
safe_headers[key] = "***REDACTED***"
|
222
|
-
|
231
|
+
|
223
232
|
logger.debug(f"请求头: {safe_headers}")
|
224
|
-
|
233
|
+
|
225
234
|
# 打印查询参数
|
226
235
|
if request_kwargs.get("params"):
|
227
236
|
logger.debug(f"查询参数: {request_kwargs['params']}")
|
228
|
-
|
237
|
+
|
229
238
|
# 打印请求体
|
230
239
|
if request_kwargs.get("json"):
|
231
|
-
logger.debug(
|
240
|
+
logger.debug(
|
241
|
+
f"JSON请求体: {json.dumps(request_kwargs['json'], ensure_ascii=False)}")
|
232
242
|
elif request_kwargs.get("data"):
|
233
243
|
logger.debug(f"表单数据: {request_kwargs['data']}")
|
234
|
-
|
244
|
+
|
235
245
|
# 打印文件信息
|
236
246
|
if request_kwargs.get("files"):
|
237
|
-
file_info = {
|
247
|
+
file_info = {
|
248
|
+
k: f"<文件: {getattr(v, 'name', '未知文件')}>" for k, v in request_kwargs["files"].items()}
|
238
249
|
logger.debug(f"上传文件: {file_info}")
|
239
|
-
|
250
|
+
|
240
251
|
def _log_response(self, response: requests.Response) -> None:
|
241
252
|
"""记录响应信息
|
242
|
-
|
253
|
+
|
243
254
|
Args:
|
244
255
|
response: 响应对象
|
245
256
|
"""
|
246
|
-
logger.info(
|
247
|
-
|
257
|
+
logger.info(
|
258
|
+
f"收到响应: {response.status_code} {response.reason} ({response.elapsed_ms:.2f}ms)")
|
259
|
+
|
248
260
|
# 打印响应头
|
249
261
|
logger.debug(f"响应头: {dict(response.headers)}")
|
250
|
-
|
262
|
+
|
251
263
|
# 尝试打印响应体
|
252
264
|
try:
|
253
265
|
if 'application/json' in response.headers.get('Content-Type', ''):
|
254
|
-
logger.debug(
|
266
|
+
logger.debug(
|
267
|
+
f"响应体 (JSON): {json.dumps(response.json(), ensure_ascii=False)}")
|
255
268
|
elif len(response.content) < 1024: # 只打印小响应
|
256
269
|
logger.debug(f"响应体: {response.text}")
|
257
270
|
else:
|
258
271
|
logger.debug(f"响应体: <{len(response.content)} 字节>")
|
259
272
|
except Exception as e:
|
260
273
|
logger.debug(f"无法打印响应体: {str(e)}")
|
261
|
-
|
274
|
+
|
262
275
|
def close(self) -> None:
|
263
276
|
"""关闭客户端会话"""
|
264
277
|
if self._session:
|
@@ -268,21 +281,21 @@ class HTTPClient:
|
|
268
281
|
|
269
282
|
class HTTPClientManager:
|
270
283
|
"""HTTP客户端管理器
|
271
|
-
|
284
|
+
|
272
285
|
负责管理多个HTTP客户端实例和会话
|
273
286
|
"""
|
274
|
-
|
287
|
+
|
275
288
|
def __init__(self):
|
276
289
|
"""初始化客户端管理器"""
|
277
290
|
self._clients: Dict[str, HTTPClient] = {}
|
278
291
|
self._sessions: Dict[str, HTTPClient] = {}
|
279
|
-
|
292
|
+
|
280
293
|
def create_client(self, config: Dict[str, Any]) -> HTTPClient:
|
281
294
|
"""从配置创建客户端
|
282
|
-
|
295
|
+
|
283
296
|
Args:
|
284
297
|
config: 客户端配置
|
285
|
-
|
298
|
+
|
286
299
|
Returns:
|
287
300
|
HTTPClient实例
|
288
301
|
"""
|
@@ -299,24 +312,24 @@ class HTTPClientManager:
|
|
299
312
|
auth_config=config.get("auth", None) # 获取认证配置
|
300
313
|
)
|
301
314
|
return client
|
302
|
-
|
315
|
+
|
303
316
|
def get_client(self, name: str = "default") -> HTTPClient:
|
304
317
|
"""获取或创建客户端
|
305
|
-
|
318
|
+
|
306
319
|
Args:
|
307
320
|
name: 客户端名称
|
308
|
-
|
321
|
+
|
309
322
|
Returns:
|
310
323
|
HTTPClient实例
|
311
324
|
"""
|
312
325
|
# 如果客户端已存在,直接返回
|
313
326
|
if name in self._clients:
|
314
327
|
return self._clients[name]
|
315
|
-
|
328
|
+
|
316
329
|
# 从YAML变量中读取客户端配置
|
317
330
|
http_clients = yaml_vars.get_variable("http_clients") or {}
|
318
331
|
client_config = http_clients.get(name)
|
319
|
-
|
332
|
+
|
320
333
|
if not client_config:
|
321
334
|
# 如果请求的是default但配置中没有,创建一个默认客户端
|
322
335
|
if name == "default":
|
@@ -326,91 +339,91 @@ class HTTPClientManager:
|
|
326
339
|
return client
|
327
340
|
else:
|
328
341
|
raise ValueError(f"未找到名为 '{name}' 的HTTP客户端配置")
|
329
|
-
|
342
|
+
|
330
343
|
# 创建新客户端
|
331
344
|
client_config["name"] = name
|
332
345
|
client = self.create_client(client_config)
|
333
346
|
self._clients[name] = client
|
334
347
|
return client
|
335
|
-
|
348
|
+
|
336
349
|
def get_session(self, name: str = "default", client_name: str = None) -> HTTPClient:
|
337
350
|
"""获取或创建命名会话
|
338
|
-
|
351
|
+
|
339
352
|
Args:
|
340
353
|
name: 会话名称
|
341
354
|
client_name: 用于创建会话的客户端名称
|
342
|
-
|
355
|
+
|
343
356
|
Returns:
|
344
357
|
HTTPClient实例
|
345
358
|
"""
|
346
359
|
session_key = name
|
347
|
-
|
360
|
+
|
348
361
|
# 如果会话已存在,直接返回
|
349
362
|
if session_key in self._sessions:
|
350
363
|
return self._sessions[session_key]
|
351
|
-
|
364
|
+
|
352
365
|
# 使用指定的客户端配置创建新会话
|
353
366
|
client_name = client_name or name
|
354
367
|
client_config = self._get_client_config(client_name)
|
355
|
-
|
368
|
+
|
356
369
|
if not client_config:
|
357
370
|
raise ValueError(f"未找到名为 '{client_name}' 的HTTP客户端配置,无法创建会话")
|
358
|
-
|
371
|
+
|
359
372
|
# 创建新会话
|
360
373
|
client_config["name"] = f"session_{name}"
|
361
374
|
client_config["session"] = True # 确保启用会话
|
362
375
|
session = self.create_client(client_config)
|
363
376
|
self._sessions[session_key] = session
|
364
377
|
return session
|
365
|
-
|
378
|
+
|
366
379
|
def _get_client_config(self, name: str) -> Dict[str, Any]:
|
367
380
|
"""从YAML变量获取客户端配置
|
368
|
-
|
381
|
+
|
369
382
|
Args:
|
370
383
|
name: 客户端名称
|
371
|
-
|
384
|
+
|
372
385
|
Returns:
|
373
386
|
客户端配置
|
374
387
|
"""
|
375
388
|
http_clients = yaml_vars.get_variable("http_clients") or {}
|
376
389
|
client_config = http_clients.get(name)
|
377
|
-
|
390
|
+
|
378
391
|
if not client_config and name == "default":
|
379
392
|
# 如果没有默认配置,返回空配置
|
380
393
|
return {"name": "default"}
|
381
|
-
|
394
|
+
|
382
395
|
return client_config
|
383
|
-
|
396
|
+
|
384
397
|
def close_client(self, name: str) -> None:
|
385
398
|
"""关闭指定的客户端
|
386
|
-
|
399
|
+
|
387
400
|
Args:
|
388
401
|
name: 客户端名称
|
389
402
|
"""
|
390
403
|
if name in self._clients:
|
391
404
|
self._clients[name].close()
|
392
405
|
del self._clients[name]
|
393
|
-
|
406
|
+
|
394
407
|
def close_session(self, name: str) -> None:
|
395
408
|
"""关闭指定的会话
|
396
|
-
|
409
|
+
|
397
410
|
Args:
|
398
411
|
name: 会话名称
|
399
412
|
"""
|
400
413
|
if name in self._sessions:
|
401
414
|
self._sessions[name].close()
|
402
415
|
del self._sessions[name]
|
403
|
-
|
416
|
+
|
404
417
|
def close_all(self) -> None:
|
405
418
|
"""关闭所有客户端和会话"""
|
406
419
|
for client in self._clients.values():
|
407
420
|
client.close()
|
408
421
|
self._clients.clear()
|
409
|
-
|
422
|
+
|
410
423
|
for session in self._sessions.values():
|
411
424
|
session.close()
|
412
425
|
self._sessions.clear()
|
413
426
|
|
414
427
|
|
415
428
|
# 创建全局HTTP客户端管理器实例
|
416
|
-
http_client_manager = HTTPClientManager()
|
429
|
+
http_client_manager = HTTPClientManager()
|