pytest-testkit 1.0.2b1__tar.gz → 1.0.3__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.
Files changed (59) hide show
  1. {pytest_testkit-1.0.2b1/pytest_testkit.egg-info → pytest_testkit-1.0.3}/PKG-INFO +47 -23
  2. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/README.md +45 -19
  3. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pyproject.toml +2 -4
  4. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/base/sftp.py +1 -1
  5. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/common/environment/device.py +32 -25
  6. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3/pytest_testkit.egg-info}/PKG-INFO +47 -23
  7. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit.egg-info/SOURCES.txt +0 -1
  8. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit.egg-info/requires.txt +1 -3
  9. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/requirements.txt +2 -4
  10. pytest_testkit-1.0.2b1/pytest_testkit/lib/base/http.py +0 -431
  11. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/LICENSE +0 -0
  12. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/MANIFEST.in +0 -0
  13. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/__init__.py +0 -0
  14. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/__init__.py +0 -0
  15. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/base/__init__.py +0 -0
  16. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/base/excel.py +0 -0
  17. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/base/ftp.py +0 -0
  18. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/base/local_exec.py +0 -0
  19. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/base/ssh.py +0 -0
  20. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/common/__init__.py +0 -0
  21. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/common/environment/__init__.py +0 -0
  22. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/common/environment/allocator.py +0 -0
  23. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/common/environment/manager.py +0 -0
  24. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/common/log/__init__.py +0 -0
  25. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/common/log/filters.py +0 -0
  26. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/common/log/infra_logger.py +0 -0
  27. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/common/log/manager.py +0 -0
  28. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/pre_action/__init__.py +0 -0
  29. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/pre_action/action.py +0 -0
  30. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/pre_action/exceptions.py +0 -0
  31. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/lib/pre_action/registry.py +0 -0
  32. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit/plugin.py +0 -0
  33. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit.egg-info/dependency_links.txt +0 -0
  34. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit.egg-info/entry_points.txt +0 -0
  35. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/pytest_testkit.egg-info/top_level.txt +0 -0
  36. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/setup.cfg +0 -0
  37. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_collect_case_with_inifile.py +0 -0
  38. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_docker_proxy_creation.py +0 -0
  39. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_docker_proxy_exit_docker.py +0 -0
  40. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_docker_proxy_getitem.py +0 -0
  41. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_docker_proxy_integration.py +0 -0
  42. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_docker_proxy_is_interactive_shell.py +0 -0
  43. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_docker_proxy_non_shell_commands.py +0 -0
  44. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_docker_proxy_sendcmd.py +0 -0
  45. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_docker_proxy_sendcmd_interactive.py +0 -0
  46. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_env_nodes_filter.py +0 -0
  47. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_get_inifile_info.py +0 -0
  48. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_get_item_case_info.py +0 -0
  49. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_local_exec.py +0 -0
  50. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_local_interactive.py +0 -0
  51. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_log_factory.py +0 -0
  52. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_log_factory_integration.py +0 -0
  53. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_security_features.py +0 -0
  54. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_ssh_cwd.py +0 -0
  55. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_ssh_file_ops.py +0 -0
  56. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_ssh_ignore_err.py +0 -0
  57. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_ssh_only_stdout.py +0 -0
  58. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_ssh_remove_command_echo.py +0 -0
  59. {pytest_testkit-1.0.2b1 → pytest_testkit-1.0.3}/tests/test_ssh_success.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest_testkit
3
- Version: 1.0.2b1
3
+ Version: 1.0.3
4
4
  Summary: Pytest plugin for Infra logging framework and environment management
5
5
  Author: Huawei
6
6
  License: MIT License
@@ -43,14 +43,12 @@ Classifier: Operating System :: OS Independent
43
43
  Requires-Python: >=3.6
44
44
  Description-Content-Type: text/markdown
45
45
  License-File: LICENSE
46
- Requires-Dist: pytest==8.3.2
46
+ Requires-Dist: pytest>=8.3.2
47
47
  Requires-Dist: requests
48
48
  Requires-Dist: paramiko
49
49
  Requires-Dist: xlrd>=2.0.0
50
50
  Requires-Dist: PyYaml
51
51
  Requires-Dist: scp
52
- Requires-Dist: execnet
53
- Requires-Dist: pybind11
54
52
  Provides-Extra: dev
55
53
  Requires-Dist: build; extra == "dev"
56
54
  Requires-Dist: twine; extra == "dev"
@@ -126,6 +124,8 @@ docker run -d --name <name> -p 2222:22 <image>:<tag>
126
124
  ```
127
125
  ### logger 使用
128
126
 
127
+ > **⚠️ 安全提醒:该`logger`为 `pytest-testkit` 插件提供的基础日志能力,请勿在测试用例中打印敏感信息(如密码、密钥、Token 等),以免造成信息泄露。**
128
+
129
129
  导入 logger:
130
130
 
131
131
  ```python
@@ -135,7 +135,7 @@ from pytest_testkit.lib.common.log import logger
135
135
  使用 logger 记录日志:
136
136
 
137
137
  ```python
138
- logger.info(f"[Device._login] Successfully connected to device")
138
+ logger.info("Successfully connected to device")
139
139
  ```
140
140
 
141
141
  #### 日志目录结构
@@ -152,12 +152,30 @@ logs/
152
152
 
153
153
  **目录结构说明**:
154
154
  - **TestCases**: 测试用例日志根目录
155
- - **日期目录**: 按执行日期组织(格式:`YYYY-MM-DD`)
156
- - **用例子目录**: 按用例名和执行时间命名(格式:`用例名_HH-MM-SS`)
155
+ - **日期目录**: 按执行日期组织(格式:`YYYY-MM-DD`,使用本机时区)
156
+ - **用例子目录**: 按用例名和执行时间命名(格式:`用例名_HH-MM-SS`,使用本机时区)
157
157
  - **HTML 文件**: 插件生成的测试报告,文件名与用例名一致
158
158
 
159
159
  通过 `logger.path` 属性可以获取当前用例的日志归档目录路径,方便在测试用例中创建自定义文件。
160
160
 
161
+ #### HTML 报告渲染
162
+
163
+ > **⚠️ 安全提醒:HTML 报告会完整渲染日志中记录的命令文本、回显输出及登录 banner 等内容,请勿在命令或日志中携带敏感信息(如密码、密钥、Token 等),以免在报告中明文暴露。**
164
+
165
+ 测试用例的 HTML 日志报告支持以下渲染特性:
166
+
167
+ - **op_type 标签**: 命令与连接日志通过 `logging` 的 `extra={"op_type": ...}` 标记操作类型,HTML 报告 Location 列显示该标签(未标记记录回退为 `文件名:行号`)。内置标签包括:
168
+ - `sendcmd[<设备短名>]`: 发送命令,消息带最近 shell 提示符前缀(如 `root@host:~#ls -la`),设置 `cwd` 时追加 `(cwd: ...)`
169
+ - `recvcmd[<设备短名>]`: 命令文本回显(stdout+stderr 合并,不打印 dict),末尾带最新提示符;无输出时显示 `(empty)`
170
+ - `login[<设备短名>]` / `loginresult[<设备短名>]`: 登录开始与结果(登录失败为 ERROR 级别)
171
+ - `loginecho[<设备短名>]`: 登录 banner(交互 channel 首次创建时吸收,在第一条 sendcmd 之前打印)
172
+ - `disconnect[<设备短名>]`: 连接关闭
173
+ - `<设备短名>` 取设备名最后一段(按 `-` 或 `/` 分割),如 `rack1/node_3` → `node_3`
174
+ - **ANSI 颜色渲染**: 报告自动把 ANSI 前景色码(30-37、90-97)转换为带颜色的 `<span>` 标签。例如命令日志中的蓝色码(`\x1b[34m`)渲染为蓝色(`#2196F3`)
175
+ - **错误词高亮**: `recvcmd` 回显中的常见错误词(error、failed、fatal、panic、traceback、timeout、"No such file or directory" 等)会以粗体红色显示,便于快速定位失败。命令行(sendcmd)与普通日志不高亮,避免误伤命令本身
176
+ - **列宽调整**: Level 列 70px、Thread 列 50px、Location 列 128px,Message 列获得更多空间
177
+ - **本地时间**: 会话目录、日期目录、用例子目录均使用本机时区时间,与报告表内日志时间戳一致
178
+
161
179
  日志级别:
