mtr-cli 0.2.0__py3-none-any.whl → 0.3.0__py3-none-any.whl

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.py CHANGED
@@ -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__/"
@@ -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
- with console.status("[bold blue]Syncing code...", spinner="dots"):
229
- syncer.sync()
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")
@@ -260,6 +287,9 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
260
287
  # Resolve exclude
261
288
  exclude = config.global_defaults.get("exclude", []) + server_conf.get("exclude", [])
262
289
 
290
+ # Get respect_gitignore setting
291
+ respect_gitignore = config.get_respect_gitignore()
292
+
263
293
  # Determine engine
264
294
  engine = server_conf.get("sync", config.global_defaults.get("sync", "rsync"))
265
295
 
@@ -273,6 +303,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
273
303
  password=password,
274
304
  port=port,
275
305
  exclude=exclude,
306
+ respect_gitignore=respect_gitignore,
276
307
  )
277
308
  elif engine == "sftp":
278
309
  syncer = SftpSyncer(
@@ -284,6 +315,7 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
284
315
  password=password,
285
316
  port=port,
286
317
  exclude=exclude,
318
+ respect_gitignore=respect_gitignore,
287
319
  )
288
320
  else:
289
321
  click.secho(
@@ -298,12 +330,27 @@ def cli(server, sync, dry_run, tty, init, enable_log, log_level, log_file, remot
298
330
  logger.info(f"[DryRun] Would download {remote_get_path} -> {local_dest}", module="mtr.sync")
299
331
  else:
300
332
  if is_interactive and console:
301
- with console.status(f"[bold blue]Downloading {remote_get_path}...", spinner="dots"):
302
- syncer.download(remote_get_path, local_dest)
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"))
303
346
  console.print(f"✅ [green]Downloaded:[/green] {remote_get_path} -> {local_dest}")
304
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
+
305
352
  click.secho(f"Downloading {remote_get_path}...", fg="blue")
306
- syncer.download(remote_get_path, local_dest)
353
+ syncer.download(remote_get_path, local_dest, show_progress=True, progress_callback=show_download_progress)
307
354
  click.secho(f"Download completed: {local_dest}", fg="green")
308
355
  logger.info(f"Download completed: {remote_get_path} -> {local_dest}", module="mtr.sync")
309
356
  except SyncError as e:
mtr/config.py CHANGED
@@ -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):
mtr/sync.py CHANGED
@@ -14,13 +14,14 @@ class SyncError(Exception):
14
14
 
15
15
 
16
16
  class BaseSyncer(ABC):
17
- def __init__(self, local_dir: str, remote_dir: str, exclude: List[str]):
17
+ def __init__(self, local_dir: str, remote_dir: str, exclude: List[str], respect_gitignore: bool = True):
18
18
  self.local_dir = local_dir
19
19
  self.remote_dir = remote_dir
20
20
  self.exclude = exclude
21
+ self.respect_gitignore = respect_gitignore
21
22
 
22
23
  @abstractmethod
23
- def sync(self):
24
+ def sync(self, show_progress: bool = False, progress_callback=None):
24
25
  pass
25
26
 
26
27
 
@@ -35,8 +36,9 @@ class SftpSyncer(BaseSyncer):
35
36
  password: Optional[str] = None,
36
37
  port: int = 22,
37
38
  exclude: List[str] = None,
39
+ respect_gitignore: bool = True,
38
40
  ):
39
- super().__init__(local_dir, remote_dir, exclude or [])
41
+ super().__init__(local_dir, remote_dir, exclude or [], respect_gitignore)
40
42
  self.host = host
41
43
  self.user = user
42
44
  self.key_filename = key_filename
@@ -45,6 +47,13 @@ class SftpSyncer(BaseSyncer):
45
47
  self.transport = None
46
48
  self.sftp = None
47
49
 
50
+ # SFTP mode does not support respect_gitignore
51
+ if respect_gitignore:
52
+ raise SyncError(
53
+ "respect_gitignore is not supported in SFTP mode. "
54
+ "Please set respect_gitignore to false in your config or use rsync mode."
55
+ )
56
+
48
57
  def _should_ignore(self, filename: str) -> bool:
