remote-cmd-manager 2.6.0__tar.gz → 2.8.0__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 (101) hide show
  1. {remote_cmd_manager-2.6.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.8.0}/PKG-INFO +25 -18
  2. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/README.md +24 -17
  3. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/pyproject.toml +4 -0
  4. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/__init__.py +15 -8
  5. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/_version.py +1 -1
  6. remote_cmd_manager-2.8.0/remote_cmd/cli/formatters/__init__.py +27 -0
  7. remote_cmd_manager-2.8.0/remote_cmd/cli/formatters/base.py +71 -0
  8. remote_cmd_manager-2.8.0/remote_cmd/cli/formatters/json.py +82 -0
  9. remote_cmd_manager-2.8.0/remote_cmd/cli/formatters/table.py +84 -0
  10. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/cli/main.py +226 -14
  11. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/async_connection_pool.py +3 -1
  12. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/budget.py +116 -44
  13. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/host.py +3 -0
  14. remote_cmd_manager-2.8.0/remote_cmd/core/profile.py +114 -0
  15. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/sync_connection_pool.py +3 -1
  16. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/json_host_repository.py +48 -4
  17. remote_cmd_manager-2.8.0/remote_cmd/repository/profile_store.py +48 -0
  18. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/sqlite_host_repository.py +134 -4
  19. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/_host_runner.py +10 -1
  20. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/credential_provider.py +26 -3
  21. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/host_service.py +92 -11
  22. remote_cmd_manager-2.8.0/remote_cmd/service/profile_service.py +123 -0
  23. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/crypto.py +7 -2
  24. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0/remote_cmd_manager.egg-info}/PKG-INFO +25 -18
  25. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/SOURCES.txt +15 -1
  26. remote_cmd_manager-2.8.0/tests/performance/test_paramiko_threads.py +174 -0
  27. remote_cmd_manager-2.8.0/tests/test_cli_profile.py +262 -0
  28. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_connection_budget.py +80 -0
  29. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_credential_provider.py +69 -0
  30. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_crypto.py +28 -0
  31. remote_cmd_manager-2.8.0/tests/test_formatters.py +149 -0
  32. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_host_service.py +20 -1
  33. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_keyring_provider.py +5 -3
  34. remote_cmd_manager-2.8.0/tests/test_package_import.py +59 -0
  35. remote_cmd_manager-2.8.0/tests/test_profile.py +72 -0
  36. remote_cmd_manager-2.8.0/tests/test_profile_merge.py +196 -0
  37. remote_cmd_manager-2.8.0/tests/test_profile_service.py +149 -0
  38. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_sqlite_repository.py +42 -0
  39. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_ssh_client.py +63 -0
  40. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/LICENSE +0 -0
  41. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/MANIFEST.in +0 -0
  42. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/config.example.yaml +0 -0
  43. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/examples/basic_usage.py +0 -0
  44. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/examples/deploy_script.py +0 -0
  45. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/examples/nginx_batch_update.py +0 -0
  46. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/examples/system_health_check.py +0 -0
  47. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/api/__init__.py +0 -0
  48. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/api/host_manager.py +0 -0
  49. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/cli/__init__.py +0 -0
  50. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/__init__.py +0 -0
  51. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/async_ssh_client.py +0 -0
  52. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/host_manager.py +0 -0
  53. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/pool_policy.py +0 -0
  54. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/ssh_client.py +0 -0
  55. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/__init__.py +0 -0
  56. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/host_repository.py +0 -0
  57. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/__init__.py +0 -0
  58. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/_pool_policy.py +0 -0
  59. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/_types.py +0 -0
  60. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/async_batch_executor.py +0 -0
  61. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/batch_executor.py +0 -0
  62. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/retry_policy.py +0 -0
  63. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/ssh_service.py +0 -0
  64. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/storage_factory.py +0 -0
  65. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/task_runner.py +0 -0
  66. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/__init__.py +0 -0
  67. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/config.py +0 -0
  68. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/credential_guard.py +0 -0
  69. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/exceptions.py +0 -0
  70. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/logging_utils.py +0 -0
  71. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
  72. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
  73. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/requires.txt +0 -0
  74. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
  75. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/requirements.txt +0 -0
  76. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/setup.cfg +0 -0
  77. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/__init__.py +0 -0
  78. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/conftest.py +0 -0
  79. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/integration/conftest.py +0 -0
  80. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/integration/test_ssh_connection.py +0 -0
  81. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/performance/__init__.py +0 -0
  82. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/performance/conftest.py +0 -0
  83. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/performance/test_benchmarks.py +0 -0
  84. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_async_batch_executor.py +0 -0
  85. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_async_ssh_client.py +0 -0
  86. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_batch_executor.py +0 -0
  87. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_cli.py +0 -0
  88. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_config.py +0 -0
  89. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_exceptions.py +0 -0
  90. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_host.py +0 -0
  91. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_host_manager.py +0 -0
  92. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_logging_utils.py +0 -0
  93. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_output_policy.py +0 -0
  94. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_pool_policy.py +0 -0
  95. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_release_metadata.py +0 -0
  96. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_repository.py +0 -0
  97. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_retry_policy.py +0 -0
  98. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_ssh_service.py +0 -0
  99. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_storage_factory.py +0 -0
  100. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_sync_connection_pool.py +0 -0
  101. {remote_cmd_manager-2.6.0 → remote_cmd_manager-2.8.0}/tests/test_task_runner.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remote_cmd_manager