162
180
 
163
181
  | 级别 | 值 | 方法 | 说明 |
@@ -411,9 +429,9 @@ def test_environment(logger):
411
429
 
412
430
  ### quiet_stdout 功能
413
431
 
414
- pytest-testkit 插件支持全局静默 `sendcmd` / `sendcmd_interactive` 的 stdout 输出,避免大量回显淹没日志。
415
- **开启后,stdout 内容不会出现在日志中,即使调用方显式传 `echo_output=True` 也会被全局压制。**
416
- exit_code、prompt、stderr 等信息不受影响。
432
+ pytest-testkit 插件支持全局静默 `sendcmd` / `sendcmd_oneshot` / `sendcmd_interactive` 的命令与回显日志,避免大量回显淹没日志。
433
+ **开启后,sendcmd / recvcmd 日志都不会打印,即使调用方显式传 `echo_output=True` 也会被全局压制。**
434
+ 命令返回值(stdout / stderr / exit_code 等)不受影响,仅影响日志输出。
417
435
 
418
436
  **支持CLI 参数 和 ini 配置,默认为禁用**
419
437
 
@@ -528,23 +546,27 @@ def test_use_ci_metadata():
528
546
 
529
547
  - **environment/environments**: 提供单设备/多设备对象
530
548
 
531
- - **sendcmd(cmd, timeout=None, env_vars=None, cwd=None, ignore_err=False, only_stdout=True)**:执行命令(非交互式)
549
+ - **sendcmd(cmd, timeout=None, env_vars=None, cwd=None, ignore_err=False, only_stdout=True, echo_output=True)**:在持久化交互 shell channel 上执行命令
532
550
  - `cmd`: 要执行的命令字符串
533
- - `timeout`: 命令执行超时时间(秒)
534
- - `env_vars`: 环境变量字典
551
+ - `timeout`: 命令执行超时时间(秒),支持 int 或 str,默认 600s
552
+ - `env_vars`: 环境变量字典,内联为单命令前缀(`VAR=value cmd`),仅对本次命令生效,不污染持久化 shell
535
553
  - `cwd`: 工作目录,命令将在该目录下执行(可选)
536
554
  - `ignore_err`: 是否忽略命令执行错误,默认 False。设为 True 时即使命令失败 success 也为 True
537
555
  - `only_stdout`: 为 True 时只返回 stdout 字符串,为 False 时返回完整字典
556
+ - `echo_output`: 是否打印 sendcmd/recvcmd 日志,默认 True
557
+ - 行为说明: 同一线程内 shell 状态(`cd`、`export`)跨调用持久化;PTY 合并 stderr 到 stdout,返回 dict 的 `stderr` 通常为空;退出码通过 `$?` 哨兵尽力捕获。需要独立进程执行时请用 `sendcmd_oneshot`
538
558
  - 返回值: 默认返回 stdout 字符串;当 `only_stdout=False` 时返回 `{'success': bool, 'stdout': str, 'stderr': str}`
539
559
 
540
- - **sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True)**:执行命令(交互式)
541
- - `cmd`: 要执行的命令字符串
542
- - `expect_prompt`: 期望匹配的提示符正则表达式(可选)
543
- - `timeout`: 命令执行超时时间(秒)。默认值600s
544
- - `cwd`: 工作目录,命令将在该目录下执行(可选)
545
- - `ignore_err`: 是否忽略提示符匹配失败,默认 False。设为 True 时即使未匹配到提示符 success 也为 True
546
- - `only_stdout`: 为 True 时只返回 stdout 字符串,为 False 时返回完整字典
547
- - 返回值: 默认返回 stdout 字符串;当 `only_stdout=False` 时返回 `{'success': bool, 'stdout': str, 'stderr': str}`
560
+ - **sendcmd_oneshot(cmd, timeout=None, env_vars=None, cwd=None, ignore_err=False, only_stdout=True, echo_output=True)**:独立进程执行命令(原 sendcmd 行为)
561
+ - 每次调用启动独立的 `exec_command` / `subprocess.run` 进程,无持久 shell 状态,与交互 channel 互相独立
562
+ - SSH 路径: 以 `bash -l -c` 包装执行,原生环境变量支持,退出码为 channel 权威退出状态,stderr 为独立流
563
+ - `local_exec` 路径: 通过 `subprocess.run` 执行
564
+ - 参数与返回值与 `sendcmd` 一致(`env_vars` 通过原生环境注入而非内联前缀)
565
+
566
+ - **sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True)**:等同于 `sendcmd`(保留向后兼容)
567
+ - 现直接代理给 `sendcmd`,在持久化交互 shell channel 上执行
568
+ - `expect_prompt`: 保留签名兼容但不再生效(被忽略)
569
+ - 参数与返回值与 `sendcmd` 一致
548
570
  - **输出清理**: 返回的 stdout 会自动清理以下内容,便于用户直接使用:
549
571
  - CSI 控制序列(如 `\x1b[32m` 颜色代码、`\x1b[?2004h` bracketed paste mode 等)
550
572
  - OSC 控制序列(如 `\x1b]0;title\x07` 窗口标题设置等)
@@ -608,8 +630,10 @@ def test_use_ci_metadata():
608
630
  - **DockerProxy 容器代理对象**:通过 `environment["container_name"]` 获取
609
631
  - 代理对象提供与 Device 一致的接口,自动包装 `docker exec` 命令
610
632
 
611
- - **sendcmd(cmd, timeout=None, ignore_err=False, only_stdout=True)**:非交互式命令
612
- - 每次独立执行 `docker exec {docker_name} {cmd}`
633
+ - **sendcmd(cmd, timeout=None, ignore_err=False, only_stdout=True)**:非交互式命令(docker session 感知)
634
+ - 已在该容器交互式 session 中: 直接在当前 session 执行命令
635
+ - 其他容器持有交互式 session: 直接返回失败(提示"请退出上一个交互对话")
636
+ - 否则: 包装为 `docker exec {docker_name} {cmd}` 执行(不创建持久 session)
613
637
  - 返回值: stdout 字符串或完整字典
614
638
 
615
639
  - **sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True)**:交互式命令
@@ -68,6 +68,8 @@ docker run -d --name <name> -p 2222:22 <image>:<tag>
68
68
  ```
69
69
  ### logger 使用
70
70
 
71
+ > **⚠️ 安全提醒:该`logger`为 `pytest-testkit` 插件提供的基础日志能力,请勿在测试用例中打印敏感信息(如密码、密钥、Token 等),以免造成信息泄露。**
72
+
71
73
  导入 logger:
72
74
 
73
75
  ```python
