remote-cmd-manager 2.7.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.7.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.8.0}/PKG-INFO +24 -21
  2. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/README.md +23 -20
  3. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/pyproject.toml +2 -0
  4. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/__init__.py +7 -1
  5. {remote_cmd_manager-2.7.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.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/cli/main.py +226 -14
  11. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/host.py +3 -0
  12. remote_cmd_manager-2.8.0/remote_cmd/core/profile.py +114 -0
  13. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/json_host_repository.py +48 -4
  14. remote_cmd_manager-2.8.0/remote_cmd/repository/profile_store.py +48 -0
  15. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/sqlite_host_repository.py +94 -0
  16. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/_host_runner.py +10 -1
  17. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/host_service.py +89 -8
  18. remote_cmd_manager-2.8.0/remote_cmd/service/profile_service.py +123 -0
  19. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0/remote_cmd_manager.egg-info}/PKG-INFO +24 -21
  20. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/SOURCES.txt +12 -0
  21. remote_cmd_manager-2.8.0/tests/test_cli_profile.py +262 -0
  22. remote_cmd_manager-2.8.0/tests/test_formatters.py +149 -0
  23. remote_cmd_manager-2.8.0/tests/test_profile.py +72 -0
  24. remote_cmd_manager-2.8.0/tests/test_profile_merge.py +196 -0
  25. remote_cmd_manager-2.8.0/tests/test_profile_service.py +149 -0
  26. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/LICENSE +0 -0
  27. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/MANIFEST.in +0 -0
  28. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/config.example.yaml +0 -0
  29. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/examples/basic_usage.py +0 -0
  30. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/examples/deploy_script.py +0 -0
  31. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/examples/nginx_batch_update.py +0 -0
  32. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/examples/system_health_check.py +0 -0
  33. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/api/__init__.py +0 -0
  34. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/api/host_manager.py +0 -0
  35. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/cli/__init__.py +0 -0
  36. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/__init__.py +0 -0
  37. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/async_connection_pool.py +0 -0
  38. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/async_ssh_client.py +0 -0
  39. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/budget.py +0 -0
  40. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/host_manager.py +0 -0
  41. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/pool_policy.py +0 -0
  42. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/ssh_client.py +0 -0
  43. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/core/sync_connection_pool.py +0 -0
  44. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/__init__.py +0 -0
  45. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/repository/host_repository.py +0 -0
  46. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/__init__.py +0 -0
  47. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/_pool_policy.py +0 -0
  48. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/_types.py +0 -0
  49. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/async_batch_executor.py +0 -0
  50. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/batch_executor.py +0 -0
  51. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/credential_provider.py +0 -0
  52. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/retry_policy.py +0 -0
  53. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/ssh_service.py +0 -0
  54. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/storage_factory.py +0 -0
  55. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/service/task_runner.py +0 -0
  56. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/__init__.py +0 -0
  57. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/config.py +0 -0
  58. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/credential_guard.py +0 -0
  59. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/crypto.py +0 -0
  60. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/exceptions.py +0 -0
  61. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd/utils/logging_utils.py +0 -0
  62. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
  63. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
  64. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/requires.txt +0 -0
  65. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
  66. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/requirements.txt +0 -0
  67. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/setup.cfg +0 -0
  68. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/__init__.py +0 -0
  69. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/conftest.py +0 -0
  70. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/integration/conftest.py +0 -0
  71. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/integration/test_ssh_connection.py +0 -0
  72. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/performance/__init__.py +0 -0
  73. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/performance/conftest.py +0 -0
  74. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/performance/test_benchmarks.py +0 -0
  75. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/performance/test_paramiko_threads.py +0 -0
  76. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_async_batch_executor.py +0 -0
  77. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_async_ssh_client.py +0 -0
  78. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_batch_executor.py +0 -0
  79. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_cli.py +0 -0
  80. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_config.py +0 -0
  81. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_connection_budget.py +0 -0
  82. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_credential_provider.py +0 -0
  83. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_crypto.py +0 -0
  84. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_exceptions.py +0 -0
  85. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_host.py +0 -0
  86. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_host_manager.py +0 -0
  87. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_host_service.py +0 -0
  88. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_keyring_provider.py +0 -0
  89. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_logging_utils.py +0 -0
  90. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_output_policy.py +0 -0
  91. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_package_import.py +0 -0
  92. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_pool_policy.py +0 -0
  93. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_release_metadata.py +0 -0
  94. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_repository.py +0 -0
  95. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_retry_policy.py +0 -0
  96. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_sqlite_repository.py +0 -0
  97. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_ssh_client.py +0 -0
  98. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_ssh_service.py +0 -0
  99. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_storage_factory.py +0 -0
  100. {remote_cmd_manager-2.7.0 → remote_cmd_manager-2.8.0}/tests/test_sync_connection_pool.py +0 -0
  101. {remote_cmd_manager-2.7.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.7.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,36 +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.7.0 Release Highlights
92
+ ## v2.8.0 Release Highlights
93
93
 
94
- v2.7.0 is a runtime-efficiency and error-semantics release: credential-layer
95
- exceptions are narrowed so real bugs surface, SQLite no longer truncates its WAL
96
- on every write, and the global connection budget waits on a true async queue
97
- instead of polling. Public APIs remain backward compatible. See the
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
98
97
  [full migration notes](./CHANGELOG.md).
99
98
 
100
- ### Error Semantics
99
+ ### Connection Profiles
101
100
 
102
- - Keyring, encryption, and host-service decrypt paths now catch only expected failure classes; programming errors propagate instead of being silently swallowed.
103
- - Background pool monitor loops keep their catch-all guard but log full stack traces.
104
- - Optional `asyncssh` detection uses `find_spec`, so a defect in the async modules themselves can no longer masquerade as "dependency not installed".
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.
105
105
 
106
- ### SQLite WAL Efficiency
106
+ ### Machine-Readable Output
107
107
 
108
- - `flush()` now runs a **PASSIVE** checkpoint instead of `TRUNCATE` — no more writer waits or WAL rewrites on every host add/update/remove.
109
- - New explicit `repo.checkpoint("TRUNCATE")` (also `PASSIVE`/`FULL`/`RESTART`) for deliberate WAL compaction.
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
- ### Connection Budget
112
+ ### Try It
112
113
 
113
- - `ConnectionBudget.acquire_async()` now waits on a real queue (`asyncio.Future` woken via `call_soon_threadsafe`), removing the v2.6 polling loop; cancellation-safe and FIFO among async waiters.
114
-
115
- ### Benchmark Data (P2.4)
116
-
117
- - The sync Paramiko path costs ~100 µs per command in thread create/join and peaks at ≈ 3× sustained concurrency in live threads. A centralized channel-reader rewrite is a v2.7.x decision based on these measurements.
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
+ ```
118
119
 
119
120
  ## Table of Contents
120
121
 
121
- - [v2.7.0 Release Highlights](#v270-release-highlights)
122
+ - [v2.8.0 Release Highlights](#v280-release-highlights)
122
123
  - [Why Remote CMD?](#why-remote-cmd)
123
124
  - [Quick Start](#quick-start)
124
125
  - [Use Cases](#use-cases)
@@ -265,7 +266,9 @@ with SSHClient(config) as client:
265
266
  | **Commands** | Single, multi-line, sudo with password |
266
267
  | **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
267
268
  | **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
268
- | **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 |
269
272
  | **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
270
273
  | **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
271
274
  | **Global Connection Budget** | Optional `ConnectionBudget` caps live SSH connections process-wide across pools and executors |
@@ -36,36 +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.7.0 Release Highlights
39
+ ## v2.8.0 Release Highlights
40
40
 
41
- v2.7.0 is a runtime-efficiency and error-semantics release: credential-layer
42
- exceptions are narrowed so real bugs surface, SQLite no longer truncates its WAL
43
- on every write, and the global connection budget waits on a true async queue
44
- instead of polling. Public APIs remain backward compatible. See the
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
45
44
  [full migration notes](./CHANGELOG.md).
46
45
 
47
- ### Error Semantics
46
+ ### Connection Profiles
48
47
 
49
- - Keyring, encryption, and host-service decrypt paths now catch only expected failure classes; programming errors propagate instead of being silently swallowed.
50
- - Background pool monitor loops keep their catch-all guard but log full stack traces.
51
- - Optional `asyncssh` detection uses `find_spec`, so a defect in the async modules themselves can no longer masquerade as "dependency not installed".
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.
52
52
 
53
- ### SQLite WAL Efficiency
53
+ ### Machine-Readable Output
54
54
 
55
- - `flush()` now runs a **PASSIVE** checkpoint instead of `TRUNCATE` — no more writer waits or WAL rewrites on every host add/update/remove.
56
- - New explicit `repo.checkpoint("TRUNCATE")` (also `PASSIVE`/`FULL`/`RESTART`) for deliberate WAL compaction.
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
- ### Connection Budget
59
+ ### Try It
59
60
 
60
- - `ConnectionBudget.acquire_async()` now waits on a real queue (`asyncio.Future` woken via `call_soon_threadsafe`), removing the v2.6 polling loop; cancellation-safe and FIFO among async waiters.
61
-
62
- ### Benchmark Data (P2.4)
63
-
64
- - The sync Paramiko path costs ~100 µs per command in thread create/join and peaks at ≈ 3× sustained concurrency in live threads. A centralized channel-reader rewrite is a v2.7.x decision based on these measurements.
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
+ ```
65
66
 
66
67
  ## Table of Contents
67
68
 
68
- - [v2.7.0 Release Highlights](#v270-release-highlights)
69
+ - [v2.8.0 Release Highlights](#v280-release-highlights)
69
70
  - [Why Remote CMD?](#why-remote-cmd)
70
71
  - [Quick Start](#quick-start)
71
72
  - [Use Cases](#use-cases)
@@ -212,7 +213,9 @@ with SSHClient(config) as client:
212
213
  | **Commands** | Single, multi-line, sudo with password |
213
214
  | **File Transfer** | Upload/download via SFTP (`remote-cmd upload/download`) |
214
215
  | **Host Management** | CRUD with pluggable JSON or **SQLite** persistence |
215
- | **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 |
216
219
  | **Batch Ops** | Run commands across any host group, synchronously or asynchronously; optional per-host retained-output cap (`max_output_bytes`) |
217
220
  | **Async Kernel** | `AsyncSSHClient` / `AsyncConnectionPool` / `AsyncBatchExecutor` via the `[async]` extra |
218
221
  | **Global Connection Budget** | Optional `ConnectionBudget` caps live SSH connections process-wide across pools and executors |
@@ -124,6 +124,8 @@ ignore = [
124
124
  "tests/test_async_ssh_client.py" = ["ARG002"]
125
125
  # mock_paramiko fixture 以 patch 副作用生效,线程模型测试不直接引用其返回值
126
126
  "tests/test_ssh_client.py" = ["ARG002"]
127
+ # Profile 合并测试中的 BareRepo stub 需保留与 HostRepository ABC 一致的签名
128
+ "tests/test_profile_merge.py" = ["ARG002"]
127
129
  "tests/test_async_batch_executor.py" = ["ARG001", "ARG002"]
128
130
  # 连接池测试中 patched_client fixture 触发 patch 副作用,factory 保留 cfg 参数以匹配真实签名
129
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.7.0(单一真相源见 remote_cmd._version)
51
+ Version: 2.8.0(单一真相源见 remote_cmd._version)
52
52
  License: MIT
53
53
  """
54
54
 
@@ -78,6 +78,7 @@ if _HAS_ASYNC:
78
78
  from remote_cmd.api.host_manager import HostManager
79
79
  from remote_cmd.core.budget import ConnectionBudget
80
80
  from remote_cmd.core.host import Host
81
+ from remote_cmd.core.profile import HostProfile
81
82
  from remote_cmd.core.ssh_client import SSHClient
82
83
  from remote_cmd.core.sync_connection_pool import SyncConnectionPool
83
84
 
@@ -105,6 +106,7 @@ from remote_cmd.service.batch_executor import (
105
106
  OutputPolicy,
106
107
  )
107
108
  from remote_cmd.service.credential_provider import KeyringCredentialProvider
109
+ from remote_cmd.service.profile_service import ProfileService
108
110
  from remote_cmd.service.task_runner import Task, TaskRunner, TaskStatus
109
111
  from remote_cmd.utils.crypto import CredentialEncryption
110
112
  from remote_cmd.utils.exceptions import BudgetTimeoutError, PlaintextCredentialWarning
@@ -124,11 +126,13 @@ if _HAS_ASYNC:
124
126
  "AsyncConnectionPool",
125
127
  "AsyncBatchExecutor",
126
128
  "Host",
129
+ "HostProfile",
127
130
  "HostManager",
128
131
  # 新架构导出
129
132
  "HostRepository",
130
133
  "JsonHostRepository",
131
134
  "HostService",
135
+ "ProfileService",
132
136
  "SSHService",
133
137
  "CredentialProvider",
134
138
  "EnvCredentialProvider",
@@ -161,11 +165,13 @@ else:
161
165
  # 原有导出(向后兼容,不含异步符号)
162
166
  "SSHClient",
163
167
  "Host",
168
+ "HostProfile",
164
169
  "HostManager",
165
170
  # 新架构导出
166
171
  "HostRepository",
167
172
  "JsonHostRepository",
168
173
  "HostService",
174
+ "ProfileService",
169
175
  "SSHService",
170
176
  "CredentialProvider",
171
177
  "EnvCredentialProvider",
@@ -9,4 +9,4 @@
9
9
  - ``__version__`` 由 ``remote_cmd/__init__.py`` 导入并继续作为公共 API 导出。
10
10
  """
11
11
 
12
- __version__ = "2.7.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"]