49
58
  for pattern in self.exclude:
50
59
  # Handle directory exclusion (basic)
@@ -110,7 +119,7 @@ class SftpSyncer(BaseSyncer):
110
119
  except OSError:
111
120
  pass # Already exists maybe
112
121
 
113
- def sync(self):
122
+ def sync(self, show_progress: bool = False, progress_callback=None):
114
123
  if not self.sftp:
115
124
  self._connect()
116
125
 
@@ -156,7 +165,9 @@ class SftpSyncer(BaseSyncer):
156
165
  pass # Does not exist, must upload
157
166
 
158
167
  if should_upload:
159
- # print(f"Uploading {local_file} -> {remote_file}")
168
+ # Call progress callback if provided
169
+ if show_progress and progress_callback:
170
+ progress_callback(local_file)
160
171
  self.sftp.put(local_file, remote_file)
161
172
  # Preserve permissions
162
173
  mode = os.stat(local_file).st_mode
@@ -167,7 +178,7 @@ class SftpSyncer(BaseSyncer):
167
178
  if self.transport:
168
179
  self.transport.close()
169
180
 
170
- def download(self, remote_path: str, local_path: str):
181
+ def download(self, remote_path: str, local_path: str, show_progress: bool = False, progress_callback=None):
171
182
  """Download file or directory from remote to local."""
172
183
  if not self.sftp:
173
184
  self._connect()
@@ -185,9 +196,13 @@ class SftpSyncer(BaseSyncer):
185
196
  is_dir = stat.S_ISDIR(remote_stat.st_mode)
186
197
 
187
198
  if is_dir:
188
- self._download_dir(remote_path, local_path)
199
+ self._download_dir(
200
+ remote_path, local_path, show_progress=show_progress, progress_callback=progress_callback
201
+ )
189
202
  else:
190
- self._download_file(remote_path, local_path)
203
+ self._download_file(
204
+ remote_path, local_path, show_progress=show_progress, progress_callback=progress_callback
205
+ )
191
206
  except FileNotFoundError:
192
207
  raise SyncError(f"Remote path not found: {remote_path}")
193
208
  except Exception as e:
@@ -213,17 +228,21 @@ class SftpSyncer(BaseSyncer):
213
228
  except FileNotFoundError:
214
229
  return True # Local file doesn't exist, must download
215
230
 
216
- def _download_file(self, remote_file: str, local_file: str):
231
+ def _download_file(self, remote_file: str, local_file: str, show_progress: bool = False, progress_callback=None):
217
232
  """Download a single file with incremental check."""
218
233
  if not self._should_download_file(remote_file, local_file):
219
234
  return # No need to download
220
235
 
236
+ # Call progress callback if provided
237
+ if show_progress and progress_callback:
238
+ progress_callback(remote_file)
239
+
221
240
  self.sftp.get(remote_file, local_file)
222
241
  # Preserve permissions
223
242
  remote_stat = self.sftp.stat(remote_file)
224
243
  os.chmod(local_file, remote_stat.st_mode)
225
244
 
226
- def _download_dir(self, remote_dir: str, local_dir: str):
245
+ def _download_dir(self, remote_dir: str, local_dir: str, show_progress: bool = False, progress_callback=None):
227
246
  """Recursively download a directory."""
228
247
  if not os.path.exists(local_dir):
229
248
  os.makedirs(local_dir, exist_ok=True)
@@ -238,9 +257,9 @@ class SftpSyncer(BaseSyncer):
238
257
  import stat
239
258
 
240
259
  if stat.S_ISDIR(entry.st_mode):
241
- self._download_dir(remote_path, local_path)
260
+ self._download_dir(remote_path, local_path, show_progress=show_progress, progress_callback=progress_callback)
242
261
  else:
243
- self._download_file(remote_path, local_path)
262
+ self._download_file(remote_path, local_path, show_progress=show_progress, progress_callback=progress_callback)
244
263
 
245
264
 