@@ -77,7 +79,7 @@ from pytest_testkit.lib.common.log import logger
77
79
  使用 logger 记录日志:
78
80
 
79
81
  ```python
80
- logger.info(f"[Device._login] Successfully connected to device")
82
+ logger.info("Successfully connected to device")
81
83
  ```
82
84
 
83
85
  #### 日志目录结构
@@ -94,12 +96,30 @@ logs/
94
96
 
95
97
  **目录结构说明**:
96
98
  - **TestCases**: 测试用例日志根目录
97
- - **日期目录**: 按执行日期组织(格式:`YYYY-MM-DD`)
98
- - **用例子目录**: 按用例名和执行时间命名(格式:`用例名_HH-MM-SS`)
99
+ - **日期目录**: 按执行日期组织(格式:`YYYY-MM-DD`,使用本机时区)
100
+ - **用例子目录**: 按用例名和执行时间命名(格式:`用例名_HH-MM-SS`,使用本机时区)
99
101
  - **HTML 文件**: 插件生成的测试报告,文件名与用例名一致
100
102
 
101
103
  通过 `logger.path` 属性可以获取当前用例的日志归档目录路径,方便在测试用例中创建自定义文件。
102
104
 
105
+ #### HTML 报告渲染
106
+
107
+ > **⚠️ 安全提醒:HTML 报告会完整渲染日志中记录的命令文本、回显输出及登录 banner 等内容,请勿在命令或日志中携带敏感信息(如密码、密钥、Token 等),以免在报告中明文暴露。**
108
+
109
+ 测试用例的 HTML 日志报告支持以下渲染特性:
110
+
111
+ - **op_type 标签**: 命令与连接日志通过 `logging` 的 `extra={"op_type": ...}` 标记操作类型,HTML 报告 Location 列显示该标签(未标记记录回退为 `文件名:行号`)。内置标签包括:
112
+ - `sendcmd[<设备短名>]`: 发送命令,消息带最近 shell 提示符前缀(如 `root@host:~#ls -la`),设置 `cwd` 时追加 `(cwd: ...)`
113
+ - `recvcmd[<设备短名>]`: 命令文本回显(stdout+stderr 合并,不打印 dict),末尾带最新提示符;无输出时显示 `(empty)`
114
+ - `login[<设备短名>]` / `loginresult[<设备短名>]`: 登录开始与结果(登录失败为 ERROR 级别)
115
+ - `loginecho[<设备短名>]`: 登录 banner(交互 channel 首次创建时吸收,在第一条 sendcmd 之前打印)
116
+ - `disconnect[<设备短名>]`: 连接关闭
117
+ - `<设备短名>` 取设备名最后一段(按 `-` 或 `/` 分割),如 `rack1/node_3` → `node_3`
118
+ - **ANSI 颜色渲染**: 报告自动把 ANSI 前景色码(30-37、90-97)转换为带颜色的 `<span>` 标签。例如命令日志中的蓝色码(`\x1b[34m`)渲染为蓝色(`#2196F3`)
119
+ - **错误词高亮**: `recvcmd` 回显中的常见错误词(error、failed、fatal、panic、traceback、timeout、"No such file or directory" 等)会以粗体红色显示,便于快速定位失败。命令行(sendcmd)与普通日志不高亮,避免误伤命令本身
120
+ - **列宽调整**: Level 列 70px、Thread 列 50px、Location 列 128px,Message 列获得更多空间
121
+ - **本地时间**: 会话目录、日期目录、用例子目录均使用本机时区时间,与报告表内日志时间戳一致
122
+
103
123
  日志级别:
104
124
 
105
125
  | 级别 | 值 | 方法 | 说明 |
@@ -353,9 +373,9 @@ def test_environment(logger):
353
373
 
354
374
  ### quiet_stdout 功能
355
375
 
356
- pytest-testkit 插件支持全局静默 `sendcmd` / `sendcmd_interactive` 的 stdout 输出,避免大量回显淹没日志。
357
- **开启后,stdout 内容不会出现在日志中,即使调用方显式传 `echo_output=True` 也会被全局压制。**
358
- exit_code、prompt、stderr 等信息不受影响。
376
+ pytest-testkit 插件支持全局静默 `sendcmd` / `sendcmd_oneshot` / `sendcmd_interactive` 的命令与回显日志,避免大量回显淹没日志。
377
+ **开启后,sendcmd / recvcmd 日志都不会打印,即使调用方显式传 `echo_output=True` 也会被全局压制。**
378
+ 命令返回值(stdout / stderr / exit_code 等)不受影响,仅影响日志输出。
359
379
 
360
380
  **支持CLI 参数 和 ini 配置,默认为禁用**
361
381
 
@@ -470,23 +490,27 @@ def test_use_ci_metadata():
470
490
 
471
491
  - **environment/environments**: 提供单设备/多设备对象
472
492
 
473
- - **sendcmd(cmd, timeout=None, env_vars=None, cwd=None, ignore_err=False, only_stdout=True)**:执行命令(非交互式)
493
+ - **sendcmd(cmd, timeout=None, env_vars=None, cwd=None, ignore_err=False, only_stdout=True, echo_output=True)**:在持久化交互 shell channel 上执行命令
474
494
  - `cmd`: 要执行的命令字符串
475
- - `timeout`: 命令执行超时时间(秒)
476
- - `env_vars`: 环境变量字典
495
+ - `timeout`: 命令执行超时时间(秒),支持 int 或 str,默认 600s
496
+ - `env_vars`: 环境变量字典,内联为单命令前缀(`VAR=value cmd`),仅对本次命令生效,不污染持久化 shell
477
497
  - `cwd`: 工作目录,命令将在该目录下执行(可选)
478
498
  - `ignore_err`: 是否忽略命令执行错误,默认 False。设为 True 时即使命令失败 success 也为 True
479
499
  - `only_stdout`: 为 True 时只返回 stdout 字符串,为 False 时返回完整字典
500
+ - `echo_output`: 是否打印 sendcmd/recvcmd 日志,默认 True
501
+ - 行为说明: 同一线程内 shell 状态(`cd`、`export`)跨调用持久化;PTY 合并 stderr 到 stdout,返回 dict 的 `stderr` 通常为空;退出码通过 `$?` 哨兵尽力捕获。需要独立进程执行时请用 `sendcmd_oneshot`
480
502
  - 返回值: 默认返回 stdout 字符串;当 `only_stdout=False` 时返回 `{'success': bool, 'stdout': str, 'stderr': str}`
481
503
 
