remote-cmd-manager 2.4.0__tar.gz → 2.5.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.
- {remote_cmd_manager-2.4.0/remote_cmd_manager.egg-info → remote_cmd_manager-2.5.0}/PKG-INFO +25 -19
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/README.md +20 -14
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/pyproject.toml +15 -8
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/__init__.py +12 -2
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/_version.py +1 -1
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/json_host_repository.py +61 -2
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/sqlite_host_repository.py +43 -2
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/_host_runner.py +31 -1
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/_types.py +33 -1
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/async_batch_executor.py +83 -48
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/batch_executor.py +11 -4
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/storage_factory.py +37 -11
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/credential_guard.py +18 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/exceptions.py +17 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0/remote_cmd_manager.egg-info}/PKG-INFO +25 -19
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/SOURCES.txt +1 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/requires.txt +3 -2
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_async_batch_executor.py +47 -0
- remote_cmd_manager-2.5.0/tests/test_output_policy.py +172 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_release_metadata.py +13 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_repository.py +69 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_sqlite_repository.py +49 -2
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_storage_factory.py +40 -2
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/LICENSE +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/MANIFEST.in +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/config.example.yaml +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/examples/basic_usage.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/examples/deploy_script.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/examples/nginx_batch_update.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/examples/system_health_check.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/cli/__init__.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/cli/main.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/__init__.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/async_connection_pool.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/async_ssh_client.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/host.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/host_manager.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/ssh_client.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/core/sync_connection_pool.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/__init__.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/host_repository.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/__init__.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/_pool_policy.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/credential_provider.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/host_service.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/retry_policy.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/ssh_service.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/service/task_runner.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/__init__.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/config.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/crypto.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/utils/logging_utils.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/dependency_links.txt +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/entry_points.txt +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd_manager.egg-info/top_level.txt +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/requirements.txt +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/setup.cfg +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/__init__.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/conftest.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/integration/conftest.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/integration/test_ssh_connection.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/performance/__init__.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/performance/conftest.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/performance/test_benchmarks.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_async_ssh_client.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_batch_executor.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_cli.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_config.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_credential_provider.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_crypto.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_exceptions.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_host.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_host_manager.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_host_service.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_keyring_provider.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_logging_utils.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_pool_policy.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_retry_policy.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_ssh_client.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_ssh_service.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/tests/test_sync_connection_pool.py +0 -0
- {remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.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.
|
|
3
|
+
Version: 2.5.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
|
|
@@ -15,7 +15,6 @@ Classifier: Intended Audience :: Developers
|
|
|
15
15
|
Classifier: Intended Audience :: System Administrators
|
|
16
16
|
Classifier: License :: OSI Approved :: MIT License
|
|
17
17
|
Classifier: Programming Language :: Python :: 3
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
@@ -27,17 +26,17 @@ Classifier: Environment :: Console
|
|
|
27
26
|
Classifier: Operating System :: POSIX :: Linux
|
|
28
27
|
Classifier: Operating System :: MacOS
|
|
29
28
|
Classifier: Operating System :: Microsoft :: Windows
|
|
30
|
-
Requires-Python: >=3.
|
|
29
|
+
Requires-Python: >=3.10
|
|
31
30
|
Description-Content-Type: text/markdown
|
|
32
31
|
License-File: LICENSE
|
|
33
|
-
Requires-Dist: paramiko
|
|
32
|
+
Requires-Dist: paramiko<6,>=5.0
|
|
34
33
|
Requires-Dist: click>=8.0
|
|
35
34
|
Requires-Dist: cryptography>=41.0
|
|
36
35
|
Requires-Dist: rich>=13.0
|
|
37
36
|
Requires-Dist: PyYAML>=6.0
|
|
38
37
|
Requires-Dist: aiofiles>=3.0
|
|
39
38
|
Provides-Extra: async
|
|
40
|
-
Requires-Dist: asyncssh
|
|
39
|
+
Requires-Dist: asyncssh<3,>=2.24.0; extra == "async"
|
|
41
40
|
Provides-Extra: cloud
|
|
42
41
|
Requires-Dist: boto3>=1.34; extra == "cloud"
|
|
43
42
|
Provides-Extra: dev
|
|
@@ -46,6 +45,7 @@ Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
|
46
45
|
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
47
46
|
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
48
47
|
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
48
|
+
Requires-Dist: asyncssh<3,>=2.24.0; extra == "dev"
|
|
49
49
|
Requires-Dist: keyring>=24.0; extra == "dev"
|
|
50
50
|
Provides-Extra: docs
|
|
51
51
|
Requires-Dist: pdoc==15.0.4; extra == "docs"
|
|
@@ -55,7 +55,7 @@ Dynamic: license-file
|
|
|
55
55
|
<img src="https://img.shields.io/pypi/v/remote_cmd_manager?style=for-the-badge&logo=pypi&logoColor=white&label=PyPI" alt="PyPI">
|
|
56
56
|
<img src="https://img.shields.io/pypi/dm/remote_cmd_manager?style=for-the-badge&logo=python&logoColor=white&label=Downloads" alt="Downloads">
|
|
57
57
|
<img src="https://img.shields.io/github/stars/Vae-Scrooge/remote-cmd?style=for-the-badge&logo=github" alt="Stars">
|
|
58
|
-
<img src="https://img.shields.io/badge/python-3.
|
|
58
|
+
<img src="https://img.shields.io/badge/python-3.10%2B-blue?style=for-the-badge&logo=python" alt="Python">
|
|
59
59
|
<img src="https://img.shields.io/github/license/Vae-Scrooge/remote-cmd?style=for-the-badge" alt="License">
|
|
60
60
|
<img src="https://img.shields.io/github/actions/workflow/status/Vae-Scrooge/remote-cmd/ci.yml?style=for-the-badge&logo=githubactions&label=CI" alt="CI">
|
|
61
61
|
</p>
|
|
@@ -89,30 +89,36 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
89
89
|
|
|
90
90
|
---
|
|
91
91
|
|
|
92
|
-
## v2.
|
|
92
|
+
## v2.5.0 Release Highlights
|
|
93
93
|
|
|
94
|
-
v2.
|
|
95
|
-
|
|
94
|
+
v2.5.0 is a hardening and scalability release: dependency security floors, Python 3.9
|
|
95
|
+
removal, bounded async scheduling, and explicit output-retention / credential-persistence
|
|
96
|
+
policies. Backward-compatible defaults are preserved where behavior is involved. See the
|
|
96
97
|
[full migration notes](./CHANGELOG.md) before upgrading automated callers.
|
|
97
98
|
|
|
98
|
-
###
|
|
99
|
+
### Security & Platform
|
|
99
100
|
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
- The cap bounds the retained `BatchResult`; the SSH client may still hold full output transiently during execution, so it is not a process-wide RSS limit.
|
|
101
|
+
- Minimum supported Python is now **3.10** (3.9 reached EOL on 2025-10-31).
|
|
102
|
+
- Security floors raised: **Paramiko >= 5.0,<6** and **AsyncSSH >= 2.24.0,<3** (2.23.x and earlier are affected by 2026 AsyncSSH advisories).
|
|
103
103
|
|
|
104
|
-
###
|
|
104
|
+
### Scalability
|
|
105
105
|
|
|
106
|
-
- `
|
|
107
|
-
-
|
|
106
|
+
- `AsyncBatchExecutor` now uses a **bounded worker queue**: only `min(max_concurrency, host count)` worker tasks are created instead of one task per host, so scheduling memory no longer grows with fleet size.
|
|
107
|
+
- New `OutputPolicy` (exported from `remote_cmd`) gives an explicit, mutually exclusive alternative to `max_output_bytes`; `None` still means full retention in v2.5, and a bounded default is planned for v3.0.
|
|
108
|
+
|
|
109
|
+
### Credential Safety
|
|
110
|
+
|
|
111
|
+
- `JsonHostRepository` / `SqliteHostRepository` accept `allow_plaintext_credentials` (default `None`): plaintext persistence now emits `PlaintextCredentialWarning`; pass `True` to opt in silently or `False` to reject it with `CredentialError`. v3.0 plans to reject by default.
|
|
112
|
+
- JSON repository concurrency semantics are now explicit: single-process/single-writer; use SQLite for multi-process writers.
|
|
113
|
+
- The `dev` extra now includes `asyncssh`, so `pip install -e ".[dev]"` runs the full test suite out of the box.
|
|
108
114
|
|
|
109
115
|
### Compatibility
|
|
110
116
|
|
|
111
|
-
-
|
|
117
|
+
- `max_output_bytes=None` still retains full output, and the public `BatchResult` / `BatchHostResult` schemas are unchanged; the legacy `max_output_bytes` parameter continues to work alongside `OutputPolicy`.
|
|
112
118
|
|
|
113
119
|
## Table of Contents
|
|
114
120
|
|
|
115
|
-
- [v2.
|
|
121
|
+
- [v2.5.0 Release Highlights](#v250-release-highlights)
|
|
116
122
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
117
123
|
- [Quick Start](#quick-start)
|
|
118
124
|
- [Use Cases](#use-cases)
|
|
@@ -350,7 +356,7 @@ Good first issues are labelled `good first issue` in the
|
|
|
350
356
|
developed independently as a focused alternative to heavyweight tools for the
|
|
351
357
|
ad-hoc SSH tasks that come up in day-to-day server work.
|
|
352
358
|
|
|
353
|
-
- **Project health:** CI runs on every PR, Python 3.
|
|
359
|
+
- **Project health:** CI runs on every PR, Python 3.10+ is supported, and the
|
|
354
360
|
public API is versioned under [semantic versioning](https://semver.org/).
|
|
355
361
|
- **Your code, your servers:** usage stays open under the MIT license — nothing
|
|
356
362
|
is telemetry-driven or locked behind a service.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<img src="https://img.shields.io/pypi/v/remote_cmd_manager?style=for-the-badge&logo=pypi&logoColor=white&label=PyPI" alt="PyPI">
|
|
3
3
|
<img src="https://img.shields.io/pypi/dm/remote_cmd_manager?style=for-the-badge&logo=python&logoColor=white&label=Downloads" alt="Downloads">
|
|
4
4
|
<img src="https://img.shields.io/github/stars/Vae-Scrooge/remote-cmd?style=for-the-badge&logo=github" alt="Stars">
|
|
5
|
-
<img src="https://img.shields.io/badge/python-3.
|
|
5
|
+
<img src="https://img.shields.io/badge/python-3.10%2B-blue?style=for-the-badge&logo=python" alt="Python">
|
|
6
6
|
<img src="https://img.shields.io/github/license/Vae-Scrooge/remote-cmd?style=for-the-badge" alt="License">
|
|
7
7
|
<img src="https://img.shields.io/github/actions/workflow/status/Vae-Scrooge/remote-cmd/ci.yml?style=for-the-badge&logo=githubactions&label=CI" alt="CI">
|
|
8
8
|
</p>
|
|
@@ -36,30 +36,36 @@ pip install remote_cmd_manager && remote-cmd host add web-01 192.168.1.10 ubuntu
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
-
## v2.
|
|
39
|
+
## v2.5.0 Release Highlights
|
|
40
40
|
|
|
41
|
-
v2.
|
|
42
|
-
|
|
41
|
+
v2.5.0 is a hardening and scalability release: dependency security floors, Python 3.9
|
|
42
|
+
removal, bounded async scheduling, and explicit output-retention / credential-persistence
|
|
43
|
+
policies. Backward-compatible defaults are preserved where behavior is involved. See the
|
|
43
44
|
[full migration notes](./CHANGELOG.md) before upgrading automated callers.
|
|
44
45
|
|
|
45
|
-
###
|
|
46
|
+
### Security & Platform
|
|
46
47
|
|
|
47
|
-
-
|
|
48
|
-
-
|
|
49
|
-
- The cap bounds the retained `BatchResult`; the SSH client may still hold full output transiently during execution, so it is not a process-wide RSS limit.
|
|
48
|
+
- Minimum supported Python is now **3.10** (3.9 reached EOL on 2025-10-31).
|
|
49
|
+
- Security floors raised: **Paramiko >= 5.0,<6** and **AsyncSSH >= 2.24.0,<3** (2.23.x and earlier are affected by 2026 AsyncSSH advisories).
|
|
50
50
|
|
|
51
|
-
###
|
|
51
|
+
### Scalability
|
|
52
52
|
|
|
53
|
-
- `
|
|
54
|
-
-
|
|
53
|
+
- `AsyncBatchExecutor` now uses a **bounded worker queue**: only `min(max_concurrency, host count)` worker tasks are created instead of one task per host, so scheduling memory no longer grows with fleet size.
|
|
54
|
+
- New `OutputPolicy` (exported from `remote_cmd`) gives an explicit, mutually exclusive alternative to `max_output_bytes`; `None` still means full retention in v2.5, and a bounded default is planned for v3.0.
|
|
55
|
+
|
|
56
|
+
### Credential Safety
|
|
57
|
+
|
|
58
|
+
- `JsonHostRepository` / `SqliteHostRepository` accept `allow_plaintext_credentials` (default `None`): plaintext persistence now emits `PlaintextCredentialWarning`; pass `True` to opt in silently or `False` to reject it with `CredentialError`. v3.0 plans to reject by default.
|
|
59
|
+
- JSON repository concurrency semantics are now explicit: single-process/single-writer; use SQLite for multi-process writers.
|
|
60
|
+
- The `dev` extra now includes `asyncssh`, so `pip install -e ".[dev]"` runs the full test suite out of the box.
|
|
55
61
|
|
|
56
62
|
### Compatibility
|
|
57
63
|
|
|
58
|
-
-
|
|
64
|
+
- `max_output_bytes=None` still retains full output, and the public `BatchResult` / `BatchHostResult` schemas are unchanged; the legacy `max_output_bytes` parameter continues to work alongside `OutputPolicy`.
|
|
59
65
|
|
|
60
66
|
## Table of Contents
|
|
61
67
|
|
|
62
|
-
- [v2.
|
|
68
|
+
- [v2.5.0 Release Highlights](#v250-release-highlights)
|
|
63
69
|
- [Why Remote CMD?](#why-remote-cmd)
|
|
64
70
|
- [Quick Start](#quick-start)
|
|
65
71
|
- [Use Cases](#use-cases)
|
|
@@ -297,7 +303,7 @@ Good first issues are labelled `good first issue` in the
|
|
|
297
303
|
developed independently as a focused alternative to heavyweight tools for the
|
|
298
304
|
ad-hoc SSH tasks that come up in day-to-day server work.
|
|
299
305
|
|
|
300
|
-
- **Project health:** CI runs on every PR, Python 3.
|
|
306
|
+
- **Project health:** CI runs on every PR, Python 3.10+ is supported, and the
|
|
301
307
|
public API is versioned under [semantic versioning](https://semver.org/).
|
|
302
308
|
- **Your code, your servers:** usage stays open under the MIT license — nothing
|
|
303
309
|
is telemetry-driven or locked behind a service.
|
|
@@ -7,7 +7,7 @@ name = "remote_cmd_manager"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "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."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
11
|
license = { text = "MIT" }
|
|
12
12
|
authors = [
|
|
13
13
|
{ name = "Vae-Scrooge", email = "vaescrooge@gmail.com" },
|
|
@@ -19,7 +19,6 @@ classifiers = [
|
|
|
19
19
|
"Intended Audience :: System Administrators",
|
|
20
20
|
"License :: OSI Approved :: MIT License",
|
|
21
21
|
"Programming Language :: Python :: 3",
|
|
22
|
-
"Programming Language :: Python :: 3.9",
|
|
23
22
|
"Programming Language :: Python :: 3.10",
|
|
24
23
|
"Programming Language :: Python :: 3.11",
|
|
25
24
|
"Programming Language :: Python :: 3.12",
|
|
@@ -33,7 +32,7 @@ classifiers = [
|
|
|
33
32
|
"Operating System :: Microsoft :: Windows",
|
|
34
33
|
]
|
|
35
34
|
dependencies = [
|
|
36
|
-
"paramiko>=
|
|
35
|
+
"paramiko>=5.0,<6",
|
|
37
36
|
"click>=8.0",
|
|
38
37
|
"cryptography>=41.0",
|
|
39
38
|
"rich>=13.0",
|
|
@@ -43,22 +42,24 @@ dependencies = [
|
|
|
43
42
|
|
|
44
43
|
[project.optional-dependencies]
|
|
45
44
|
# 异步原生执行内核:启用 BatchExecutor(use_async=True) / AsyncSSHClient / AsyncConnectionPool 时需要
|
|
46
|
-
|
|
45
|
+
# 下限锁定在 2.24.x:2.23.x 及更早版本存在 2026 年公开 CVE(SCP 路径穿越等)。
|
|
46
|
+
async = ["asyncssh>=2.24.0,<3"]
|
|
47
47
|
# 云平台主机发现(Phase 4 预留)
|
|
48
48
|
cloud = ["boto3>=1.34"]
|
|
49
49
|
# 开发与测试
|
|
50
|
+
# 包含 async extra:默认测试套件会 import 异步模块,仅装 dev 无法收集。
|
|
50
51
|
dev = [
|
|
51
52
|
"pytest>=8.0",
|
|
52
53
|
"pytest-asyncio>=0.23",
|
|
53
54
|
"pytest-cov>=4.0",
|
|
54
55
|
"ruff>=0.4",
|
|
55
56
|
"mypy>=1.10",
|
|
57
|
+
"asyncssh>=2.24.0,<3",
|
|
56
58
|
# 测试 keyring 凭据提供者(生产为可选依赖,测试环境固定安装)
|
|
57
59
|
"keyring>=24.0",
|
|
58
60
|
]
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
# (scripts/check_docs_drift.py)要求生成结果可复现。
|
|
61
|
+
# 文档生成。pdoc 固定到 15.0.4:docs/api 漂移门禁(scripts/check_docs_drift.py)
|
|
62
|
+
# 要求生成结果可复现。
|
|
62
63
|
docs = ["pdoc==15.0.4"]
|
|
63
64
|
|
|
64
65
|
[project.urls]
|
|
@@ -88,7 +89,7 @@ markers = [
|
|
|
88
89
|
]
|
|
89
90
|
|
|
90
91
|
[tool.ruff]
|
|
91
|
-
target-version = "
|
|
92
|
+
target-version = "py310"
|
|
92
93
|
line-length = 100
|
|
93
94
|
|
|
94
95
|
[tool.ruff.lint]
|
|
@@ -109,6 +110,12 @@ ignore = [
|
|
|
109
110
|
"N812",
|
|
110
111
|
"N999",
|
|
111
112
|
"B904",
|
|
113
|
+
# PEP 604 / 新式导入重写推迟到 v2.6(保持 v2.5 变更面最小)
|
|
114
|
+
"UP006", # dict/list 泛型重写
|
|
115
|
+
"UP007", # Union -> X|Y
|
|
116
|
+
"UP035", # typing.* 导入重写
|
|
117
|
+
"UP041", # IOError -> TimeoutError 别名
|
|
118
|
+
"UP045", # Optional -> X|None
|
|
112
119
|
]
|
|
113
120
|
|
|
114
121
|
[tool.ruff.lint.per-file-ignores]
|
|
@@ -48,7 +48,7 @@ Remote CMD - SSH 远程服务器管理工具
|
|
|
48
48
|
- 文档: 参见 docs/ 目录
|
|
49
49
|
|
|
50
50
|
Author: Vae-Scrooge
|
|
51
|
-
Version: 2.
|
|
51
|
+
Version: 2.5.0(单一真相源见 remote_cmd._version)
|
|
52
52
|
License: MIT
|
|
53
53
|
"""
|
|
54
54
|
|
|
@@ -96,10 +96,16 @@ from remote_cmd.service import (
|
|
|
96
96
|
HostService,
|
|
97
97
|
SSHService,
|
|
98
98
|
)
|
|
99
|
-
from remote_cmd.service.batch_executor import
|
|
99
|
+
from remote_cmd.service.batch_executor import (
|
|
100
|
+
BatchExecutor,
|
|
101
|
+
BatchHostResult,
|
|
102
|
+
BatchResult,
|
|
103
|
+
OutputPolicy,
|
|
104
|
+
)
|
|
100
105
|
from remote_cmd.service.credential_provider import KeyringCredentialProvider
|
|
101
106
|
from remote_cmd.service.task_runner import Task, TaskRunner, TaskStatus
|
|
102
107
|
from remote_cmd.utils.crypto import CredentialEncryption
|
|
108
|
+
from remote_cmd.utils.exceptions import PlaintextCredentialWarning
|
|
103
109
|
from remote_cmd.utils.logging_utils import (
|
|
104
110
|
SensitiveDataFilter,
|
|
105
111
|
get_logger,
|
|
@@ -134,6 +140,8 @@ if _HAS_ASYNC:
|
|
|
134
140
|
"BatchExecutor",
|
|
135
141
|
"BatchResult",
|
|
136
142
|
"BatchHostResult",
|
|
143
|
+
"OutputPolicy",
|
|
144
|
+
"PlaintextCredentialWarning",
|
|
137
145
|
"SyncConnectionPool",
|
|
138
146
|
"TaskRunner",
|
|
139
147
|
"Task",
|
|
@@ -167,6 +175,8 @@ else:
|
|
|
167
175
|
"BatchExecutor",
|
|
168
176
|
"BatchResult",
|
|
169
177
|
"BatchHostResult",
|
|
178
|
+
"OutputPolicy",
|
|
179
|
+
"PlaintextCredentialWarning",
|
|
170
180
|
"SyncConnectionPool",
|
|
171
181
|
"TaskRunner",
|
|
172
182
|
"Task",
|
{remote_cmd_manager-2.4.0 → remote_cmd_manager-2.5.0}/remote_cmd/repository/json_host_repository.py
RENAMED
|
@@ -4,8 +4,14 @@ JSON 文件主机仓库实现
|
|
|
4
4
|
使用 JSON 文件存储主机配置,支持:
|
|
5
5
|
- 原子写入(先写临时文件再重命名,防止崩溃导致数据丢失)
|
|
6
6
|
- 可选加密(通过 CredentialEncryption 加密 password 字段)
|
|
7
|
+
- 明文持久化策略(v2.5)
|
|
7
8
|
- config version management
|
|
8
9
|
- 自动从旧版本迁移
|
|
10
|
+
|
|
11
|
+
并发语义(single-writer):
|
|
12
|
+
- 本实现面向**单进程/单写入者**"; 多进程并发写同一 JSON 文件
|
|
13
|
+
可能丢失更新(读-改-写无跨进程锁)。需要多进程/多写入者时,
|
|
14
|
+
请使用 SqliteHostRepository(WAL + busy_timeout 处理并发写)。
|
|
9
15
|
"""
|
|
10
16
|
|
|
11
17
|
import builtins
|
|
@@ -14,13 +20,15 @@ import json
|
|
|
14
20
|
import logging
|
|
15
21
|
import os
|
|
16
22
|
import tempfile
|
|
23
|
+
import warnings
|
|
17
24
|
from pathlib import Path
|
|
18
25
|
from typing import Optional
|
|
19
26
|
|
|
20
27
|
from remote_cmd.core.host import Host
|
|
21
28
|
from remote_cmd.repository.host_repository import HostRepository
|
|
22
|
-
from remote_cmd.utils.credential_guard import PasswordGuard
|
|
29
|
+
from remote_cmd.utils.credential_guard import PasswordGuard, is_plaintext_password
|
|
23
30
|
from remote_cmd.utils.crypto import CredentialEncryption
|
|
31
|
+
from remote_cmd.utils.exceptions import CredentialError, PlaintextCredentialWarning
|
|
24
32
|
|
|
25
33
|
logger = logging.getLogger(__name__)
|
|
26
34
|
|
|
@@ -36,6 +44,16 @@ class JsonHostRepository(HostRepository):
|
|
|
36
44
|
filepath: JSON 文件路径
|
|
37
45
|
encryption: 可选的凭据加密器(设置后自动加密 password)
|
|
38
46
|
auto_load: 初始化时是否自动加载已有文件(默认 True)
|
|
47
|
+
allow_plaintext_credentials: 明文密码持久化策略(v2.5;默认 None
|
|
48
|
+
为兼容模式)。三态:
|
|
49
|
+
- None:允许,但在 flush 会持久化明文密码时发出
|
|
50
|
+
PlaintextCredentialWarning(兼容 v2.4 及更早)
|
|
51
|
+
- True:显式允许明文落盘(不再告警)
|
|
52
|
+
- False:flush 遇到明文密码时抛出 CredentialError
|
|
53
|
+
v3.0 起默认值计划切换为 False。
|
|
54
|
+
|
|
55
|
+
并发语义:单进程/单写入者(见模块 docstring);多进程场景请使用
|
|
56
|
+
SqliteHostRepository。
|
|
39
57
|
"""
|
|
40
58
|
|
|
41
59
|
def __init__(
|
|
@@ -43,10 +61,12 @@ class JsonHostRepository(HostRepository):
|
|
|
43
61
|
filepath: str,
|
|
44
62
|
encryption: Optional[CredentialEncryption] = None,
|
|
45
63
|
auto_load: bool = True,
|
|
64
|
+
allow_plaintext_credentials: Optional[bool] = None,
|
|
46
65
|
) -> None:
|
|
47
66
|
self._filepath = Path(filepath)
|
|
48
67
|
self._encryption = encryption
|
|
49
68
|
self._guard = PasswordGuard(encryption)
|
|
69
|
+
self._allow_plaintext = allow_plaintext_credentials
|
|
50
70
|
self._hosts: dict[str, Host] = {}
|
|
51
71
|
|
|
52
72
|
if auto_load and self._filepath.exists():
|
|
@@ -100,7 +120,14 @@ class JsonHostRepository(HostRepository):
|
|
|
100
120
|
# ========================================================================
|
|
101
121
|
|
|
102
122
|
def flush(self) -> None:
|
|
103
|
-
"""原子写入 JSON 文件
|
|
123
|
+
"""原子写入 JSON 文件
|
|
124
|
+
|
|
125
|
+
注意:JSON 持久化是单进程/单写入者语义,多进程并发写同一文件
|
|
126
|
+
可能丢失更新(无跨进程锁);多进程场景请使用 SqliteHostRepository。
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
CredentialError: allow_plaintext_credentials=False 且存在明文密码
|
|
130
|
+
"""
|
|
104
131
|
data = self._serialize_hosts()
|
|
105
132
|
self._atomic_write(data)
|
|
106
133
|
|
|
@@ -112,12 +139,44 @@ class JsonHostRepository(HostRepository):
|
|
|
112
139
|
if self._guard.enabled:
|
|
113
140
|
for host_data in hosts_dict.values():
|
|
114
141
|
host_data["password"] = self._guard.encrypt(host_data.get("password"))
|
|
142
|
+
else:
|
|
143
|
+
self._enforce_plaintext_policy(hosts_dict)
|
|
115
144
|
|
|
116
145
|
return {
|
|
117
146
|
"version": CONFIG_VERSION,
|
|
118
147
|
"hosts": hosts_dict,
|
|
119
148
|
}
|
|
120
149
|
|
|
150
|
+
def _enforce_plaintext_policy(self, hosts_dict: dict) -> None:
|
|
151
|
+
"""执行明文密码持久化策略(未配置加密器时)。
|
|
152
|
+
|
|
153
|
+
集合所有违规主机,一次 flush 最多发出一次警告或抛出一次异常。
|
|
154
|
+
"""
|
|
155
|
+
if self._allow_plaintext is True:
|
|
156
|
+
return
|
|
157
|
+
offenders = [
|
|
158
|
+
name
|
|
159
|
+
for name, data in hosts_dict.items()
|
|
160
|
+
if is_plaintext_password(data.get("password"))
|
|
161
|
+
]
|
|
162
|
+
if not offenders:
|
|
163
|
+
return
|
|
164
|
+
if self._allow_plaintext is False:
|
|
165
|
+
raise CredentialError(
|
|
166
|
+
"refusing to persist plaintext credentials for hosts: "
|
|
167
|
+
f"{', '.join(sorted(offenders))}. "
|
|
168
|
+
"Pass encryption=... to encrypt at rest, or set "
|
|
169
|
+
"allow_plaintext_credentials=True to explicitly opt in."
|
|
170
|
+
)
|
|
171
|
+
warnings.warn(
|
|
172
|
+
f"Plaintext credentials will be persisted for {len(offenders)} host(s): "
|
|
173
|
+
f"{', '.join(sorted(offenders))}. Pass encryption=... to encrypt at rest, or "
|
|
174
|
+
"allow_plaintext_credentials=True to suppress this warning "
|
|
175
|
+
"(v3.0 will reject plaintext persistence by default).",
|
|
176
|
+
PlaintextCredentialWarning,
|
|
177
|
+
stacklevel=4,
|
|
178
|
+
)
|
|
179
|
+
|
|
121
180
|
def _load(self) -> None:
|
|
122
181
|
"""从 JSON 文件加载主机配置"""
|
|
123
182
|
try:
|
|
@@ -22,12 +22,14 @@ import logging
|
|
|
22
22
|
import sqlite3
|
|
23
23
|
import threading
|
|
24
24
|
import time
|
|
25
|
+
import warnings
|
|
25
26
|
from typing import Optional
|
|
26
27
|
|
|
27
28
|
from remote_cmd.core.host import Host
|
|
28
29
|
from remote_cmd.repository.host_repository import HostRepository
|
|
29
|
-
from remote_cmd.utils.credential_guard import PasswordGuard
|
|
30
|
+
from remote_cmd.utils.credential_guard import PasswordGuard, is_plaintext_password
|
|
30
31
|
from remote_cmd.utils.crypto import CredentialEncryption
|
|
32
|
+
from remote_cmd.utils.exceptions import CredentialError, PlaintextCredentialWarning
|
|
31
33
|
|
|
32
34
|
logger = logging.getLogger(__name__)
|
|
33
35
|
|
|
@@ -79,9 +81,19 @@ class SqliteHostRepository(HostRepository):
|
|
|
79
81
|
auto_create: 是否自动创建表和数据库,默认 True
|
|
80
82
|
encryption: 可选的凭据加密器(设置后 save() 自动加密 password)
|
|
81
83
|
busy_timeout_ms: 写锁等待上限(毫秒),默认 5000
|
|
84
|
+
allow_plaintext_credentials: 明文密码持久化策略(v2.5;默认 None
|
|
85
|
+
为兼容模式)。三态:
|
|
86
|
+
- None:允许,但 save() 将要持久化明文密码时发出
|
|
87
|
+
PlaintextCredentialWarning(兼容 v2.4 及更早)
|
|
88
|
+
- True:显式允许明文落盘(不再告警)
|
|
89
|
+
- False:save() 遇到明文密码时抛出 CredentialError
|
|
90
|
+
v3.0 起默认值计划切换为 False。
|
|
82
91
|
|
|
83
92
|
注意: 密码的加密依赖传入 encryption。若直接以明文密码调用 save()
|
|
84
93
|
且未提供 encryption,明文会被持久化到数据库。请勿绕过 HostService。
|
|
94
|
+
|
|
95
|
+
并发:WAL + busy_timeout 处理多进程并发写,适合作为多写入者后端
|
|
96
|
+
(与 JsonHostRepository 的 single-writer 语义不同)。
|
|
85
97
|
"""
|
|
86
98
|
|
|
87
99
|
def __init__(
|
|
@@ -91,12 +103,14 @@ class SqliteHostRepository(HostRepository):
|
|
|
91
103
|
auto_create: bool = True,
|
|
92
104
|
encryption: Optional[CredentialEncryption] = None,
|
|
93
105
|
busy_timeout_ms: int = DEFAULT_BUSY_TIMEOUT_MS,
|
|
106
|
+
allow_plaintext_credentials: Optional[bool] = None,
|
|
94
107
|
) -> None:
|
|
95
108
|
self._db_path = db_path
|
|
96
109
|
self._lock = threading.Lock()
|
|
97
110
|
self._encryption = encryption
|
|
98
111
|
self._guard = PasswordGuard(encryption)
|
|
99
112
|
self._busy_timeout_ms = busy_timeout_ms
|
|
113
|
+
self._allow_plaintext = allow_plaintext_credentials
|
|
100
114
|
|
|
101
115
|
if auto_create:
|
|
102
116
|
self._init_db()
|
|
@@ -246,7 +260,13 @@ class SqliteHostRepository(HostRepository):
|
|
|
246
260
|
# ========================================================================
|
|
247
261
|
|
|
248
262
|
def save(self, host: Host) -> None:
|
|
249
|
-
"""保存或更新主机
|
|
263
|
+
"""保存或更新主机
|
|
264
|
+
|
|
265
|
+
Raises:
|
|
266
|
+
CredentialError: allow_plaintext_credentials=False 且密码为明文
|
|
267
|
+
"""
|
|
268
|
+
if not self._guard.enabled:
|
|
269
|
+
self._enforce_plaintext_policy(host.name, host.password)
|
|
250
270
|
with self._lock, self._txn(write=True) as conn:
|
|
251
271
|
tags_json = json.dumps(host.tags or [], ensure_ascii=False)
|
|
252
272
|
# 配置了加密器时,明文密码先加密再落库
|
|
@@ -289,6 +309,27 @@ class SqliteHostRepository(HostRepository):
|
|
|
289
309
|
|
|
290
310
|
return self._row_to_host(row)
|
|
291
311
|
|
|
312
|
+
def _enforce_plaintext_policy(self, name: str, password: Optional[str]) -> None:
|
|
313
|
+
"""执行明文密码持久化策略(未配置加密器时)。"""
|
|
314
|
+
if self._allow_plaintext is True:
|
|
315
|
+
return
|
|
316
|
+
if not is_plaintext_password(password):
|
|
317
|
+
return
|
|
318
|
+
if self._allow_plaintext is False:
|
|
319
|
+
raise CredentialError(
|
|
320
|
+
f"refusing to persist plaintext credential for host '{name}'. "
|
|
321
|
+
"Pass encryption=... to encrypt at rest, or set "
|
|
322
|
+
"allow_plaintext_credentials=True to explicitly opt in."
|
|
323
|
+
)
|
|
324
|
+
warnings.warn(
|
|
325
|
+
f"Plaintext credential will be persisted for host '{name}'. "
|
|
326
|
+
"Pass encryption=... to encrypt at rest, or "
|
|
327
|
+
"allow_plaintext_credentials=True to suppress this warning "
|
|
328
|
+
"(v3.0 will reject plaintext persistence by default).",
|
|
329
|
+
PlaintextCredentialWarning,
|
|
330
|
+
stacklevel=4,
|
|
331
|
+
)
|
|
332
|
+
|
|
292
333
|
def delete(self, name: str) -> None:
|
|
293
334
|
"""按名称删除主机"""
|
|
294
335
|
with self._lock, self._txn(write=True) as conn:
|
|
@@ -23,7 +23,7 @@ from typing import Optional, Union
|
|
|
23
23
|
|
|
24
24
|
from remote_cmd.core.host import Host
|
|
25
25
|
from remote_cmd.core.ssh_client import CommandResult, ConnectionConfig
|
|
26
|
-
from remote_cmd.service._types import BatchHostResult
|
|
26
|
+
from remote_cmd.service._types import BatchHostResult, OutputPolicy
|
|
27
27
|
from remote_cmd.service.host_service import HostService
|
|
28
28
|
from remote_cmd.utils.exceptions import ValidationError
|
|
29
29
|
|
|
@@ -56,6 +56,36 @@ def validate_max_output_bytes(value: Optional[int]) -> Optional[int]:
|
|
|
56
56
|
return value
|
|
57
57
|
|
|
58
58
|
|
|
59
|
+
def resolve_max_output_bytes(
|
|
60
|
+
max_output_bytes: Optional[int],
|
|
61
|
+
output_policy: Optional[OutputPolicy],
|
|
62
|
+
) -> Optional[int]:
|
|
63
|
+
"""解析执行器的输出保留配置,返回生效的 max_output_bytes。
|
|
64
|
+
|
|
65
|
+
v2.5 引入 ``OutputPolicy`` 作为 ``max_output_bytes`` 的显式替代;
|
|
66
|
+
两者只能传且最多传一个,防止歧义。
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
max_output_bytes: legacy 参数(与 ``output_policy`` 互斥)
|
|
70
|
+
output_policy: ``OutputPolicy`` 实例(优先)
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Optional[int]: 生效的保留上限(``None`` 表示不限)
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
ValidationError: 两者同时给出,或值非法
|
|
77
|
+
"""
|
|
78
|
+
if max_output_bytes is not None and output_policy is not None:
|
|
79
|
+
raise ValidationError(
|
|
80
|
+
"ambiguous output policy: pass either max_output_bytes or "
|
|
81
|
+
"output_policy, not both"
|
|
82
|
+
)
|
|
83
|
+
if output_policy is not None:
|
|
84
|
+
return validate_max_output_bytes(output_policy.max_output_bytes)
|
|
85
|
+
return validate_max_output_bytes(max_output_bytes)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
59
89
|
def truncate_output(text: str, max_bytes: Optional[int]) -> str:
|
|
60
90
|
"""按 UTF-8 字节上限确定性地截断单个输出流。
|
|
61
91
|
|
|
@@ -13,10 +13,42 @@ from collections.abc import Awaitable
|
|
|
13
13
|
from dataclasses import dataclass, field
|
|
14
14
|
from typing import Callable, Optional
|
|
15
15
|
|
|
16
|
+
from remote_cmd.utils.exceptions import ValidationError
|
|
17
|
+
|
|
16
18
|
# 进度回调签名:completed, total, current_host_name;async 或 sync 均可
|
|
17
19
|
ProgressCallback = Callable[[int, int, str], Optional[Awaitable[None]]]
|
|
18
20
|
|
|
19
21
|
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class OutputPolicy:
|
|
24
|
+
"""批量执行的输出保留策略(v2.5 引入,为 v3 默认值迁移预留)。
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
max_output_bytes: 每台主机每个输出流(stdout/stderr)保留的最大
|
|
28
|
+
字节数(UTF-8)。``None`` 保留完整输出(兼容默认,与 v2.4 一致);
|
|
29
|
+
正整数时确定性截断并追加 ``[output truncated: N bytes omitted]``
|
|
30
|
+
标记(语义见 _host_runner.truncate_output)。
|
|
31
|
+
|
|
32
|
+
Raises:
|
|
33
|
+
ValidationError: max_output_bytes 非 None、非正整数
|
|
34
|
+
|
|
35
|
+
Warning:
|
|
36
|
+
``None`` 表示无保留上限:大批量 + 大输出组合下 BatchResult 的内存
|
|
37
|
+
占用可能显著。v3.0 计划改为有界默认(候选 1 MiB)。
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
max_output_bytes: Optional[int] = None
|
|
41
|
+
|
|
42
|
+
def __post_init__(self) -> None:
|
|
43
|
+
value = self.max_output_bytes
|
|
44
|
+
if value is None:
|
|
45
|
+
return
|
|
46
|
+
if isinstance(value, bool) or not isinstance(value, int) or value <= 0:
|
|
47
|
+
raise ValidationError(
|
|
48
|
+
f"max_output_bytes must be None or a positive integer, got: {value!r}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
20
52
|
@dataclass
|
|
21
53
|
class BatchHostResult:
|
|
22
54
|
"""
|
|
@@ -90,4 +122,4 @@ class BatchResult:
|
|
|
90
122
|
)
|
|
91
123
|
|
|
92
124
|
|
|
93
|
-
__all__ = ["BatchHostResult", "BatchResult", "ProgressCallback"]
|
|
125
|
+
__all__ = ["BatchHostResult", "BatchResult", "OutputPolicy", "ProgressCallback"]
|