3
- Version: 2.6.0
3
+ Version: 2.8.0
4
4
  Summary: Cross-platform SSH remote server management tool (Linux/macOS/Windows) — lightweight, zero-configuration CLI and Python API for managing servers over SSH. Supports host CRUD, batch commands, file transfer, credential encryption, async execution, and more.
5
5
  Author-email: Vae-Scrooge <vaescrooge@gmail.com>
6
6
  License: MIT
@@ -89,32 +89,37 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
89
89
 
90
90
  ---
91
91
 
92
- ## v2.6.0 Release Highlights
92
+ ## v2.8.0 Release Highlights
93
93
 
94
- v2.6.0 is an architecture and runtime-scalability release: the compatibility facade
95
- moved out of `core` (eliminating the `core → service` dependency) and a process-wide
96
- live-connection budget was added for fleet-scale resource control. Public APIs remain
97
- backward compatible. See the [full migration notes](./CHANGELOG.md).
94
+ v2.8.0 adds the first P3 user-facing features: **connection profiles** and
95
+ **machine-readable CLI output**. Public APIs remain backward compatible; the
96
+ default CLI output and all existing commands behave exactly as before. See the
97
+ [full migration notes](./CHANGELOG.md).
98
98
 
99
- ### Architecture
99
+ ### Connection Profiles
100
100
 
101
- - `HostManager` now lives in `remote_cmd.api.host_manager`; `remote_cmd.core.host_manager` remains a re-export shim, so existing imports keep working.
102
- - Pool policy helpers moved to `remote_cmd.core.pool_policy` (shim kept at the old path); `core` no longer imports `service`.
103
- - The async kernel's worker queue now converts unexpected per-host errors into failure results, so a single internal error can no longer stall an entire batch.
101
+ - New `HostProfile` (exported from `remote_cmd`): reusable connection defaults — username, port, key file, tags, description — stored by both JSON and SQLite repositories via the optional `ProfileStore` capability (the `HostRepository` ABC is unchanged).
102
+ - Hosts reference a profile (`host add --profile aws`); values merge at connect time (reference model): port when the host uses the default 22, key file when unset, description when empty, tags as a union, username when the host stores none (live default — later profile username changes propagate).
103
+ - Changing a profile applies to every referencing host; unknown profiles fail with a clear `ConfigError` (batch execution reports them per host).
104
+ - `ProfileService` CRUD + `remote-cmd profile add/list/show/remove`, with deletion protection for profiles still referenced by hosts.
104
105
 
