mtr-cli 0.1.0__tar.gz → 0.3.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.
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/PKG-INFO +29 -12
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/README.md +28 -11
- mtr_cli-0.3.0/docs/intro-to-mtr.md +180 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/cli.py +63 -9
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/config.py +14 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/sync.py +157 -25
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/pyproject.toml +1 -1
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_config.py +56 -0
- mtr_cli-0.3.0/tests/unit/test_sync_rsync.py +368 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_sync_sftp.py +28 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/uv.lock +2 -2
- mtr_cli-0.1.0/tests/unit/test_sync_rsync.py +0 -142
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/.gitignore +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/.pre-commit-config.yaml +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/.python-version +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/AGENTS.md +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/LICENSE +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/examples/config.yaml +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/__init__.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/logger.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/mtr/ssh.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/__init__.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/conftest.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/integration/__init__.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/integration/test_cli_flow.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/integration/test_cli_phase1.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/__init__.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_logger.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_ssh.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_ssh_interactive.py +0 -0
- {mtr_cli-0.1.0 → mtr_cli-0.3.0}/tests/unit/test_ssh_pre_cmd.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mtr-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: A CLI tool for seamless local development and remote execution on GPU servers.
|
|
5
5
|
Project-URL: Homepage, https://github.com/lecoan/mtremote
|
|
6
6
|
Project-URL: Repository, https://github.com/lecoan/mtremote
|
|
@@ -25,7 +25,7 @@ Requires-Dist: pyyaml>=6.0
|
|
|
25
25
|
Requires-Dist: rich>=12.0.0
|
|
26
26
|
Description-Content-Type: text/markdown
|
|
27
27
|
|
|
28
|
-
# MTRemote (mtr)
|
|
28
|
+
# MTRemote (mtr-cli)
|
|
29
29
|
|
|
30
30
|
MTRemote 是一个专为 AI Infra 和 Python/C++ 混合开发设计的命令行工具。它允许你在本地修改代码,通过简单的 `mtr` 前缀,自动将代码同步到远端 GPU 服务器并执行命令,同时保留本地的交互体验(实时日志、颜色高亮、Ctrl+C 支持)。
|
|
31
31
|
|
|
@@ -48,20 +48,30 @@ MTRemote 是一个专为 AI Infra 和 Python/C++ 混合开发设计的命令行
|
|
|
48
48
|
推荐使用 `uv` 或 `pipx` 安装:
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
|
-
uv tool install
|
|
51
|
+
uv tool install mtr-cli
|
|
52
52
|
# 或者
|
|
53
|
-
pip install
|
|
53
|
+
pip install mtr-cli
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
### 系统依赖
|
|
57
57
|
|
|
58
58
|
MTRemote 需要以下系统命令:
|
|
59
59
|
|
|
60
|
-
| 命令 | 用途 | 安装方式 |
|
|
61
|
-
|
|
62
|
-
| `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` |
|
|
63
|
-
| `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 |
|
|
64
|
-
| `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) |
|
|
60
|
+
| 命令 | 用途 | 安装方式 | 版本要求 |
|
|
61
|
+
|------|------|----------|----------|
|
|
62
|
+
| `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` | - |
|
|
63
|
+
| `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 | **≥ 3.1.0** (TTY 进度显示需要) |
|
|
64
|
+
| `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) | - |
|
|
65
|
+
|
|
66
|
+
**注意**:macOS 自带的 rsync 版本较旧(2.6.9),不支持 TTY 模式下的进度显示。建议通过 Homebrew 安装新版:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# macOS 用户建议升级 rsync
|
|
70
|
+
brew install rsync
|
|
71
|
+
|
|
72
|
+
# 验证版本
|
|
73
|
+
rsync --version # 应显示 3.1.0 或更高版本
|
|
74
|
+
```
|
|
65
75
|
|
|
66
76
|
**注意**:交互式 Shell 功能(如 `mtr bash`, `mtr ipython`)**必须**安装 `ssh`。密码认证**必须**安装 `sshpass`。
|
|
67
77
|
|
|
@@ -129,7 +139,7 @@ Options:
|
|
|
129
139
|
--to TEXT Local destination path for download (optional)
|
|
130
140
|
--enable-log Enable logging to file
|
|
131
141
|
--log-level TEXT Log level: DEBUG/INFO/WARNING/ERROR [default: INFO]
|
|
132
|
-
--log-file PATH Custom log file path (default:
|
|
142
|
+
--log-file PATH Custom log file path (default: ./.mtr/logs/mtr_YYYYMMDD_HHMMSS.log)
|
|
133
143
|
--init Initialize configuration file
|
|
134
144
|
--help Show this message and exit
|
|
135
145
|
```
|
|
@@ -192,9 +202,12 @@ sudo yum install sshpass
|
|
|
192
202
|
使用 `--get` 参数可以从远端服务器下载文件或文件夹到本地:
|
|
193
203
|
|
|
194
204
|
```bash
|
|
195
|
-
#
|
|
205
|
+
# 下载文件(绝对路径)
|
|
196
206
|
mtr --get /remote/path/to/file.txt
|
|
197
207
|
|
|
208
|
+
# 下载文件(相对路径,基于 remote_dir)
|
|
209
|
+
mtr --get checkpoints/model.pt
|
|
210
|
+
|
|
198
211
|
# 下载文件到指定位置
|
|
199
212
|
mtr --get /remote/path/to/file.txt --to ./local/path/
|
|
200
213
|
|
|
@@ -205,6 +218,10 @@ mtr --get /remote/path/to/checkpoints/ --to ./backups/
|
|
|
205
218
|
mtr --no-sync --get /remote/path/to/file.txt
|
|
206
219
|
```
|
|
207
220
|
|
|
221
|
+
**路径解析规则**:
|
|
222
|
+
- **绝对路径**(以 `/` 开头):直接使用指定的完整路径
|
|
223
|
+
- **相对路径**:自动拼接 `remote_dir`,例如配置 `remote_dir: "/workdir/project"`,执行 `--get checkpoints/model.pt` 将下载 `/workdir/project/checkpoints/model.pt`
|
|
224
|
+
|
|
208
225
|
**配置下载目录**:
|
|
209
226
|
可以在配置文件中设置默认下载位置:
|
|
210
227
|
|
|
@@ -235,7 +252,7 @@ mtr --enable-log python train.py
|
|
|
235
252
|
mtr --enable-log --log-level DEBUG python train.py
|
|
236
253
|
|
|
237
254
|
# 查看日志
|
|
238
|
-
cat
|
|
255
|
+
cat ./.mtr/logs/mtr_20260128_171216.log
|
|
239
256
|
```
|
|
240
257
|
|
|
241
258
|
日志文件按会话独立生成,格式为 `mtr_YYYYMMDD_HHMMSS.log`,包含:
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# MTRemote (mtr)
|
|
1
|
+
# MTRemote (mtr-cli)
|
|
2
2
|
|
|
3
3
|
MTRemote 是一个专为 AI Infra 和 Python/C++ 混合开发设计的命令行工具。它允许你在本地修改代码,通过简单的 `mtr` 前缀,自动将代码同步到远端 GPU 服务器并执行命令,同时保留本地的交互体验(实时日志、颜色高亮、Ctrl+C 支持)。
|
|
4
4
|
|
|
@@ -21,20 +21,30 @@ MTRemote 是一个专为 AI Infra 和 Python/C++ 混合开发设计的命令行
|
|
|
21
21
|
推荐使用 `uv` 或 `pipx` 安装:
|
|
22
22
|
|
|
23
23
|
```bash
|
|
24
|
-
uv tool install
|
|
24
|
+
uv tool install mtr-cli
|
|
25
25
|
# 或者
|
|
26
|
-
pip install
|
|
26
|
+
pip install mtr-cli
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
### 系统依赖
|
|
30
30
|
|
|
31
31
|
MTRemote 需要以下系统命令:
|
|
32
32
|
|
|
33
|
-
| 命令 | 用途 | 安装方式 |
|
|
34
|
-
|
|
35
|
-
| `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` |
|
|
36
|
-
| `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 |
|
|
37
|
-
| `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) |
|
|
33
|
+
| 命令 | 用途 | 安装方式 | 版本要求 |
|
|
34
|
+
|------|------|----------|----------|
|
|
35
|
+
| `ssh` | 交互式 Shell (TTY) | macOS/Linux 自带,或 `brew install openssh` | - |
|
|
36
|
+
| `rsync` | 快速文件同步 (推荐) | macOS/Linux 自带 | **≥ 3.1.0** (TTY 进度显示需要) |
|
|
37
|
+
| `sshpass` | 密码认证 (可选) | `brew install hudochenkov/sshpass/sshpass` (macOS) / `apt install sshpass` (Ubuntu) | - |
|
|
38
|
+
|
|
39
|
+
**注意**:macOS 自带的 rsync 版本较旧(2.6.9),不支持 TTY 模式下的进度显示。建议通过 Homebrew 安装新版:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# macOS 用户建议升级 rsync
|
|
43
|
+
brew install rsync
|
|
44
|
+
|
|
45
|
+
# 验证版本
|
|
46
|
+
rsync --version # 应显示 3.1.0 或更高版本
|
|
47
|
+
```
|
|
38
48
|
|
|
39
49
|
**注意**:交互式 Shell 功能(如 `mtr bash`, `mtr ipython`)**必须**安装 `ssh`。密码认证**必须**安装 `sshpass`。
|
|
40
50
|
|
|
@@ -102,7 +112,7 @@ Options:
|
|
|
102
112
|
--to TEXT Local destination path for download (optional)
|
|
103
113
|
--enable-log Enable logging to file
|
|
104
114
|
--log-level TEXT Log level: DEBUG/INFO/WARNING/ERROR [default: INFO]
|
|
105
|
-
--log-file PATH Custom log file path (default:
|
|
115
|
+
--log-file PATH Custom log file path (default: ./.mtr/logs/mtr_YYYYMMDD_HHMMSS.log)
|
|
106
116
|
--init Initialize configuration file
|
|
107
117
|
--help Show this message and exit
|
|
108
118
|
```
|
|
@@ -165,9 +175,12 @@ sudo yum install sshpass
|
|
|
165
175
|
使用 `--get` 参数可以从远端服务器下载文件或文件夹到本地:
|
|
166
176
|
|
|
167
177
|
```bash
|
|
168
|
-
#
|
|
178
|
+
# 下载文件(绝对路径)
|
|
169
179
|
mtr --get /remote/path/to/file.txt
|
|
170
180
|
|
|
181
|
+
# 下载文件(相对路径,基于 remote_dir)
|
|
182
|
+
mtr --get checkpoints/model.pt
|
|
183
|
+
|
|
171
184
|
# 下载文件到指定位置
|
|
172
185
|
mtr --get /remote/path/to/file.txt --to ./local/path/
|
|
173
186
|
|
|
@@ -178,6 +191,10 @@ mtr --get /remote/path/to/checkpoints/ --to ./backups/
|
|
|
178
191
|
mtr --no-sync --get /remote/path/to/file.txt
|
|
179
192
|
```
|
|
180
193
|
|
|
194
|
+
**路径解析规则**:
|
|
195
|
+
- **绝对路径**(以 `/` 开头):直接使用指定的完整路径
|
|
196
|
+
- **相对路径**:自动拼接 `remote_dir`,例如配置 `remote_dir: "/workdir/project"`,执行 `--get checkpoints/model.pt` 将下载 `/workdir/project/checkpoints/model.pt`
|
|
197
|
+
|
|
181
198
|
**配置下载目录**:
|
|
182
199
|
可以在配置文件中设置默认下载位置:
|
|
183
200
|
|
|
@@ -208,7 +225,7 @@ mtr --enable-log python train.py
|
|
|
208
225
|
mtr --enable-log --log-level DEBUG python train.py
|
|
209
226
|
|
|
210
227
|
# 查看日志
|
|
211
|
-
cat
|
|
228
|
+
cat ./.mtr/logs/mtr_20260128_171216.log
|
|
212
229
|
```
|
|
213
230
|
|
|
214
231
|
日志文件按会话独立生成,格式为 `mtr_YYYYMMDD_HHMMSS.log`,包含:
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# mtr-cli:远程训练框架开发的工作流优化
|
|
2
|
+
|
|
3
|
+
## 背景与问题
|
|
4
|
+
|
|
5
|
+
在训练框架开发过程中,我们面临一个结构性矛盾:代码开发在本地进行,但功能验证必须在远程 GPU/NPU 集群上完成。这种"本地-远程"的割裂带来了三个核心问题。
|
|
6
|
+
|
|
7
|
+
### 网络与环境隔离
|
|
8
|
+
|
|
9
|
+
训练集群通常部署在内网或隔离环境中,无法直接访问外部网络。这导致依赖安装困难,而训练框架的依赖链本身就很复杂(PyTorch、DeepSpeed、Megatron 等)。环境配置耗时较长,一旦集群环境需要重建,成本很高。
|
|
10
|
+
|
|
11
|
+
### 多集群切换成本高
|
|
12
|
+
|
|
13
|
+
框架开发需要在 GPU 集群和 NPU 集群之间频繁切换验证。每次切换涉及代码同步、路径检查、环境确认等重复性操作。虽然启动方式统一使用 torchrun,但不同集群的节点地址、Python 环境路径等配置存在差异,需要反复适配。
|
|
14
|
+
|
|
15
|
+
### AI Agent 集成受限
|
|
16
|
+
|
|
17
|
+
训练任务在远程集群执行时,代码报错和异常堆栈输出在远程终端。AI Agent 无法直接访问这些信息,开发者需要手动复制粘贴错误内容,打断开发流程。Agent 无法实时感知执行状态,失去了即时辅助调试的能力。
|
|
18
|
+
|
|
19
|
+
## 解决方案:mtr-cli
|
|
20
|
+
|
|
21
|
+
mtr-cli 采用"本地开发,远程执行"的架构,在保持本地开发体验的同时,实现与远程集群的无缝集成。
|
|
22
|
+
|
|
23
|
+
### 核心功能
|
|
24
|
+
|
|
25
|
+
**智能代码同步**
|
|
26
|
+
|
|
27
|
+
支持 rsync(增量同步,速度快)和 SFTP(兼容性好)两种引擎。自动过滤 .git、__pycache__ 等无需同步的文件,避免大数据集误传。
|
|
28
|
+
|
|
29
|
+
**多集群配置管理**
|
|
30
|
+
|
|
31
|
+
通过配置文件集中管理多个训练集群:
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
servers:
|
|
35
|
+
gpu-cluster:
|
|
36
|
+
host: "gpu-node-01"
|
|
37
|
+
user: "dev"
|
|
38
|
+
remote_dir: "/data/train-project"
|
|
39
|
+
|
|
40
|
+
npu-cluster:
|
|
41
|
+
host: "npu-node-01"
|
|
42
|
+
user: "dev"
|
|
43
|
+
remote_dir: "/home/dev/project"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
集群切换通过命令行参数完成:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# GPU 集群执行
|
|
50
|
+
mtr -s gpu-cluster torchrun --nproc_per_node=8 train.py
|
|
51
|
+
|
|
52
|
+
# NPU 集群执行
|
|
53
|
+
mtr -s npu-cluster torchrun --nproc_per_node=8 train.py
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**实时流式输出**
|
|
57
|
+
|
|
58
|
+
远程训练日志实时回显至本地终端,包括 loss 曲线、吞吐量、显存占用等关键指标。代码异常时,堆栈信息直接显示在本地,AI Agent 可即时获取并分析。支持交互式命令(vim、ipython 等)。
|
|
59
|
+
|
|
60
|
+
## 应用场景
|
|
61
|
+
|
|
62
|
+
**分布式训练框架开发**
|
|
63
|
+
|
|
64
|
+
在本地完成数据并行或模型并行逻辑的实现后,需要上多卡环境验证。传统流程涉及打包代码、scp 传输、ssh 登录、路径定位、执行运行等多个步骤。使用 mtr-cli 可简化为:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
mtr torchrun --nproc_per_node=8 train.py
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
代码自动同步,远程执行,日志实时回传。
|
|
71
|
+
|
|
72
|
+
**跨硬件平台验证**
|
|
73
|
+
|
|
74
|
+
框架在 GPU 环境验证通过后,需要在 NPU 环境测试兼容性。传统方式需要重复环境配置和代码同步流程。使用 mtr-cli 只需切换服务器参数:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
mtr -s npu-cluster torchrun --nproc_per_node=8 train.py
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**即时调试**
|
|
81
|
+
|
|
82
|
+
训练脚本执行过程中发生异常,报错信息和堆栈直接输出在本地终端。AI Agent 实时获取错误内容,可立即进行分析和建议,无需手动复制粘贴。
|
|
83
|
+
|
|
84
|
+
## 收益分析
|
|
85
|
+
|
|
86
|
+
- **时间效率**:消除手动 scp/rsync 和反复 ssh 登录的操作 overhead
|
|
87
|
+
- **认知负担**:本地维护单一的代码库和配置,远程仅作为计算资源
|
|
88
|
+
- **AI 辅助能力**:Agent 可获取完整执行日志,恢复实时调试辅助
|
|
89
|
+
- **流程一致性**:屏蔽 GPU/NPU 后端差异,提供统一的开发体验
|
|
90
|
+
|
|
91
|
+
## 快速开始
|
|
92
|
+
|
|
93
|
+
### 安装
|
|
94
|
+
|
|
95
|
+
推荐使用 uv 安装(更快、更可靠):
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
uv pip install mtr-cli
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
或使用 pip:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pip install mtr-cli
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 初始化配置
|
|
108
|
+
|
|
109
|
+
在项目目录下运行初始化命令,生成默认配置文件:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
mtr --init
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
这会创建 `.mtr/config.yaml` 文件,包含默认配置模板。根据你的集群信息编辑该文件:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
servers:
|
|
119
|
+
dev-gpu:
|
|
120
|
+
host: "192.168.1.10"
|
|
121
|
+
user: "dev"
|
|
122
|
+
key_filename: "~/.ssh/id_rsa"
|
|
123
|
+
remote_dir: "/data/project"
|
|
124
|
+
sync: "rsync"
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 执行命令
|
|
128
|
+
|
|
129
|
+
配置完成后,在项目根目录执行:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# 使用默认服务器
|
|
133
|
+
mtr python train.py
|
|
134
|
+
|
|
135
|
+
# 指定服务器
|
|
136
|
+
mtr -s dev-gpu torchrun --nproc_per_node=8 train.py
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
完整 workflow:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
# 1. 安装
|
|
143
|
+
uv pip install mtr-cli
|
|
144
|
+
|
|
145
|
+
# 2. 进入项目目录
|
|
146
|
+
cd my-project
|
|
147
|
+
|
|
148
|
+
# 3. 初始化配置
|
|
149
|
+
mtr --init
|
|
150
|
+
# 编辑 .mtr/config.yaml 添加服务器信息
|
|
151
|
+
|
|
152
|
+
# 4. 运行
|
|
153
|
+
mtr python train.py
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 常用参数
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
# 指定服务器
|
|
160
|
+
mtr -s gpu-cluster python train.py
|
|
161
|
+
|
|
162
|
+
# 跳过代码同步(仅执行命令)
|
|
163
|
+
mtr --no-sync python train.py
|
|
164
|
+
|
|
165
|
+
# 预览将要执行的命令(不实际运行)
|
|
166
|
+
mtr --dry-run python train.py
|
|
167
|
+
|
|
168
|
+
# 强制禁用 TTY(用于日志记录场景)
|
|
169
|
+
mtr --no-tty python train.py
|
|
170
|
+
|
|
171
|
+
# 启用日志记录
|
|
172
|
+
mtr --enable-log python train.py
|
|
173
|
+
|
|
174
|
+
# 从远程下载文件
|
|
175
|
+
mtr --get /remote/path/to/file.txt --to ./local/file.txt
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
mtr-cli 的目标是将远程训练的日常操作自动化,使开发者能够专注于框架本身,而非环境切换的繁琐流程。
|
|
@@ -15,6 +15,11 @@ defaults:
|
|
|
15
15
|
# 选项: "rsync" (推荐), "sftp"
|
|
16
16
|
sync: "rsync"
|
|
17
17
|
|
|
18
|
+
# 是否尊重 .gitignore 文件(仅 rsync 模式支持)
|
|
19
|
+
# 设置为 true 时,rsync 会自动读取项目根目录的 .gitignore 并排除匹配的文件
|
|
20
|
+
# SFTP 模式不支持此选项,如启用会报错
|
|
21
|
+
respect_gitignore: true
|
|
22
|
+
|
|
18
23
|
exclude:
|
|
19
24
|
- ".git/"
|
|
20
25
|
- "__pycache__/"
|
|
@@ -73,7 +78,7 @@ def _init_config():
|
|
|
73
78
|
@click.option("--init", is_flag=True, help="Initialize a configuration file in current directory")
|
|
74
79
|
@click.option("--enable-log", is_flag=True, help="Enable logging to file")
|
|
75
80
|
@click.option("--log-level", default="INFO", help="Log level (DEBUG/INFO/WARNING/ERROR)")
|
|
76
|
-
@click.option("--log-file", help="Path to log file (default:
|
|
81
|
+
@click.option("--log-file", help="Path to log file (default: ./.mtr/logs/mtr_YYYYMMDD_HHMMSS.log)")
|
|
77
82
|
@click.option("--get", "remote_get_path", help="Remote path to download from")
|
|
78
83
|
@click.option("--to", "local_dest_path", help="Local destination path for download (optional)")
|
|
79
84
|
@click.argument("command", nargs=-1, type=click.UNPROCESSED)
|
|
@@ -86,9 +91,9 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
86
91
|
# Setup logging if enabled
|
|
87
92
|
if enable_log:
|
|
88
93
|
if not log_file:
|
|
89
|
-
# Generate default log file path:
|
|
94
|
+
# Generate default log file path: ./.mtr/logs/mtr_YYYYMMDD_HHMMSS.log
|
|
90
95
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
91
|
-
log_dir = os.path.
|
|
96
|
+
log_dir = os.path.join(os.getcwd(), ".mtr/logs")
|
|
92
97
|
log_file = os.path.join(log_dir, f"mtr_{timestamp}.log")
|
|
93
98
|
|
|
94
99
|
try:
|
|
@@ -186,6 +191,9 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
186
191
|
# Resolve exclude
|
|
187
192
|
exclude = config.global_defaults.get("exclude", []) + server_conf.get("exclude", [])
|
|
188
193
|
|
|
194
|
+
# Get respect_gitignore setting
|
|
195
|
+
respect_gitignore = config.get_respect_gitignore()
|
|
196
|
+
|
|
189
197
|
# Determine engine
|
|
190
198
|
engine = server_conf.get("sync", config.global_defaults.get("sync", "rsync"))
|
|
191
199
|
|
|
@@ -199,6 +207,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
199
207
|
password=password,
|
|
200
208
|
port=port,
|
|
201
209
|
exclude=exclude,
|
|
210
|
+
respect_gitignore=respect_gitignore,
|
|
202
211
|
)
|
|
203
212
|
elif engine == "sftp":
|
|
204
213
|
syncer = SftpSyncer(
|
|
@@ -210,6 +219,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
210
219
|
password=password,
|
|
211
220
|
port=port,
|
|
212
221
|
exclude=exclude,
|
|
222
|
+
respect_gitignore=respect_gitignore,
|
|
213
223
|
)
|
|
214
224
|
else:
|
|
215
225
|
click.secho(
|
|
@@ -225,11 +235,28 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
225
235
|
logger.info(f"[DryRun] Would sync {local_dir} -> {remote_dir}", module="mtr.sync")
|
|
226
236
|
else:
|
|
227
237
|
if is_interactive and console:
|
|
228
|
-
|
|
229
|
-
|
|
238
|
+
# TTY mode: single line real-time update using Rich Live
|
|
239
|
+
from rich.live import Live
|
|
240
|
+
from rich.text import Text
|
|
241
|
+
|
|
242
|
+
with Live(Text("Starting sync...", style="blue"), refresh_per_second=10) as live:
|
|
243
|
+
|
|
244
|
+
def show_sync_progress(filename):
|
|
245
|
+
# Get relative path for cleaner display
|
|
246
|
+
rel_path = os.path.relpath(filename, local_dir)
|
|
247
|
+
live.update(Text(f"Syncing: {rel_path}", style="blue"))
|
|
248
|
+
|
|
249
|
+
syncer.sync(show_progress=True, progress_callback=show_sync_progress)
|
|
250
|
+
live.update(Text("Sync completed!", style="green"))
|
|
230
251
|
else:
|
|
252
|
+
# no_tty mode: print each file on new line
|
|
253
|
+
def show_sync_progress(filename):
|
|
254
|
+
rel_path = os.path.relpath(filename, local_dir)
|
|
255
|
+
click.echo(f"Syncing: {rel_path}")
|
|
256
|
+
|
|
231
257
|
click.secho("Syncing code...", fg="blue")
|
|
232
|
-
syncer.sync()
|
|
258
|
+
syncer.sync(show_progress=True, progress_callback=show_sync_progress)
|
|
259
|
+
click.secho("Sync completed!", fg="green")
|
|
233
260
|
logger.info(f"Sync completed: {local_dir} -> {remote_dir}", module="mtr.sync")
|
|
234
261
|
except SyncError as e:
|
|
235
262
|
logger.error(f"Sync failed: {e}", module="mtr.sync")
|
|
@@ -238,6 +265,13 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
238
265
|
|
|
239
266
|
# 3. Download from remote (if --get is specified)
|
|
240
267
|
if remote_get_path:
|
|
268
|
+
# Resolve relative remote path from remote_dir
|
|
269
|
+
if not remote_get_path.startswith("/"):
|
|
270
|
+
if not remote_dir:
|
|
271
|
+
click.secho("Error: 'remote_dir' is required for relative --get path.", fg="red", err=True)
|
|
272
|
+
sys.exit(1)
|
|
273
|
+
remote_get_path = os.path.join(remote_dir, remote_get_path)
|
|
274
|
+
|
|
241
275
|
# Resolve local destination path
|
|
242
276
|
if cli_dest:
|
|
243
277
|
local_dest = cli_dest
|
|
@@ -253,6 +287,9 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
253
287
|
# Resolve exclude
|
|
254
288
|
exclude = config.global_defaults.get("exclude", []) + server_conf.get("exclude", [])
|
|
255
289
|
|
|
290
|
+
# Get respect_gitignore setting
|
|
291
|
+
respect_gitignore = config.get_respect_gitignore()
|
|
292
|
+
|
|
256
293
|
# Determine engine
|
|
257
294
|
engine = server_conf.get("sync", config.global_defaults.get("sync", "rsync"))
|
|
258
295
|
|
|
@@ -266,6 +303,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
266
303
|
password=password,
|
|
267
304
|
port=port,
|
|
268
305
|
exclude=exclude,
|
|
306
|
+
respect_gitignore=respect_gitignore,
|
|
269
307
|
)
|
|
270
308
|
elif engine == "sftp":
|
|
271
309
|
syncer = SftpSyncer(
|
|
@@ -277,6 +315,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
277
315
|
password=password,
|
|
278
316
|
port=port,
|
|
279
317
|
exclude=exclude,
|
|
318
|
+
respect_gitignore=respect_gitignore,
|
|
280
319
|
)
|
|
281
320
|
else:
|
|
282
321
|
click.secho(
|
|
@@ -291,12 +330,27 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
|
|
|
291
330
|
logger.info(f"[DryRun] Would download {remote_get_path} -> {local_dest}", module="mtr.sync")
|
|
292
331
|
else:
|
|
293
332
|
if is_interactive and console:
|
|
294
|
-
|
|
295
|
-
|
|
333
|
+
# TTY mode: single line real-time update using Rich Live
|
|
334
|
+
from rich.live import Live
|
|
335
|
+
from rich.text import Text
|
|
336
|
+
|
|
337
|
+
with Live(Text("Starting download...", style="blue"), refresh_per_second=10) as live:
|
|
338
|
+
|
|
339
|
+
def show_download_progress(filename):
|
|
340
|
+
live.update(Text(f"Downloading: {filename}", style="blue"))
|
|
341
|
+
|
|
342
|
+
syncer.download(
|
|
343
|
+
remote_get_path, local_dest, show_progress=True, progress_callback=show_download_progress
|
|
344
|
+
)
|
|
345
|
+
live.update(Text("Download completed!", style="green"))
|
|
296
346
|
console.print(f"✅ [green]Downloaded:[/green] {remote_get_path} -> {local_dest}")
|
|
297
347
|
else:
|
|
348
|
+
# no_tty mode: print each file on new line
|
|
349
|
+
def show_download_progress(filename):
|
|
350
|
+
click.echo(f"Downloading: {filename}")
|
|
351
|
+
|
|
298
352
|
click.secho(f"Downloading {remote_get_path}...", fg="blue")
|
|
299
|
-
syncer.download(remote_get_path, local_dest)
|
|
353
|
+
syncer.download(remote_get_path, local_dest, show_progress=True, progress_callback=show_download_progress)
|
|
300
354
|
click.secho(f"Download completed: {local_dest}", fg="green")
|
|
301
355
|
logger.info(f"Download completed: {remote_get_path} -> {local_dest}", module="mtr.sync")
|
|
302
356
|
except SyncError as e:
|
|
@@ -19,6 +19,20 @@ class Config:
|
|
|
19
19
|
server_config: Dict[str, Any]
|
|
20
20
|
global_defaults: Dict[str, Any]
|
|
21
21
|
|
|
22
|
+
def get_respect_gitignore(self) -> bool:
|
|
23
|
+
"""Get respect_gitignore setting, default True.
|
|
24
|
+
|
|
25
|
+
Priority: server config > global defaults > True (default)
|
|
26
|
+
"""
|
|
27
|
+
# Check server config first
|
|
28
|
+
if "respect_gitignore" in self.server_config:
|
|
29
|
+
return self.server_config["respect_gitignore"]
|
|
30
|
+
# Then check global defaults
|
|
31
|
+
if "respect_gitignore" in self.global_defaults:
|
|
32
|
+
return self.global_defaults["respect_gitignore"]
|
|
33
|
+
# Default to True
|
|
34
|
+
return True
|
|
35
|
+
|
|
22
36
|
|
|
23
37
|
class ConfigLoader:
|
|
24
38
|
def __init__(self, config_path: Optional[str] = None):
|