482
- - **sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True)**:执行命令(交互式)
483
- - `cmd`: 要执行的命令字符串
484
- - `expect_prompt`: 期望匹配的提示符正则表达式(可选)
485
- - `timeout`: 命令执行超时时间(秒)。默认值600s
486
- - `cwd`: 工作目录,命令将在该目录下执行(可选)
487
- - `ignore_err`: 是否忽略提示符匹配失败,默认 False。设为 True 时即使未匹配到提示符 success 也为 True
488
- - `only_stdout`: 为 True 时只返回 stdout 字符串,为 False 时返回完整字典
489
- - 返回值: 默认返回 stdout 字符串;当 `only_stdout=False` 时返回 `{'success': bool, 'stdout': str, 'stderr': str}`
504
+ - **sendcmd_oneshot(cmd, timeout=None, env_vars=None, cwd=None, ignore_err=False, only_stdout=True, echo_output=True)**:独立进程执行命令(原 sendcmd 行为)
505
+ - 每次调用启动独立的 `exec_command` / `subprocess.run` 进程,无持久 shell 状态,与交互 channel 互相独立
506
+ - SSH 路径: 以 `bash -l -c` 包装执行,原生环境变量支持,退出码为 channel 权威退出状态,stderr 为独立流
507
+ - `local_exec` 路径: 通过 `subprocess.run` 执行
508
+ - 参数与返回值与 `sendcmd` 一致(`env_vars` 通过原生环境注入而非内联前缀)
509
+
510
+ - **sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True)**:等同于 `sendcmd`(保留向后兼容)
511
+ - 现直接代理给 `sendcmd`,在持久化交互 shell channel 上执行
512
+ - `expect_prompt`: 保留签名兼容但不再生效(被忽略)
513
+ - 参数与返回值与 `sendcmd` 一致
490
514
  - **输出清理**: 返回的 stdout 会自动清理以下内容,便于用户直接使用:
491
515
  - CSI 控制序列(如 `\x1b[32m` 颜色代码、`\x1b[?2004h` bracketed paste mode 等)
492
516
  - OSC 控制序列(如 `\x1b]0;title\x07` 窗口标题设置等)
@@ -550,8 +574,10 @@ def test_use_ci_metadata():
550
574
  - **DockerProxy 容器代理对象**:通过 `environment["container_name"]` 获取
551
575
  - 代理对象提供与 Device 一致的接口,自动包装 `docker exec` 命令
552
576
 
553
- - **sendcmd(cmd, timeout=None, ignore_err=False, only_stdout=True)**:非交互式命令
554
- - 每次独立执行 `docker exec {docker_name} {cmd}`
577
+ - **sendcmd(cmd, timeout=None, ignore_err=False, only_stdout=True)**:非交互式命令(docker session 感知)
578
+ - 已在该容器交互式 session 中: 直接在当前 session 执行命令
579
+ - 其他容器持有交互式 session: 直接返回失败(提示"请退出上一个交互对话")
580
+ - 否则: 包装为 `docker exec {docker_name} {cmd}` 执行(不创建持久 session)
555
581
  - 返回值: stdout 字符串或完整字典
556
582
 
557
583
  - **sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True)**:交互式命令
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pytest_testkit"
7
- version = "1.0.2b1"
7
+ version = "1.0.3"
8
8
  description = "Pytest plugin for Infra logging framework and environment management"
9
9
  readme = "README.md"
10
10
  license = { file = "LICENSE" }
@@ -30,14 +30,12 @@ classifiers = [
30
30
  ]
31
31
  keywords = ["pytest", "plugin", "logging", "html", "infra", "test", "automation"]
32
32
  dependencies = [
33
- "pytest==8.3.2",
33
+ "pytest>=8.3.2",
34
34
  "requests",
35
35
  "paramiko",
36
36
  "xlrd>=2.0.0",
37
37
  "PyYaml",
38
38
  "scp",
39
- "execnet",
40
- "pybind11",
41
39
  ]
42
40
 
43
41
  [project.optional-dependencies]
@@ -58,7 +58,7 @@ class Sftp:
58
58
  hostname=self.jump_ip,
59
59
  port=self.jump_port,
60
60
  username=self.jump_user,
61
- password=self.jump_passwd,
61
+ password=self.target_pwd,
62
62
  pkey=self.private_k,
63
63
  timeout=10,
64
64
  )
@@ -594,10 +594,11 @@ class Device:
594
594
  return True
595
595
 
596
596
  # 登录动作日志: HTML location 列显示 login[设备短名]
597
- logger.info(
598
- f"Logging in to device: {self.name}",
599
- extra={"op_type": f"login[{_short_name(self.name)}]"},
600
- )
597
+ # 改成框架日志,不在此处打印
598
+ # logger.info(
599
+ # f"Logging in to device: {self.name}",
600
+ # extra={"op_type": f"login[{_short_name(self.name)}]"},
601
+ # )
601
602
 
602
603
  # Implement actual SSH connection logic
603
604
  # 由于有跳板机,需要先登录跳板机,再登录目标设备
@@ -617,24 +618,27 @@ class Device:
617
618
  )
618
619
  login_result = self._thread_local.sshConnect.login()
619
620
  if login_result is False:
620
- logger.error(
621
- f"ssh login failed for {self.__str__()}",
622
- extra={"op_type": f"loginresult[{_short_name(self.name)}]"},
623
- )
621
+ # 改成框架日志,不在此处打印 这个是否不用修改
622
+ # logger.error(
623
+ # f"ssh login failed for {self.__str__()}",
624
+ # extra={"op_type": f"loginresult[{_short_name(self.name)}]"},
625
+ # )
624
626
  return False
625
627
  self._set_logged_in(True)
626
- logger.info(
627
- f"Successfully connected to {self.__str__()}",
628
- extra={"op_type": f"loginresult[{_short_name(self.name)}]"},
629
- )
628
+ # 改成框架日志,不在此处打印
629
+ # logger.info(
630
+ # f"Successfully connected to {self.__str__()}",
631
+ # extra={"op_type": f"loginresult[{_short_name(self.name)}]"},
632
+ # )
630
633
  # 登录后立即预创建交互 channel 并吸收 banner,
631
634
  # 保证 loginecho 出现在第一条 sendcmd 之前
632
635
  login_echo = self._thread_local.sshConnect.prepare_interactive_channel()
633
636
  if login_echo:
634
- logger.info(
635
- login_echo,
636
- extra={"op_type": f"loginecho[{_short_name(self.name)}]"},
637
- )
637
+ # 改成框架日志,不在此处打印
638
+ # logger.info(
639
+ # login_echo,
640
+ # extra={"op_type": f"loginecho[{_short_name(self.name)}]"},
641
+ # )
638
642
  self._thread_local.sshConnect._login_echo_logged = True
639
643
  return True
640
644
 
@@ -750,10 +754,11 @@ class Device:
750
754
  if getattr(ssh_connect, "login_echo", "") and not getattr(
751
755
  ssh_connect, "_login_echo_logged", False
752
756
  ):
753
- logger.info(
754
- ssh_connect.login_echo,
755
- extra={"op_type": f"loginecho[{_short_name(self.name)}]"},
756
- )
757
+ # 改成框架日志,不在此处打印
758
+ # logger.info(
759
+ # ssh_connect.login_echo,
760
+ # extra={"op_type": f"loginecho[{_short_name(self.name)}]"},
761
+ # )
757
762
  ssh_connect._login_echo_logged = True
758
763
 