105
- ### Global Connection Budget
106
+ ### Machine-Readable Output
106
107
 
107
- - New `ConnectionBudget` (exported from `remote_cmd`): caps **live** SSH connections process-wide across pools and executors — idle pooled connections count, and slots release on close/cleanup/`close_all`.
108
- - One instance can be shared by both kernels via `connection_budget=` on `BatchExecutor`, `AsyncBatchExecutor`, `SyncConnectionPool`, and `AsyncConnectionPool`.
109
- - Optional `acquire_timeout` raises the transient `BudgetTimeoutError`; the default (`None`) keeps unlimited behavior.
108
+ - `remote-cmd run` and `remote-cmd batch-run` accept `--format rich|json|table` (default `rich` = byte-compatible with previous output).
109
+ - JSON uses a stable schema with sorted result keys; table output is plain aligned text. Machine formats suppress the progress bar/header so stdout stays parseable.
110
+ - The formatter layer lives in `remote_cmd/cli/formatters/` and never touches the execution kernels.
110
111
 
111
- ### Compatibility
112
+ ### Try It
112
113
 
113
- - All existing imports and constructor signatures keep working; new parameters are optional and default to previous behavior.
114
+ ```bash
115
+ remote-cmd profile add aws -u ec2-user -k ~/.ssh/aws.pem -t cloud
116
+ remote-cmd host add web-01 10.0.0.10 --profile aws
117
+ remote-cmd batch-run web-01 web-02 "uptime" --format json | jq .
118
+ ```
114
119
 
115
120
  ## Table of Contents
116
121
 