246
265
  class RsyncSyncer(BaseSyncer):
@@ -254,8 +273,9 @@ class RsyncSyncer(BaseSyncer):
254
273
  password: Optional[str] = None,
255
274
  port: int = 22,
256
275
  exclude: List[str] = None,
276
+ respect_gitignore: bool = True,
257
277
  ):
258
- super().__init__(local_dir, remote_dir, exclude or [])
278
+ super().__init__(local_dir, remote_dir, exclude or [], respect_gitignore)
259
279
  self.host = host
260
280
  self.user = user
261
281
  self.key_filename = key_filename
@@ -269,9 +289,20 @@ class RsyncSyncer(BaseSyncer):
269
289
  opts += f" -i {self.key_filename}"
270
290
  return opts
271
291
 
272
- def _build_rsync_base(self) -> List[str]:
292
+ def _build_rsync_base(self, show_progress: bool = False) -> List[str]:
273
293
  """Build rsync base command with common options."""
274
- cmd = ["rsync", "-azq"]
294
+ if show_progress:
295
+ # In progress mode, use -av --info=NAME to show filenames only
296
+ cmd = ["rsync", "-av", "--info=NAME"]
297
+ else:
298
+ # Silent mode
299
+ cmd = ["rsync", "-azq"]
300
+
301
+ # Add gitignore filter if enabled
302
+ if self.respect_gitignore:
303
+ gitignore_path = os.path.join(self.local_dir, ".gitignore")
304
+ if os.path.exists(gitignore_path):
305
+ cmd.append("--filter=:- .gitignore")
275
306
 
276
307
  # Add excludes
277
308
  for item in self.exclude:
@@ -294,35 +325,115 @@ class RsyncSyncer(BaseSyncer):
294
325
  if not shutil.which("sshpass"):
295
326
  raise SyncError("Rsync with password requires 'sshpass'. Please install it or use SSH Key.")
296
327
 
297
- def _build_rsync_command(self) -> List[str]:
328
+ def _check_rsync_version(self) -> tuple:
329
+ """Check local rsync version and return (major, minor, patch) tuple.
330
+
331
+ Returns:
332
+ Tuple of (major, minor, patch) version numbers
333
+ Raises:
334
+ SyncError: If rsync is not installed or version cannot be parsed
335
+ """
336
+ try:
337
+ result = subprocess.run(["rsync", "--version"], capture_output=True, text=True, timeout=5)
338
+ if result.returncode != 0:
339
+ raise SyncError("Failed to check rsync version. Is rsync installed?")
340
+
341
+ # Parse version from first line, e.g., "rsync version 3.2.5 protocol version 31"
342
+ first_line = result.stdout.split("\n")[0]
343
+ import re
344
+
345
+ match = re.search(r"version\s+(\d+)\.(\d+)\.(\d+)", first_line)
346
+ if not match:
347
+ # Try alternative format: "rsync version 2.6.9 compatible"
348
+ match = re.search(r"version\s+(\d+)\.(\d+)\.(\d+)", first_line)
349
+ if not match:
350
+ raise SyncError(f"Cannot parse rsync version from: {first_line}")
351
+
352
+ major, minor, patch = int(match.group(1)), int(match.group(2)), int(match.group(3))
353
+ return (major, minor, patch)
354
+ except FileNotFoundError:
355
+ raise SyncError("rsync not found. Please install rsync.")
356
+ except subprocess.TimeoutExpired:
357
+ raise SyncError("Timeout while checking rsync version.")
358
+ except Exception as e:
359
+ raise SyncError(f"Failed to check rsync version: {e}")
360
+
361
+ def _is_rsync_version_supported(self, min_version: tuple = (3, 1, 0)) -> bool:
362
+ """Check if local rsync version meets minimum requirement.
363
+
364
+ Args:
365
+ min_version: Minimum required version as (major, minor, patch) tuple
366
+ Returns:
367
+ True if version is supported, False otherwise
368
+ """
369
+ try:
370
+ current_version = self._check_rsync_version()
371
+ return current_version >= min_version
372
+ except SyncError:
373
+ return False
374
+
375
+ def _build_rsync_command(self, show_progress: bool = False) -> List[str]:
298
376
  """Build rsync command for uploading (local -> remote)."""