759
764
  param = SshCmdInteractiveParam(
@@ -771,6 +776,8 @@ class Device:
771
776
  if echo_output and not _SESSION_QUIET_STDOUT:
772
777
  node_tag = f"[{_short_name(self.name)}]"
773
778
  prompt = getattr(ssh_connect, "last_prompt", "") if ssh_connect else ""
779
+ # root@lease-env1792052573-5446-nodes1-device1792052573-2811:~#ls -l
780
+ # 打印回显如上 需要带上 shell 提示符吗
774
781
  send_msg = f"{prompt}{cmd}" if prompt else cmd
775
782
  if cwd is not None:
776
783
  send_msg += f" (cwd: {cwd})"
@@ -792,11 +799,11 @@ class Device:
792
799
  new_prompt = (
793
800
  getattr(ssh_connect, "last_prompt", "") if ssh_connect else ""
794
801
  )
795
- recv_msg = (
796
- result_echo
797
- if result_echo.endswith(new_prompt)
798
- else (f"{result_echo}\n{new_prompt}" if new_prompt else result_echo)
799
- )
802
+ # 回显结果以shell提示符结尾移除它, 没有提示符则直接打印不拼装
803
+ if new_prompt and result_echo.endswith(new_prompt):
804
+ recv_msg = result_echo[: -len(new_prompt)].rstrip("\n")
805
+ else:
806
+ recv_msg = result_echo
800
807
  logger.info(recv_msg, extra={"op_type": f"recvcmd{node_tag}"})
801
808
  else:
802
809
  logger.info("(empty)", extra={"op_type": f"recvcmd{node_tag}"})
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest_testkit
3
- Version: 1.0.2b1
3
+ Version: 1.0.3
4
4
  Summary: Pytest plugin for Infra logging framework and environment management
5
5
  Author: Huawei
6
6
  License: MIT License
@@ -43,14 +43,12 @@ Classifier: Operating System :: OS Independent
43
43
  Requires-Python: >=3.6
44
44
  Description-Content-Type: text/markdown
45
45
  License-File: LICENSE
46
- Requires-Dist: pytest==8.3.2
46
+ Requires-Dist: pytest>=8.3.2
47
47
  Requires-Dist: requests
48
48
  Requires-Dist: paramiko
49
49
  Requires-Dist: xlrd>=2.0.0
50
50
  Requires-Dist: PyYaml
51
51
  Requires-Dist: scp
52
- Requires-Dist: execnet
53
- Requires-Dist: pybind11
54
52
  Provides-Extra: dev
55
53
  Requires-Dist: build; extra == "dev"
56
54
  Requires-Dist: twine; extra == "dev"
@@ -126,6 +124,8 @@ docker run -d --name <name> -p 2222:22 <image>:<tag>
126
124
  ```
127
125
  ### logger 使用
128
126
 
127
+ > **⚠️ 安全提醒:该`logger`为 `pytest-testkit` 插件提供的基础日志能力,请勿在测试用例中打印敏感信息(如密码、密钥、Token 等),以免造成信息泄露。**
128
+
129
129
  导入 logger:
130
130
 
131
131
  ```python
@@ -135,7 +135,7 @@ from pytest_testkit.lib.common.log import logger
135
135
  使用 logger 记录日志:
136
136
 
137
137
  ```python
138
- logger.info(f"[Device._login] Successfully connected to device")
138
+ logger.info("Successfully connected to device")
139
139
  ```
140
140
 
141
141
  #### 日志目录结构
@@ -152,12 +152,30 @@ logs/
152
152
 
153
153
  **目录结构说明**:
154
154
  - **TestCases**: 测试用例日志根目录
155
- - **日期目录**: 按执行日期组织(格式:`YYYY-MM-DD`)
156
- - **用例子目录**: 按用例名和执行时间命名(格式:`用例名_HH-MM-SS`)
155
+ - **日期目录**: 按执行日期组织(格式:`YYYY-MM-DD`,使用本机时区)
156
+ - **用例子目录**: 按用例名和执行时间命名(格式:`用例名_HH-MM-SS`,使用本机时区)
157
157
  - **HTML 文件**: 插件生成的测试报告,文件名与用例名一致
158
158
 
159
159
  通过 `logger.path` 属性可以获取当前用例的日志归档目录路径,方便在测试用例中创建自定义文件。
160
160
 
161
+ #### HTML 报告渲染
162
+
163
+ > **⚠️ 安全提醒:HTML 报告会完整渲染日志中记录的命令文本、回显输出及登录 banner 等内容,请勿在命令或日志中携带敏感信息(如密码、密钥、Token 等),以免在报告中明文暴露。**
164
+
165
+ 测试用例的 HTML 日志报告支持以下渲染特性:
166
+
167
+ - **op_type 标签**: 命令与连接日志通过 `logging` 的 `extra={"op_type": ...}` 标记操作类型,HTML 报告 Location 列显示该标签(未标记记录回退为 `文件名:行号`)。内置标签包括:
168
+ - `sendcmd[<设备短名>]`: 发送命令,消息带最近 shell 提示符前缀(如 `root@host:~#ls -la`),设置 `cwd` 时追加 `(cwd: ...)`
169
+ - `recvcmd[<设备短名>]`: 命令文本回显(stdout+stderr 合并,不打印 dict),末尾带最新提示符;无输出时显示 `(empty)`
170
+ - `login[<设备短名>]` / `loginresult[<设备短名>]`: 登录开始与结果(登录失败为 ERROR 级别)
171
+ - `loginecho[<设备短名>]`: 登录 banner(交互 channel 首次创建时吸收,在第一条 sendcmd 之前打印)
172
+ - `disconnect[<设备短名>]`: 连接关闭
173
+ - `<设备短名>` 取设备名最后一段(按 `-` 或 `/` 分割),如 `rack1/node_3` → `node_3`
174
+ - **ANSI 颜色渲染**: 报告自动把 ANSI 前景色码(30-37、90-97)转换为带颜色的 `<span>` 标签。例如命令日志中的蓝色码(`\x1b[34m`)渲染为蓝色(`#2196F3`)
175
+ - **错误词高亮**: `recvcmd` 回显中的常见错误词(error、failed、fatal、panic、traceback、timeout、"No such file or directory" 等)会以粗体红色显示,便于快速定位失败。命令行(sendcmd)与普通日志不高亮,避免误伤命令本身
176
+ - **列宽调整**: Level 列 70px、Thread 列 50px、Location 列 128px,Message 列获得更多空间
177
+ - **本地时间**: 会话目录、日期目录、用例子目录均使用本机时区时间,与报告表内日志时间戳一致
178
+
161
179
  日志级别:
162
180
 
163
181
  | 级别 | 值 | 方法 | 说明 |
@@ -411,9 +429,9 @@ def test_environment(logger):
411
429
 
412
430
  ### quiet_stdout 功能
413
431
 
414
- pytest-testkit 插件支持全局静默 `sendcmd` / `sendcmd_interactive` 的 stdout 输出,避免大量回显淹没日志。
415
- **开启后,stdout 内容不会出现在日志中,即使调用方显式传 `echo_output=True` 也会被全局压制。**
416
- exit_code、prompt、stderr 等信息不受影响。
432
+ pytest-testkit 插件支持全局静默 `sendcmd` / `sendcmd_oneshot` / `sendcmd_interactive` 的命令与回显日志,避免大量回显淹没日志。
433
+ **开启后,sendcmd / recvcmd 日志都不会打印,即使调用方显式传 `echo_output=True` 也会被全局压制。**
434
+ 命令返回值(stdout / stderr / exit_code 等)不受影响,仅影响日志输出。
417
435
 
418
436
  **支持CLI 参数 和 ini 配置,默认为禁用**
419
437
 
@@ -528,23 +546,27 @@ def test_use_ci_metadata():
528
546
 
529
547
  - **environment/environments**: 提供单设备/多设备对象
530
548
 
531
- - **sendcmd(cmd, timeout=None, env_vars=None, cwd=None, ignore_err=False, only_stdout=True)**:执行命令(非交互式)
549
+ - **sendcmd(cmd, timeout=None, env_vars=None, cwd=None, ignore_err=False, only_stdout=True, echo_output=True)**:在持久化交互 shell channel 上执行命令
532
550
  - `cmd`: 要执行的命令字符串
533
- - `timeout`: 命令执行超时时间(秒)
534
- - `env_vars`: 环境变量字典
551
+ - `timeout`: 命令执行超时时间(秒),支持 int 或 str,默认 600s
552
+ - `env_vars`: 环境变量字典,内联为单命令前缀(`VAR=value cmd`),仅对本次命令生效,不污染持久化 shell
535
553
  - `cwd`: 工作目录,命令将在该目录下执行(可选)
536
554
  - `ignore_err`: 是否忽略命令执行错误,默认 False。设为 True 时即使命令失败 success 也为 True
537
555
  - `only_stdout`: 为 True 时只返回 stdout 字符串,为 False 时返回完整字典
556
+ - `echo_output`: 是否打印 sendcmd/recvcmd 日志,默认 True
557
+ - 行为说明: 同一线程内 shell 状态(`cd`、`export`)跨调用持久化;PTY 合并 stderr 到 stdout,返回 dict 的 `stderr` 通常为空;退出码通过 `$?` 哨兵尽力捕获。需要独立进程执行时请用 `sendcmd_oneshot`
538
558
  - 返回值: 默认返回 stdout 字符串;当 `only_stdout=False` 时返回 `{'success': bool, 'stdout': str, 'stderr': str}`
539
559
 
540
- - **sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True)**:执行命令(交互式)
541
- - `cmd`: 要执行的命令字符串
542
- - `expect_prompt`: 期望匹配的提示符正则表达式(可选)
543
- - `timeout`: 命令执行超时时间(秒)。默认值600s
544
- - `cwd`: 工作目录,命令将在该目录下执行(可选)
545
- - `ignore_err`: 是否忽略提示符匹配失败,默认 False。设为 True 时即使未匹配到提示符 success 也为 True
546
- - `only_stdout`: 为 True 时只返回 stdout 字符串,为 False 时返回完整字典
547
- - 返回值: 默认返回 stdout 字符串;当 `only_stdout=False` 时返回 `{'success': bool, 'stdout': str, 'stderr': str}`
560
+ - **sendcmd_oneshot(cmd, timeout=None, env_vars=None, cwd=None, ignore_err=False, only_stdout=True, echo_output=True)**:独立进程执行命令(原 sendcmd 行为)
561
+ - 每次调用启动独立的 `exec_command` / `subprocess.run` 进程,无持久 shell 状态,与交互 channel 互相独立
562
+ - SSH 路径: 以 `bash -l -c` 包装执行,原生环境变量支持,退出码为 channel 权威退出状态,stderr 为独立流
563
+ - `local_exec` 路径: 通过 `subprocess.run` 执行
564
+ - 参数与返回值与 `sendcmd` 一致(`env_vars` 通过原生环境注入而非内联前缀)
565
+
566
+ - **sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True)**:等同于 `sendcmd`(保留向后兼容)
567
+ - 现直接代理给 `sendcmd`,在持久化交互 shell channel 上执行
568
+ - `expect_prompt`: 保留签名兼容但不再生效(被忽略)
569
+ - 参数与返回值与 `sendcmd` 一致
548
570
  - **输出清理**: 返回的 stdout 会自动清理以下内容,便于用户直接使用:
549
571
  - CSI 控制序列(如 `\x1b[32m` 颜色代码、`\x1b[?2004h` bracketed paste mode 等)
550
572
  - OSC 控制序列(如 `\x1b]0;title\x07` 窗口标题设置等)
@@ -608,8 +630,10 @@ def test_use_ci_metadata():
608
630
  - **DockerProxy 容器代理对象**:通过 `environment["container_name"]` 获取
609
631
  - 代理对象提供与 Device 一致的接口,自动包装 `docker exec` 命令
610
632
 
611
- - **sendcmd(cmd, timeout=None, ignore_err=False, only_stdout=True)**:非交互式命令
612
- - 每次独立执行 `docker exec {docker_name} {cmd}`
633
+ - **sendcmd(cmd, timeout=None, ignore_err=False, only_stdout=True)**:非交互式命令(docker session 感知)
634
+ - 已在该容器交互式 session 中: 直接在当前 session 执行命令
635
+ - 其他容器持有交互式 session: 直接返回失败(提示"请退出上一个交互对话")
636
+ - 否则: 包装为 `docker exec {docker_name} {cmd}` 执行(不创建持久 session)
613
637
  - 返回值: stdout 字符串或完整字典
614
638
 
615
639
  - **sendcmd_interactive(cmd, expect_prompt=None, timeout=600, cwd=None, ignore_err=False, only_stdout=True)**:交互式命令
@@ -15,7 +15,6 @@ pytest_testkit/lib/__init__.py
15
15
  pytest_testkit/lib/base/__init__.py
16
16
  pytest_testkit/lib/base/excel.py
17
17
  pytest_testkit/lib/base/ftp.py
18
- pytest_testkit/lib/base/http.py
19
18
  pytest_testkit/lib/base/local_exec.py
20
19
  pytest_testkit/lib/base/sftp.py
21
20
  pytest_testkit/lib/base/ssh.py
@@ -1,11 +1,9 @@
1
- pytest==8.3.2
1
+ pytest>=8.3.2
2
2
  requests
3
3
  paramiko
4
4
  xlrd>=2.0.0
5
5
  PyYaml
6
6
  scp
7
- execnet
8
- pybind11
9
7
 
10
8
  [dev]
11
9
  build
@@ -1,9 +1,7 @@
1
- pytest==8.3.2
1
+ pytest>=8.3.2
2
2
  pytest-timeout>=2.3.1,<3.0
3
3
  requests
4
- paramiko
4
+ paramiko>=3.5.1
5
5
  xlrd>=2.0.0
6
6
  PyYaml
7
7
  scp