117
- - [v2.6.0 Release Highlights](#v260-release-highlights)
122
+ - [v2.8.0 Release Highlights](#v280-release-highlights)
118
123
  - [Why Remote CMD?](#why-remote-cmd)
119
124
  - [Quick Start](#quick-start)
120
125
  - [Use Cases](#use-cases)
@@ -261,7 +266,9 @@ with SSHClient(config) as client:
261
266
  | **Commands** | Single, multi-line, sudo with password |
262
267
  | **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
263
268
  | **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
264
- | **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`) |
269
+ | **Connection Profiles** | Reusable username/port/key/tag defaults referenced by hosts (`ProfileService`, `--profile`); never stores credentials |
270
+ | **Output Formats** | `--format rich\|json\|table` for `run` / `batch-run` (stable JSON schema, parseable stdout) |
271
+ | **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`); profile tags participate in filtering |
265
272
  | **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
266
273
  | **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
267
274
  | **Global Connection Budget** | Optional `ConnectionBudget` caps live SSH connections process-wide across pools and executors |
@@ -36,32 +36,37 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
36
36
 
37
37
  ---
38
38
 
39
- ## v2.6.0 Release Highlights
39
+ ## v2.8.0 Release Highlights
40
40
 
41
- v2.6.0 is an architecture and runtime-scalability release: the compatibility facade
42
- moved out of `core` (eliminating the `core → service` dependency) and a process-wide
43
- live-connection budget was added for fleet-scale resource control. Public APIs remain
44
- backward compatible. See the [full migration notes](./CHANGELOG.md).
41
+ v2.8.0 adds the first P3 user-facing features: **connection profiles** and
42
+ **machine-readable CLI output**. Public APIs remain backward compatible; the
43
+ default CLI output and all existing commands behave exactly as before. See the
44
+ [full migration notes](./CHANGELOG.md).
45
45
 
46
- ### Architecture
46
+ ### Connection Profiles
47
47
 
48
- - `HostManager` now lives in `remote_cmd.api.host_manager`; `remote_cmd.core.host_manager` remains a re-export shim, so existing imports keep working.
49
- - Pool policy helpers moved to `remote_cmd.core.pool_policy` (shim kept at the old path); `core` no longer imports `service`.
50
- - The async kernel's worker queue now converts unexpected per-host errors into failure results, so a single internal error can no longer stall an entire batch.
48
+ - New `HostProfile` (exported from `remote_cmd`): reusable connection defaults — username, port, key file, tags, description — stored by both JSON and SQLite repositories via the optional `ProfileStore` capability (the `HostRepository` ABC is unchanged).
49
+ - Hosts reference a profile (`host add --profile aws`); values merge at connect time (reference model): port when the host uses the default 22, key file when unset, description when empty, tags as a union, username when the host stores none (live default — later profile username changes propagate).
50
+ - Changing a profile applies to every referencing host; unknown profiles fail with a clear `ConfigError` (batch execution reports them per host).
51
+ - `ProfileService` CRUD + `remote-cmd profile add/list/show/remove`, with deletion protection for profiles still referenced by hosts.
51
52
 
52
- ### Global Connection Budget
53
+ ### Machine-Readable Output
53
54
 
54
- - New `ConnectionBudget` (exported from `remote_cmd`): caps **live** SSH connections process-wide across pools and executors — idle pooled connections count, and slots release on close/cleanup/`close_all`.
55
- - One instance can be shared by both kernels via `connection_budget=` on `BatchExecutor`, `AsyncBatchExecutor`, `SyncConnectionPool`, and `AsyncConnectionPool`.
56
- - Optional `acquire_timeout` raises the transient `BudgetTimeoutError`; the default (`None`) keeps unlimited behavior.
55
+ - `remote-cmd run` and `remote-cmd batch-run` accept `--format rich|json|table` (default `rich` = byte-compatible with previous output).
56
+ - JSON uses a stable schema with sorted result keys; table output is plain aligned text. Machine formats suppress the progress bar/header so stdout stays parseable.
57
+ - The formatter layer lives in `remote_cmd/cli/formatters/` and never touches the execution kernels.
57
58
 
58
- ### Compatibility
59
+ ### Try It
59
60
 
60
- - All existing imports and constructor signatures keep working; new parameters are optional and default to previous behavior.
61
+ ```bash
62
+ remote-cmd profile add aws -u ec2-user -k ~/.ssh/aws.pem -t cloud
63
+ remote-cmd host add web-01 10.0.0.10 --profile aws
64
+ remote-cmd batch-run web-01 web-02 "uptime" --format json | jq .
65
+ ```
61
66
 
62
67
  ## Table of Contents
63
68
 
64
- - [v2.6.0 Release Highlights](#v260-release-highlights)
69
+ - [v2.8.0 Release Highlights](#v280-release-highlights)
65
70
  - [Why Remote CMD?](#why-remote-cmd)
66
71
  - [Quick Start](#quick-start)
67
72
  - [Use Cases](#use-cases)
@@ -208,7 +213,9 @@ with SSHClient(config) as client:
208
213
  | **Commands** | Single, multi-line, sudo with password |
209
214
  | **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
210
215
  | **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
211
- | **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`) |
216
+ | **Connection Profiles** | Reusable username/port/key/tag defaults referenced by hosts (`ProfileService`, `--profile`); never stores credentials |
217
+ | **Output Formats** | `--format rich\|json\|table` for `run` / `batch-run` (stable JSON schema, parseable stdout) |
218
+ | **Tag System** | Filter hosts by tag (e.g., `production`, `web`, `db`); profile tags participate in filtering |
212
219
  | **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
213
220
  | **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
214
221
  | **Global Connection Budget** | Optional `ConnectionBudget` caps live SSH connections process-wide across pools and executors |
@@ -122,6 +122,10 @@ ignore = [
122
122
  "__init__.py" = ["E402"]
123
123
  # 测试中 pytest fixture 参数未直接引用属于正常模式(如 patch 装饰器触发副作用)
124
124
  "tests/test_async_ssh_client.py" = ["ARG002"]
125
+ # mock_paramiko fixture 以 patch 副作用生效,线程模型测试不直接引用其返回值
126
+ "tests/test_ssh_client.py" = ["ARG002"]
127
+ # Profile 合并测试中的 BareRepo stub 需保留与 HostRepository ABC 一致的签名
128
+ "tests/test_profile_merge.py" = ["ARG002"]
125
129
  "tests/test_async_batch_executor.py" = ["ARG001", "ARG002"]
126
130
  # 连接池测试中 patched_client fixture 触发 patch 副作用,factory 保留 cfg 参数以匹配真实签名
127
131
  "tests/test_sync_connection_pool.py" = ["ARG001", "ARG002"]
@@ -48,7 +48,7 @@ Remote CMD - SSH 远程服务器管理工具
48
48
  - 文档: 参见 docs/ 目录
49
49
 
50
50
  Author: Vae-Scrooge
51
- Version: 2.6.0(单一真相源见 remote_cmd._version)
51
+ Version: 2.8.0(单一真相源见 remote_cmd._version)
52
52
  License: MIT
53
53
  """
54
54
 
@@ -59,24 +59,26 @@ __author__ = "Vae-Scrooge"
59
59
  __email__ = "vae-scrooge@example.com"
60
60
  __license__ = "MIT"
61
61
 
62
+ import importlib.util
62
63
  import logging
63
64
 
64
65
  logging.getLogger(__name__).addHandler(logging.NullHandler())
65
66
 
66
- # 异步原生模块依赖 asyncssh(optional extra [async]),未安装时优雅降级,
67
- # 避免 `import remote_cmd` 直接失败
68
- try:
67
+ # 异步原生模块依赖 asyncssh(optional extra [async])。v2.7(P2.2)起用
68
+ # find_spec 显式探测可选依赖:仅当 asyncssh 缺失时才降级;若 asyncssh 已
69
+ # 安装而我们自己的异步模块 import 失败(代码缺陷/依赖损坏),import
70
+ # 错误会向上传播,不再被静默误判为"未安装 asyncssh"。
71
+ _HAS_ASYNC = importlib.util.find_spec("asyncssh") is not None
72
+
73
+ if _HAS_ASYNC:
69
74
  from remote_cmd.core.async_connection_pool import AsyncConnectionPool
70
75
  from remote_cmd.core.async_ssh_client import AsyncSSHClient
71
76
  from remote_cmd.service.async_batch_executor import AsyncBatchExecutor
72
77
 
73
- _HAS_ASYNC = True
74
- except ImportError: # pragma: no cover - 依赖 asyncssh,未安装时不导出异步符号
75
- _HAS_ASYNC = False
76
-
77
78
  from remote_cmd.api.host_manager import HostManager
78
79
  from remote_cmd.core.budget import ConnectionBudget
79
80
  from remote_cmd.core.host import Host
81
+ from remote_cmd.core.profile import HostProfile
80
82
  from remote_cmd.core.ssh_client import SSHClient
81
83
  from remote_cmd.core.sync_connection_pool import SyncConnectionPool
82
84
 
@@ -104,6 +106,7 @@ from remote_cmd.service.batch_executor import (
104
106
  OutputPolicy,
105
107
  )
106
108
  from remote_cmd.service.credential_provider import KeyringCredentialProvider
109
+ from remote_cmd.service.profile_service import ProfileService
107
110
  from remote_cmd.service.task_runner import Task, TaskRunner, TaskStatus
108
111
  from remote_cmd.utils.crypto import CredentialEncryption
109
112
  from remote_cmd.utils.exceptions import BudgetTimeoutError, PlaintextCredentialWarning
@@ -123,11 +126,13 @@ if _HAS_ASYNC:
123
126
  "AsyncConnectionPool",
124
127
  "AsyncBatchExecutor",
125
128
  "Host",
129
+ "HostProfile",
126
130
  "HostManager",
127
131
  # 新架构导出
128
132
  "HostRepository",
129
133
  "JsonHostRepository",
130
134
  "HostService",
135
+ "ProfileService",
131
136
  "SSHService",
132
137
  "CredentialProvider",
133
138
  "EnvCredentialProvider",
@@ -160,11 +165,13 @@ else:
160
165
  # 原有导出(向后兼容,不含异步符号)
161
166
  "SSHClient",
162
167
  "Host",
168
+ "HostProfile",
163
169
  "HostManager",
164
170
  # 新架构导出
165
171
  "HostRepository",
166
172
  "JsonHostRepository",
167
173
  "HostService",
174
+ "ProfileService",
168
175
  "SSHService",
169
176
  "CredentialProvider",
170
177
  "EnvCredentialProvider",
@@ -9,4 +9,4 @@
9
9
  - ``__version__`` 由 ``remote_cmd/__init__.py`` 导入并继续作为公共 API 导出。
10
10
  """
11
11
 
12
- __version__ = "2.6.0"
12
+ __version__ = "2.8.0"
@@ -0,0 +1,27 @@
1
+ """
2
+ 执行结果格式化层(CLI,v2.8)
3
+
4
+ 职责边界:
5
+ - 本层只消费 ``CommandResult`` / ``BatchResult`` 数据对象并渲染字符串,
6
+ **绝不进入执行内核**(BatchExecutor / 连接池等不感知输出格式)。
7
+ - ``rich`` 格式(默认)保留在 CLI 原有渲染路径(含颜色与进度条),
8
+ 本包提供 ``json`` / ``table`` 两种机器友好格式。
9
+ - JSON 输出为稳定 schema(字段与顺序固定),便于脚本消费。
10
+
11
+ 用法:
12
+ >>> from remote_cmd.cli.formatters import format_batch_result
13
+ >>> text = format_batch_result(batch_result, fmt="json") # doctest: +SKIP
14
+ """
15
+
16
+ from remote_cmd.cli.formatters.base import format_batch_result, format_single_result
17
+ from remote_cmd.cli.formatters.json import render_batch_json, render_single_json
18
+ from remote_cmd.cli.formatters.table import render_batch_table, render_single_table
19
+
20
+ __all__ = [
21
+ "format_batch_result",
22
+ "format_single_result",
23
+ "render_batch_json",
24
+ "render_batch_table",
25
+ "render_single_json",
26
+ "render_single_table",
27
+ ]
@@ -0,0 +1,71 @@
1
+ """
2
+ 格式化器分发与校验(CLI,v2.8)
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ from remote_cmd.cli.formatters.json import render_batch_json, render_single_json
8
+ from remote_cmd.cli.formatters.table import render_batch_table, render_single_table
9
+ from remote_cmd.core.ssh_client import CommandResult
10
+ from remote_cmd.service._types import BatchResult
11
+ from remote_cmd.utils.exceptions import ValidationError
12
+
13
+ #: CLI ``--format`` 可选值(rich 由 CLI 默认渲染路径处理)
14
+ MACHINE_FORMATS = ("json", "table")
15
+ FORMAT_CHOICES = ("rich", "json", "table")
16
+
17
+
18
+ def format_single_result(
19
+ host_name: str,
20
+ command: str,
21
+ result: CommandResult,
22
+ fmt: str,
23
+ duration: Optional[float] = None,
24
+ ) -> str:
25
+ """渲染单主机执行结果为 ``fmt`` 字符串。
26
+
27
+ Args:
28
+ host_name: 主机名
29
+ command: 执行的命令
30
+ result: 命令结果
31
+ fmt: ``json`` 或 ``table``
32
+ duration: 可选耗时(秒),JSON 中作为 ``duration`` 字段
33
+
34
+ Raises:
35
+ ValidationError: fmt 不是机器格式
36
+ """
37
+ if fmt == "json":
38
+ return render_single_json(host_name, command, result, duration)
39
+ if fmt == "table":
40
+ return render_single_table(host_name, command, result, duration)
41
+ raise ValidationError(f"unsupported machine format: {fmt!r} (expected json/table)")
42
+
43
+
44
+ def format_batch_result(
45
+ result: BatchResult,
46
+ fmt: str,
47
+ show_failures: bool = False,
48
+ ) -> str:
49
+ """渲染批量执行结果为 ``fmt`` 字符串。
50
+
51
+ Args:
52
+ result: 批量结果
53
+ fmt: ``json`` 或 ``table``
54
+ show_failures: table 格式下仅显示失败主机(JSON 始终完整)
55
+
56
+ Raises:
57
+ ValidationError: fmt 不是机器格式
58
+ """
59
+ if fmt == "json":
60
+ return render_batch_json(result)
61
+ if fmt == "table":
62
+ return render_batch_table(result, show_failures=show_failures)
63
+ raise ValidationError(f"unsupported machine format: {fmt!r} (expected json/table)")
64
+
65
+
66
+ __all__ = [
67
+ "FORMAT_CHOICES",
68
+ "MACHINE_FORMATS",
69
+ "format_batch_result",
70
+ "format_single_result",
71
+ ]
@@ -0,0 +1,82 @@
1
+ """
2
+ JSON 格式化器(稳定 schema,机器友好;v2.8)
3
+ """
4
+
5
+ import json
6
+ from typing import Any, Optional
7
+
8
+ from remote_cmd.core.ssh_client import CommandResult
9
+ from remote_cmd.service._types import BatchResult
10
+
11
+
12
+ def _single_payload(
13
+ host_name: str,
14
+ command: str,
15
+ result: CommandResult,
16
+ duration: Optional[float],
17
+ ) -> dict[str, Any]:
18
+ return {
19
+ "host": host_name,
20
+ "command": command,
21
+ "success": result.success,
22
+ "exit_code": result.exit_code,
23
+ "duration": duration,
24
+ "stdout": result.stdout,
25
+ "stderr": result.stderr,
26
+ }
27
+
28
+
29
+ def render_single_json(
30
+ host_name: str,
31
+ command: str,
32
+ result: CommandResult,
33
+ duration: Optional[float] = None,
34
+ ) -> str:
35
+ """渲染单主机结果为 JSON(字段顺序固定,ensure_ascii=False)。"""
36
+ return json.dumps(
37
+ _single_payload(host_name, command, result, duration),
38
+ ensure_ascii=False,
39
+ indent=2,
40
+ )
41
+
42
+
43
+ def render_batch_json(result: BatchResult) -> str:
44
+ """渲染批量结果为 JSON。
45
+
46
+ schema(稳定)::
47
+
48
+ {
49
+ "total": 3, "success": 2, "failed": 1, "duration": 1.42,
50
+ "results": {
51
+ "web1": {"success": true, "exit_code": 0, "duration": 0.1,
52
+ "stdout": "...", "stderr": "", "error": null,
53
+ "command": "uptime"},
54
+ ...
55
+ }
56
+ }
57
+
58
+ ``results`` 按主机名排序,保证与完成顺序无关的确定性输出。
59
+ """
60
+ results: dict[str, Any] = {}
61
+ for host in sorted(result.results):
62
+ host_result = result.results[host]
63
+ results[host] = {
64
+ "success": host_result.success,
65
+ "exit_code": host_result.exit_code,
66
+ "duration": host_result.duration,
67
+ "stdout": host_result.stdout,
68
+ "stderr": host_result.stderr,
69
+ "error": host_result.error,
70
+ "command": host_result.command,
71
+ }
72
+ payload = {
73
+ "total": result.total,
74
+ "success": result.success,
75
+ "failed": result.failed,
76
+ "duration": result.duration,
77
+ "results": results,
78
+ }
79
+ return json.dumps(payload, ensure_ascii=False, indent=2)
80
+
81
+
82
+ __all__ = ["render_batch_json", "render_single_json"]
@@ -0,0 +1,84 @@
1
+ """
2
+ Table 格式化器(纯文本对齐,无颜色;v2.8)
3
+ """
4
+
5
+ from typing import Optional
6
+
7
+ from remote_cmd.core.ssh_client import CommandResult
8
+ from remote_cmd.service._types import BatchResult
9
+
10
+ # 单元格最大宽度(字符);超出截断并追加省略号
11
+ _MAX_CELL = 60
12
+ _ELLIPSIS = "…"
13
+
14
+
15
+ def _shorten(text: str, limit: int = _MAX_CELL) -> str:
16
+ """单元格内容单行化 + 截断(表格不展开多行输出)。"""
17
+ first_line = (text or "").splitlines()[0] if text else ""
18
+ if len(first_line) <= limit:
19
+ return first_line
20
+ return first_line[: limit - 1] + _ELLIPSIS
21
+
22
+
23
+ def _render_rows(headers: list[str], rows: list[list[str]]) -> str:
24
+ widths = [len(h) for h in headers]
25
+ for row in rows:
26
+ for i, cell in enumerate(row):
27
+ widths[i] = max(widths[i], len(cell))
28
+ lines = [" ".join(h.ljust(widths[i]) for i, h in enumerate(headers)).rstrip()]
29
+ for row in rows:
30
+ lines.append(" ".join(c.ljust(widths[i]) for i, c in enumerate(row)).rstrip())
31
+ return "\n".join(lines)
32
+
33
+
34
+ def render_single_table(
35
+ host_name: str,
36
+ command: str,
37
+ result: CommandResult,
38
+ duration: Optional[float] = None,
39
+ ) -> str:
40
+ """渲染单主机结果:摘要表 + stdout/stderr 分节。"""
41
+ duration_text = f"{duration:.2f}s" if duration is not None else "-"
42
+ summary = _render_rows(
43
+ ["HOST", "COMMAND", "STATUS", "EXIT", "DURATION"],
44
+ [[host_name, _shorten(command), "ok" if result.success else "fail", str(result.exit_code), duration_text]],
45
+ )
46
+ parts = [summary, "", "--- stdout ---", result.stdout.rstrip("\n"), "", "--- stderr ---", result.stderr.rstrip("\n")]
47
+ return "\n".join(parts)
48
+
49
+
50
+ def render_batch_table(result: BatchResult, show_failures: bool = False) -> str:
51
+ """渲染批量结果表:HOST/STATUS/EXIT/DURATION/OUTPUT。
52
+
53
+ 输出列优先展示错误信息(失败主机),否则展示 stdout 首行;
54
+ ``show_failures=True`` 时仅渲染失败主机。
55
+ """
56
+ hosts = sorted(result.results)
57
+ if show_failures:
58
+ hosts = [h for h in hosts if not result.results[h].success]
59
+
60
+ rows: list[list[str]] = []
61
+ for host in hosts:
62
+ r = result.results[host]
63
+ output = r.error if (r.error and not r.success) else r.stdout
64
+ rows.append(
65
+ [
66
+ host,
67
+ "ok" if r.success else "fail",
68
+ str(r.exit_code),
69
+ f"{r.duration:.2f}s",
70
+ _shorten(output or ""),
71
+ ]
72
+ )
73
+
74
+ header = _render_rows(["HOST", "STATUS", "EXIT", "DURATION", "OUTPUT"], rows)
75
+ summary = (
76
+ f"Total: {result.total} Succeeded: {result.success} "
77
+ f"Failed: {result.failed} Duration: {result.duration:.2f}s"
78
+ )
79
+ if not rows and not show_failures:
80
+ return f"{header}\n\nNo hosts.\n{summary}"
81
+ return f"{header}\n\n{summary}"
82
+
83
+
84
+ __all__ = ["render_batch_table", "render_single_table"]