299
377
  # Ensure local dir ends with / to sync contents, not the dir itself
300
378
  src = self.local_dir if self.local_dir.endswith("/") else f"{self.local_dir}/"
301
379
  dest = f"{self.user}@{self.host}:{shlex.quote(self.remote_dir)}"
302
380
 
303
- cmd = self._build_rsync_base()
381
+ cmd = self._build_rsync_base(show_progress=show_progress)
304
382
  cmd.extend([src, dest])
305
383
 
306
384
  return self._wrap_with_sshpass(cmd)
307
385
 
308
- def _build_rsync_download_command(self, remote_path: str, local_path: str) -> List[str]:
386
+ def _build_rsync_download_command(self, remote_path: str, local_path: str, show_progress: bool = False) -> List[str]:
309
387
  """Build rsync command for downloading (remote -> local)."""
310
388
  src = f"{self.user}@{self.host}:{shlex.quote(remote_path)}"
311
389
 
312
- cmd = self._build_rsync_base()
390
+ cmd = self._build_rsync_base(show_progress=show_progress)
313
391
  cmd.extend([src, local_path])
314
392
 
315
393
  return self._wrap_with_sshpass(cmd)
316
394
 
317
- def sync(self):
395
+ def sync(self, show_progress: bool = False, progress_callback=None):
318
396
  self._check_sshpass()
319
- cmd = self._build_rsync_command()
397
+
398
+ # Check rsync version if progress mode is requested
399
+ if show_progress and progress_callback:
400
+ if not self._is_rsync_version_supported():
401
+ version = self._check_rsync_version()
402
+ raise SyncError(
403
+ f"rsync version {version[0]}.{version[1]}.{version[2]} is too old. "
404
+ f"Progress display requires rsync >= 3.1.0. "
405
+ f"Please upgrade rsync or use --no-tty mode."
406
+ )
407
+
408
+ cmd = self._build_rsync_command(show_progress=show_progress)
409
+
320
410
  try:
321
- subprocess.run(cmd, check=True)
411
+ if show_progress and progress_callback:
412
+ # Run with real-time output parsing for progress display
413
+ process = subprocess.Popen(
414
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True
415
+ )
416
+
417
+ # Parse rsync output line by line
418
+ for line in process.stdout:
419
+ line = line.strip()
420
+ # Skip empty lines and summary lines
421
+ if line and not line.startswith("sent") and not line.startswith("total"):
422
+ # Extract filename from rsync output
423
+ # Rsync --info=NAME outputs filenames directly
424
+ if not line.startswith("receiving") and not line.startswith("building"):
425
+ progress_callback(line)
426
+
427
+ process.wait()
428
+ if process.returncode != 0:
429
+ raise SyncError(f"Rsync failed with exit code {process.returncode}")
430
+ else:
431
+ # Silent mode - use subprocess.run
432
+ subprocess.run(cmd, check=True)
322
433
  except subprocess.CalledProcessError as e:
323
434
  raise SyncError(f"Rsync failed with exit code {e.returncode}")
324
435
 
325
- def download(self, remote_path: str, local_path: str):
436
+ def download(self, remote_path: str, local_path: str, show_progress: bool = False, progress_callback=None):
326
437
  """Download file or directory from remote to local."""
327
438
  self._check_sshpass()
328
439
 
@@ -331,8 +442,29 @@ class RsyncSyncer(BaseSyncer):
331
442
  if local_dir and not os.path.exists(local_dir):
332
443
  os.makedirs(local_dir, exist_ok=True)
333
444
 
334
- cmd = self._build_rsync_download_command(remote_path, local_path)
445
+ cmd = self._build_rsync_download_command(remote_path, local_path, show_progress=show_progress)
335
446
  try:
336
- subprocess.run(cmd, check=True)
447
+ if show_progress and progress_callback:
448
+ # Run with real-time output parsing for progress display
449
+ process = subprocess.Popen(
450
+ cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True
451
+ )
452
+
453
+ # Parse rsync output line by line
454
+ for line in process.stdout:
455
+ line = line.strip()
456
+ # Skip empty lines and summary lines
457
+ if line and not line.startswith("sent") and not line.startswith("total"):
458
+ # Extract filename from rsync output
459
+ # Rsync --info=NAME outputs filenames directly
460
+ if not line.startswith("receiving") and not line.startswith("building"):
461
+ progress_callback(line)
462
+
463
+ process.wait()
464
+ if process.returncode != 0:
465
+ raise SyncError(f"Rsync download failed with exit code {process.returncode}")
466
+ else:
467
+ # Silent mode - use subprocess.run
468
+ subprocess.run(cmd, check=True)
337
469
  except subprocess.CalledProcessError as e:
338
470
  raise SyncError(f"Rsync download failed with exit code {e.returncode}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mtr-cli
3
- Version: 0.2.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
@@ -57,11 +57,21 @@ pip install mtr-cli
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
 
@@ -0,0 +1,11 @@
1
+ mtr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ mtr/cli.py,sha256=LXoXCdajKQe_oVXlf9FPGgHt7N3P0zQxE-mh9xPwnII,16363
3
+ mtr/config.py,sha256=HA_pZ3rTFPYa6JhgFnEQlf4ukdFovx8obv58UILloWQ,3568
4
+ mtr/logger.py,sha256=9DPKTTzYsNMF7vXnvJ0bJditNYgzhZWHwLKUErtwCBY,3669
5
+ mtr/ssh.py,sha256=fCEXxfEK6ao8YrCU9FBJ_e5zrK93AKz6bgjXWkORQkk,7170
6
+ mtr/sync.py,sha256=xrljWPt8keigyiri7oc1RNvxJxBNBHjJjhgGtXP0F_g,18668
7
+ mtr_cli-0.3.0.dist-info/METADATA,sha256=XFZfTrcxMzhCbvzwY6LQrJWq_r2lnboCqL8SbDSuYnQ,9806
8
+ mtr_cli-0.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
+ mtr_cli-0.3.0.dist-info/entry_points.txt,sha256=8BRK0VoSAWGzovrOdzWqpwwNj-dmjVY1iQcz5MQseV4,36
10
+ mtr_cli-0.3.0.dist-info/licenses/LICENSE,sha256=PkuO1VHNDkFylFSOtMADb93mjExarK6DBTjtCB3kBeU,1067
11
+ mtr_cli-0.3.0.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- mtr/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- mtr/cli.py,sha256=lhK5hoOlMSp5kAhiqYvmWGfDdyUozELDKgk1x1LjFwI,13910
3
- mtr/config.py,sha256=v1lUg7z12jb-W0B61KKwiLuP6BO3I6ZLZSqxPSoAi7A,3037
4
- mtr/logger.py,sha256=9DPKTTzYsNMF7vXnvJ0bJditNYgzhZWHwLKUErtwCBY,3669
5
- mtr/ssh.py,sha256=fCEXxfEK6ao8YrCU9FBJ_e5zrK93AKz6bgjXWkORQkk,7170
6
- mtr/sync.py,sha256=LvfGs7wGzlU-RSHsXF8IQ_LK70yYPnw-sqVEGqLknHQ,11799
7
- mtr_cli-0.2.0.dist-info/METADATA,sha256=ZhfL78GWsse8Q1Ml17SqdOa2V2HUZwS9Tvl6kIKSMeU,9458
8
- mtr_cli-0.2.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
9
- mtr_cli-0.2.0.dist-info/entry_points.txt,sha256=8BRK0VoSAWGzovrOdzWqpwwNj-dmjVY1iQcz5MQseV4,36
10
- mtr_cli-0.2.0.dist-info/licenses/LICENSE,sha256=PkuO1VHNDkFylFSOtMADb93mjExarK6DBTjtCB3kBeU,1067
11
- mtr_cli-0.2.0.dist-info/RECORD,,