8
- execnet
9
- pybind11
@@ -1,431 +0,0 @@
1
- # Copyright (C) 2024 Intel Corporation
2
- # SPDX-License-Identifier: MPL-2.0
3
-
4
- import json
5
- import requests
6
- from pytest_testkit.lib.common import log
7
-
8
- HTTP_METHOD_GET = "GET"
9
- HTTP_METHOD_PUT = "PUT"
10
- HTTP_METHOD_POST = "POST"
11
- HTTP_METHOD_PATCH = "PATCH"
12
- HTTP_METHOD_DELETE = "DELETE"
13
- HTTP_METHOD_HEAD = "HEAD"
14
-
15
-
16
- class Http:
17
- """Base class of Restful"""
18
-
19
- # 1xx: Informational
20
- # 2xx: Success
21
- # 3xx: Redirection
22
- # 4xx: Client Error
23
- # 5xx: Server Error
24
- HttpErrorCode = {
25
- 200: "OK. The request has succeeded.",
26
- 201: "Created. The request has been fulfilled and resulted in a new "
27
- "resource being created.",
28
- 202: "Accepted as an async process",
29
- 204: "OK no content returned",
30
- 400: "Bad Request. Badly formed URI, parameters, headers, or body content.",
31
- 401: "Unauthorized. The request requires user authentication.",
32
- 403: "Forbidden. The request was valid, but the server is refusing action.",
33
- 404: "Not Found. Resource doesn't exist.",
34
- 405: "Method Not Allowed.",
35
- 406: "Not Acceptable. Accept headers are not satisfiable.",
36
- 409: "Conflict. The request could not be completed due to a conflict "
37
- "with the current state of the resource.",
38
- 412: "Precondition Failed. The precondition given in the "
39
- "request-header fields evaluated to false on the server.",
40
- 422: "Unprocessable Entity, Semantically invalid content on a POST.",
41
- 500: "Internal Server Error.",
42
- 503: "Service Unavailable.",
43
- }
44
-
45
- def __init__(self, max_retry=0):
46
- self.logger = log.get_logger(str(self.__module__))
47
- # pgxun: 此赋值用于指定对外公开哪些函数。
48
- self.method_release_requirement = []
49
- self.session = requests.Session()
50
-
51
- # 设置HTTP命令重试的次数。0的缺省值表示不更改HTTP模块的重试次数
52
- # (requests模块的缺省值是:0,不重试)。
53
- """
54
- max_retries: The maximum number of retries each connection
55
- should attempt. Note, this applies only to failed DNS lookups, socket
56
- connections and connection timeouts, never to requests where data has
57
- made it to the server. By default, Requests does not retry failed
58
- connections.
59
- """
60
- if max_retry != 0:
61
- retry_counts = requests.adapters.HTTPAdapter(max_retries=max_retry)
62
- self.session.mount("http://", retry_counts)
63
- self.session.mount("https://", retry_counts)
64
-
65
- def __convert_json_list_to_table(self, json_list):
66
- """Convert json list to table"""
67
-
68
- # calculate column width
69
- headers = self.__calculate_column_width(json_list)
70
- text_string = "-------------- Title --------------\n"
71
- pattern = "{0:{width}s}"
72
-
73
- # format headers
74
- for name, width in headers.items():
75
- text_string = text_string + pattern.format(name, width=width)
76
-
77
- text_string = text_string + "\n"
78
- for _, width in headers.items():
79
- split = "-" * (
80
- width - 4
81
- ) # 4 space char, the same to __calculateColumnWidth
82
- text_string = text_string + pattern.format(split, width=width)
83
-
84
- # format values
85
- for item in json_list:
86
- text_string = text_string + "\n"
87
- for name, width in headers.items():
88
- text_string = text_string + pattern.format(
89
- str(item.get(name)), width=width
90
- )
91
-
92
- return text_string + "\n"
93
-
94
- @staticmethod
95
- def __calculate_column_width(json_list):
96
- """Calculate table column width according to json's key-value"""
97
-
98
- headers = {}
99
- if not isinstance(json_list, list):
100
- return headers
101
-
102
- # at least 5 chars for each column, in the commonParser, he set
103
- # splitter must has 5 '-'s, i don't why
104
- min_header_with = 5
105
- fixed_space_width = 4 # add 4 space by default
106
- for js in json_list:
107
- for key, value in js.items():
108
- value_length = len(str(value)) + fixed_space_width
109
- if key in headers.keys():
110
- headers[key] = max(value_length, headers[key])
111
- else:
112
- key_length = len(str(key)) + fixed_space_width
113
- headers[key] = max(
114
- key_length, value_length, min_header_with + fixed_space_width
115
- )
116
-
117
- return headers
118
-
119
- @staticmethod
120
- def __convert_dict_items_to_json(params):
121
- """Convert dict type parameters in given Http parameters to json type"""
122
- for key, value in params.items():
123
- if key.lower() not in ["data", "params"]:
124
- continue
125
-
126
- if isinstance(value, dict):
127
- params[key] = json.dumps(value)
128
- return params
129
-
130
- def __log_request(self, method=None, url=None, **params):
131
- """log out user's request parameters"""
132
- self.logger.info(
133
- "-----> Http request method: %s, url: %s <-----" % (method, url)
134
- )
135
- if len(str(params["data"])) > 100:
136
- self.logger.info(
137
- "Request params(data too long, only headers shown): %s"
138
- % params["headers"]
139
- )
140
- else:
141
- self.logger.info("Request params: %s" % params)
142
-
143
- def __handle_http_error_code(self, status_code):
144
- """Return error message according to Http status code"""
145
- status_code = int(status_code)
146
- if self.HttpErrorCode.has_key(status_code):
147
- return self.HttpErrorCode[status_code]
148
- else:
149
- return "<Error Info Absent>."
150
-
151
- def __log_response(self, response):
152
- """log out response message according to response status code
153
- pgxun: 打印返回消息的日志。
154
- """
155
-
156
- # 1xx: Informational
157
- # 2xx: Success
158
- # 3xx: Redirection
159
- # 4xx: Client Error
160
- # 5xx: Server Error
161
-
162
- if not response.ok:
163
- msg = "Response error. Status code: %s, elapsed: %s, message: %s" % (
164
- response.status_code,
165
- response.elapsed,
166
- self.__handle_http_error_code(response.status_code),
167
- )
168
- self.logger.warning(str(msg))
169
- else:
170
- self.logger.info(
171
- "-----> Http response success, status code: "
172
- + str(response.status_code)
173
- + " <-----"
174
- )
175
-
176
- # 规避一个下载文件接口的问题
177
- if response.text.find("{") < 0:
178
- msg = "Response elapsed: %s, message: %s" % (
179
- response.elapsed,
180
- "This interface is used to download file, no json response data",
181
- )
182
- else:
183
- # 说明:如果转换为dict再打印,会使得打印的key排序和得到的响应的原始顺序
184
- # 不一样,导致用户观察结果不方便。
185
- msg = "Response elapsed: {}, message: \n{}".format(
186
- response.elapsed, response.text
187
- )
188
-
189
- # 部分告警信息中有中文句号,要特殊处理一下
190
- self.logger.info(str(msg))
191
-
192
- def convert_json_to_table(self, json_obj):
193
- """Convert json object to table text string
194
-
195
- Args:
196
- jsonList (list) : Json list to be convert
197
-
198
- Returns: A pretty formatted table text string.
199
-
200
- Raises:
201
- InvalidParamException
202
-
203
- Examples:
204
- In:
205
- [{u'COUNT': u'19034', u'ISONLINE': u'1',
206
- u'DESCRIPTION': u'Default user', u'LEVEL': u'1',
207
- u'LOCKSTATUS': u'0', u'CREATETIME': u'1440243958',
208
- u'SCOPE': u'0', u'TYPE': 202, u'ID': u'admin',
209
- u'NAME': u'admin'}]
210
- Out:
211
- -------------- Title --------------
212
- COUNT ISONLINE DESCRIPTION LOCKSTATUS LEVEL ID SCOPE TYPE CREATETIME NAME
213
- ----- -------- ------------ ---------- ----- ----- ----- ---- ---------- -----
214
- 19083 1 Default user 0 1 admin 0 202 1440243958 admin
215
-
216
- """
217
-
218
- json_list = json_obj
219
- if isinstance(json_obj, dict):
220
- json_list = [json_obj]
221
-
222
- # json is an array, means it contains many tables
223
- if len(json_list) > 0:
224
- if not all(isinstance(x, dict) for x in json_list):
225
- self.logger.warning(
226
- "Can not deal with json object which contains array, "
227
- "contact us please. data:" + json
228
- )
229
- return []
230
- return self.__convert_json_list_to_table(json_list)
231
- else:
232
- return []
233
-
234
- def encode_json_to_utf8(self, unicode_json):
235
- """Convert unicode json encoding to utf-8"""
236
-
237
- if isinstance(unicode_json, dict):
238
- return {
239
- self.encode_json_to_utf8(key): self.encode_json_to_utf8(value)
240
- for key, value in unicode_json.iteritems()
241
- }
242
- elif isinstance(unicode_json, list):
243
- return [self.encode_json_to_utf8(item) for item in unicode_json]
244
- elif isinstance(unicode_json, unicode):
245
- # deal with special character in the response
246
- return (
247
- unicode_json
248
- .encode("utf-8")
249
- .replace("&#40;", "(")
250
- .replace("&#41;", ")")
251
- .replace("&gt;", ">")
252
- .replace("&lt;", "<")
253
- .replace("&amp;", "&")
254
- .replace("\r", "")
255
- .replace("\n", "")
256
- )
257
- else:
258
- return unicode_json
259
-
260
- """
261
- pgxun说明:
262
- requests的request函数定义如下:
263
- requests.request(method, url, **kwargs)[source]
264
- method:是HTTP动作的名字,如GET、PUT等。
265
- url -- URL for the new Request object.
266
- kwargs -- (optional) Dictionary or bytes to be sent in the query string for the Request.
267
- kwargs包括以下12个可选的内容,常用的有:
268
- data -- 这是请求的body部分,可以是:Dict, bytes, or file-like object to send in the body of the Request.
269
- json -- 这是请求的body部分,JSON格式。json data to send in the body of the Request.
270
- headers -- 这是请求的Headers部分,dict格式。 Dictionary of HTTP Headers to send with the Request.
271
- 等等
272
- 返回值:request命令的响应response。
273
- 注意:
274
- 1、你也可以不使用params,而是直接将12个可选内容写在request的调用参数列表中。
275
- 2、params中可以直接写入"key=value"的字段格式,如果不是12个可选的内容,则会作为url的参数,例如:
276
- payload = {'key1': 'value1', 'key2': 'value2'}
277
-
278
- 3、params传入的示例:
279
-
280
- """
281
-
282
- def request(self, method, url, **kwargs):
283
- """Make a Http request according to given method
284
-
285
- Args:
286
- method (str) : Http method, values: get/post/put/delete
287
- url: (str) : Url for the request
288
- kwargs: (dict) : (Optional). Parameters see bellow
289
-
290
- Field Type Optional Description
291
- ==============================================================================
292
- 其它非12个关键字 dict True Dictionary to be sent in the query string for the request.
293
- data dict True Dictionary to send in the body of the request.
294
- headers dict True Dictionary of HTTP Headers to send with the request.
295
- timeout float
296
-
297
- def request(self, method, url,
298
- params=None, data=None, headers=None, cookies=None, files=None,
299
- auth=None, timeout=None, allow_redirects=True, proxies=None,
300
- hooks=None, stream=None, verify=None, cert=None, json=None):
301
- :param method: method for the new :class:`Request` object.
302
- :param url: URL for the new :class:`Request` object.
303
- :param params: (optional) Dictionary or bytes to be sent in the query
304
- string for the :class:`Request`.
305
- :param data: (optional) Dictionary, bytes, or file-like object to send
306
- in the body of the :class:`Request`.
307
- :param json: (optional) json to send in the body of the
308
- :class:`Request`.
309
- :param headers: (optional) Dictionary of HTTP Headers to send with the
310
- :class:`Request`.
311
- :param cookies: (optional) Dict or CookieJar object to send with the
312
- :class:`Request`.
313
- :param files: (optional) Dictionary of ``'filename': file-like-objects``
314
- for multipart encoding upload.
315
- :param auth: (optional) Auth tuple or callable to enable
316
- Basic/Digest/Custom HTTP Auth.
317
- :param timeout: (optional) How long to wait for the server to send
318
- data before giving up, as a float, or a :ref:`(connect timeout,
319
- read timeout) <timeouts>` tuple.
320
- :type timeout: float or tuple
321
- :param allow_redirects: (optional) Set to True by default.
322
- :type allow_redirects: bool
323
- :param proxies: (optional) Dictionary mapping protocol or protocol and
324
- hostname to the URL of the proxy.
325
- :param stream: (optional) whether to immediately download the response
326
- content. Defaults to ``False``.
327
- :param verify: (optional) Either a boolean, in which case it controls whether we verify
328
- the server's TLS certificate, or a string, in which case it must be a path
329
- to a CA bundle to use. Defaults to ``True``.
330
- :param cert: (optional) if String, path to ssl client cert file (.pem).
331
- If Tuple, ('cert', 'key') pair.
332
- :rtype: requests.Response
333
-
334
- Returns:
335
- Http response (Response) : A response object.
336
- Usage:
337
- r.ok (bool) : Whether the request is ok. Return True/False
338
- r.status_code (int) : Http response code. Int
339
- r.elapsed (time) : Request used time. e.g. 0:00:01.662000
340
- r.json() (json) : Response content as json format
341
- r.text (str) : Response content as text string format
342
-
343
- Raises:
344
- None
345
-
346
- Examples:
347
-
348
- """
349
- # 请注意在参数中指定Timout参数,否则可能会等很久才会异常结束。
350
- kwargs["verify"] = False
351
- # 下面的函数将每个字段的值都转化成了json格式。
352
- kwargs = self.__convert_dict_items_to_json(kwargs)
353
- method = str(method).lower()
354
-
355
- def send_request():
356
- self.__log_request(method, url, **kwargs)
357
-
358
- if method == "get":
359
- r = self.session.get(url, **kwargs)
360
- elif method == "post":
361
- r = self.session.post(url, **kwargs)
362
- elif method == "put":
363
- r = self.session.put(url, **kwargs)
364
- elif method == "delete":
365
- r = self.session.delete(url, **kwargs)
366
- elif method == "patch":
367
- r = self.session.patch(url, **kwargs)
368
- elif method == "head":
369
- r = self.session.head(url, **kwargs)
370
- else:
371
- self.logger.warning("method not implemented: " + method)
372
- return None
373
-
374
- self.__log_response(r)
375
- return r
376
-
377
- try:
378
- result = send_request()
379
- except Exception as err:
380
- self.logger.warning("Send request failed: ", err)
381
-
382
- return result
383
-
384
- def get(self, url, **kwargs):
385
- return self.request("get", url, kwargs)
386
-
387
- def put(self, url, **kwargs):
388
- return self.request("put", url, kwargs)
389
-
390
- def post(self, url, **kwargs):
391
- return self.request("post", url, kwargs)
392
-
393
- def patch(self, url, **kwargs):
394
- return self.request("patch", url, kwargs)
395
-
396
- def delete(self, url, **kwargs):
397
- return self.request("delete", url, kwargs)
398
-
399
- def head(self, url, **kwargs):
400
- return self.request("head", url, kwargs)
401
-
402
- def get_headers(self):
403
- """Get restful request headers"""
404
- return self.session.headers
405
-
406
- def update_headers(self, params):
407
- """Update restful request headers
408
-
409
- Args:
410
- params (dict) : Http header options.
411
- More information please see
412
-
413
-
414
- E.g.
415
- Field Type Optional Description
416
- ==============================================================================
417
- accept-encoding str True List of acceptable encodings. See HTTP compression
418
- content-type str True The MIME type of body of the request. E.g.application/json
419
-
420
- Returns:
421
- None
422
-
423
- Raises:
424
- None
425
-
426
- Examples:
427
- restSession.updateHeaders({'content-type': 'application/json'})
428
-
429
- """
430
-
431
- self.session.headers.update(params)
File without changes