mdify-cli 2.11.9__py3-none-any.whl → 3.0.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.
mdify/ssh/transfer.py ADDED
@@ -0,0 +1,297 @@
1
+ """File transfer and progress tracking for SSH."""
2
+
3
+ import gzip
4
+ import hashlib
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Callable
8
+ from mdify.ssh.models import TransferSession
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ class FileTransferManager:
14
+ """Manages file transfers with compression and progress tracking."""
15
+
16
+ def __init__(
17
+ self,
18
+ ssh_client,
19
+ compression_threshold: int = 1024 * 1024, # 1MB
20
+ chunk_size: int = 64 * 1024, # 64KB
21
+ verify_checksum: bool = True
22
+ ):
23
+ """Initialize file transfer manager.
24
+
25
+ Parameters:
26
+ ssh_client: Connected AsyncSSHClient instance
27
+ compression_threshold: Compress files larger than this (bytes)
28
+ chunk_size: Read/write chunk size for progress updates
29
+ verify_checksum: Calculate SHA256 checksums
30
+ """
31
+ self.ssh_client = ssh_client
32
+ self.compression_threshold = compression_threshold
33
+ self.chunk_size = chunk_size
34
+ self.verify_checksum = verify_checksum
35
+
36
+ async def upload_file(
37
+ self,
38
+ local_path: str,
39
+ remote_path: str,
40
+ progress_callback: Callable[[TransferSession], None] | None = None,
41
+ overwrite: bool = False,
42
+ compress: bool | None = None
43
+ ) -> TransferSession:
44
+ """Upload file to remote server.
45
+
46
+ Parameters:
47
+ local_path: Local file path
48
+ remote_path: Remote destination path
49
+ progress_callback: Called with TransferSession after each chunk
50
+ overwrite: Allow overwriting existing files
51
+ compress: Force compression (None = auto-detect by size)
52
+
53
+ Returns:
54
+ TransferSession with transfer results
55
+
56
+ Raises:
57
+ FileNotFoundError: Local file doesn't exist
58
+ FileExistsError: Remote file exists and overwrite=False
59
+ """
60
+ local_file = Path(local_path)
61
+ if not local_file.exists():
62
+ raise FileNotFoundError(f"Local file not found: {local_path}")
63
+
64
+ file_size = local_file.stat().st_size
65
+
66
+ # Auto-detect compression
67
+ if compress is None:
68
+ compress = file_size > self.compression_threshold
69
+
70
+ session = TransferSession(
71
+ local_path=local_path,
72
+ remote_path=remote_path,
73
+ direction="upload",
74
+ total_bytes=file_size,
75
+ status="in_progress"
76
+ )
77
+
78
+ try:
79
+ # Check if remote file exists
80
+ if not overwrite:
81
+ stdout, stderr, code = await self.ssh_client.run_command(
82
+ f"test -f {remote_path}"
83
+ )
84
+ if code == 0:
85
+ raise FileExistsError(f"Remote file exists: {remote_path}")
86
+
87
+ # Prepare target path (content is streamed directly from local_file)
88
+ if compress:
89
+ logger.debug(f"Compressing {local_file.name} for upload...")
90
+ session.status = "in_progress"
91
+ await self._compress_file(local_file)
92
+ actual_remote_path = f"{remote_path}.gz"
93
+ session.status = "in_progress"
94
+ else:
95
+ actual_remote_path = remote_path
96
+
97
+ # Upload via SFTP
98
+ async with self.ssh_client.connection.start_sftp_client() as sftp:
99
+ # Write file with progress tracking
100
+ bytes_written = 0
101
+ with open(local_file, 'rb') as local_fp:
102
+ async with await sftp.open(actual_remote_path, 'wb') as remote_fp:
103
+ while True:
104
+ chunk = local_fp.read(self.chunk_size)
105
+ if not chunk:
106
+ break
107
+
108
+ await remote_fp.write(chunk)
109
+ bytes_written += len(chunk)
110
+ session.update_progress(bytes_written)
111
+
112
+ if progress_callback:
113
+ progress_callback(session)
114
+
115
+ # Verify checksum if enabled
116
+ if self.verify_checksum:
117
+ await self._verify_upload_checksum(
118
+ local_file, actual_remote_path, session
119
+ )
120
+
121
+ session.complete()
122
+ logger.info(f"Upload complete: {local_path} → {actual_remote_path}")
123
+ return session
124
+
125
+ except Exception as e:
126
+ session.fail(e)
127
+ logger.error(f"Upload failed: {e}")
128
+ raise
129
+
130
+ async def download_file(
131
+ self,
132
+ remote_path: str,
133
+ local_path: str,
134
+ progress_callback: Callable[[TransferSession], None] | None = None,
135
+ overwrite: bool = False
136
+ ) -> TransferSession:
137
+ """Download file from remote server.
138
+
139
+ Parameters:
140
+ remote_path: Remote file path
141
+ local_path: Local destination path
142
+ progress_callback: Called with TransferSession after each chunk
143
+ overwrite: Allow overwriting existing files
144
+
145
+ Returns:
146
+ TransferSession with transfer results
147
+ """
148
+ local_file = Path(local_path)
149
+
150
+ # Check if local file exists
151
+ if local_file.exists() and not overwrite:
152
+ raise FileExistsError(f"Local file exists: {local_path}")
153
+
154
+ session = TransferSession(
155
+ remote_path=remote_path,
156
+ local_path=local_path,
157
+ direction="download",
158
+ status="in_progress"
159
+ )
160
+
161
+ try:
162
+ # Get remote file size
163
+ stdout, stderr, code = await self.ssh_client.run_command(
164
+ f"wc -c < {remote_path}"
165
+ )
166
+ if code == 0:
167
+ try:
168
+ session.total_bytes = int(stdout.strip())
169
+ except ValueError:
170
+ session.total_bytes = 0
171
+
172
+ # Download via SFTP
173
+ async with self.ssh_client.connection.start_sftp_client() as sftp:
174
+ bytes_read = 0
175
+ with open(local_file, 'wb') as local_fp:
176
+ async with await sftp.open(remote_path, 'rb') as remote_fp:
177
+ while True:
178
+ chunk = await remote_fp.read(self.chunk_size)
179
+ if not chunk:
180
+ break
181
+
182
+ local_fp.write(chunk)
183
+ bytes_read += len(chunk)
184
+ session.update_progress(bytes_read)
185
+
186
+ if progress_callback:
187
+ progress_callback(session)
188
+
189
+ session.complete()
190
+ logger.info(f"Download complete: {remote_path} → {local_path}")
191
+ return session
192
+
193
+ except Exception as e:
194
+ session.fail(e)
195
+ logger.error(f"Download failed: {e}")
196
+ raise
197
+
198
+ async def _compress_file(self, file_path: Path) -> bytes:
199
+ """Compress file for transfer.
200
+
201
+ Parameters:
202
+ file_path: Path to file to compress
203
+
204
+ Returns:
205
+ Compressed file data
206
+ """
207
+ with open(file_path, 'rb') as f_in:
208
+ compressed = gzip.compress(f_in.read())
209
+ return compressed
210
+
211
+ async def _verify_upload_checksum(
212
+ self,
213
+ local_file: Path,
214
+ remote_path: str,
215
+ session: TransferSession
216
+ ) -> None:
217
+ """Verify uploaded file checksum.
218
+
219
+ Parameters:
220
+ local_file: Local file path
221
+ remote_path: Remote file path
222
+ session: Transfer session for error reporting
223
+
224
+ Raises:
225
+ ValueError: Checksum mismatch
226
+ """
227
+ # Calculate local checksum
228
+ local_sha256 = hashlib.sha256()
229
+ with open(local_file, 'rb') as f:
230
+ for chunk in iter(lambda: f.read(self.chunk_size), b''):
231
+ local_sha256.update(chunk)
232
+ local_checksum = local_sha256.hexdigest()
233
+
234
+ # Calculate remote checksum
235
+ stdout, stderr, code = await self.ssh_client.run_command(
236
+ f"sha256sum {remote_path} | awk '{{print $1}}'"
237
+ )
238
+
239
+ if code == 0:
240
+ remote_checksum = stdout.strip()
241
+
242
+ if local_checksum != remote_checksum:
243
+ raise ValueError(
244
+ f"Checksum mismatch: {local_checksum} != {remote_checksum}"
245
+ )
246
+
247
+ logger.debug(f"Checksum verified: {local_checksum}")
248
+ else:
249
+ logger.warning(f"Could not verify checksum: {stderr}")
250
+
251
+
252
+ class ProgressBar:
253
+ """Simple progress bar display."""
254
+
255
+ def __init__(self, total_bytes: int, debug_mode: bool = False):
256
+ """Initialize progress bar.
257
+
258
+ Parameters:
259
+ total_bytes: Total bytes to transfer
260
+ debug_mode: Enable detailed logging
261
+ """
262
+ self.total_bytes = total_bytes
263
+ self.debug_mode = debug_mode
264
+ self.last_update = 0
265
+
266
+ def update(self, session: TransferSession) -> str:
267
+ """Format progress display.
268
+
269
+ Parameters:
270
+ session: TransferSession with current progress
271
+
272
+ Returns:
273
+ Formatted progress string
274
+ """
275
+ if session.total_bytes == 0:
276
+ return ""
277
+
278
+ percent = 100 * session.transferred_bytes / session.total_bytes
279
+ bar_width = 40
280
+ filled = int(bar_width * session.transferred_bytes / session.total_bytes)
281
+ bar = "█" * filled + "░" * (bar_width - filled)
282
+
283
+ speed_str = f"{session.avg_speed_mbps:.1f}MB/s"
284
+ eta_str = f"ETA {session.eta_seconds}s" if session.eta_seconds else "Computing..."
285
+
286
+ return f"[{bar}] {percent:3.0f}% {speed_str} {eta_str}"
287
+
288
+ def log_chunk(self, chunk_num: int, bytes_transferred: int, speed_mbps: float) -> None:
289
+ """Log chunk transfer (debug mode).
290
+
291
+ Parameters:
292
+ chunk_num: Chunk number
293
+ bytes_transferred: Total bytes transferred so far
294
+ speed_mbps: Current speed in MB/s
295
+ """
296
+ if self.debug_mode:
297
+ logger.debug(f"Chunk {chunk_num}: {bytes_transferred} bytes ({speed_mbps:.1f}MB/s)")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdify-cli
3
- Version: 2.11.9
3
+ Version: 3.0.0
4
4
  Summary: Convert PDFs and document images into structured Markdown for LLM workflows
5
5
  Author: tiroq
6
6
  License-Expression: MIT
@@ -14,17 +14,17 @@ Classifier: Intended Audience :: Developers
14
14
  Classifier: Intended Audience :: End Users/Desktop
15
15
  Classifier: Operating System :: OS Independent
16
16
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.8
18
- Classifier: Programming Language :: Python :: 3.9
19
17
  Classifier: Programming Language :: Python :: 3.10
20
18
  Classifier: Programming Language :: Python :: 3.11
21
19
  Classifier: Programming Language :: Python :: 3.12
22
20
  Classifier: Topic :: Text Processing :: Markup :: Markdown
23
21
  Classifier: Topic :: Utilities
24
- Requires-Python: >=3.8
22
+ Requires-Python: >=3.10
25
23
  Description-Content-Type: text/markdown
26
24
  License-File: LICENSE
27
25
  Requires-Dist: requests
26
+ Requires-Dist: asyncssh>=2.10.0
27
+ Requires-Dist: pyyaml>=6.0
28
28
  Provides-Extra: dev
29
29
  Requires-Dist: pytest>=7.0; extra == "dev"
30
30
  Dynamic: license-file
@@ -120,6 +120,72 @@ mdify --gpu documents/*.pdf
120
120
 
121
121
  Requires NVIDIA GPU with CUDA support and nvidia-container-toolkit.
122
122
 
123
+ ### 🚀 Remote Server Execution (SSH)
124
+
125
+ **NEW:** Convert documents on remote servers via SSH to offload resource-intensive processing:
126
+
127
+ ```bash
128
+ # Basic remote conversion
129
+ mdify document.pdf --remote-host server.example.com
130
+
131
+ # Use SSH config alias
132
+ mdify document.pdf --remote-host production
133
+
134
+ # With custom configuration
135
+ mdify docs/*.pdf --remote-host 192.168.1.100 \
136
+ --remote-user admin \
137
+ --remote-key ~/.ssh/id_rsa
138
+
139
+ # Validate remote server before processing
140
+ mdify document.pdf --remote-host server --remote-validate-only
141
+ ```
142
+
143
+ **How it works:**
144
+ 1. Connects to remote server via SSH
145
+ 2. Validates remote resources (disk space, memory, Docker/Podman)
146
+ 3. Uploads files via SFTP
147
+ 4. Starts remote container automatically
148
+ 5. Converts documents on remote server
149
+ 6. Downloads results via SFTP
150
+ 7. Cleans up remote files and stops container
151
+
152
+ **Requirements:**
153
+ - SSH key authentication (password auth not supported for security)
154
+ - Docker or Podman installed on remote server
155
+ - Minimum 5GB disk space and 2GB RAM on remote
156
+
157
+ **SSH Configuration:**
158
+
159
+ Create `~/.mdify/remote.conf` for reusable settings:
160
+ ```yaml
161
+ host: production.example.com
162
+ port: 22
163
+ username: deploy
164
+ key_file: ~/.ssh/deploy_key
165
+ work_dir: /tmp/mdify-remote
166
+ container_runtime: docker
167
+ timeout: 30
168
+ ```
169
+
170
+ Or use existing `~/.ssh/config`:
171
+ ```
172
+ Host production
173
+ HostName 192.168.1.100
174
+ User deploy
175
+ Port 2222
176
+ IdentityFile ~/.ssh/deploy_key
177
+ ```
178
+
179
+ Then simply: `mdify doc.pdf --remote-host production`
180
+
181
+ **Configuration Precedence** (highest to lowest):
182
+ 1. CLI arguments (`--remote-*`)
183
+ 2. `~/.mdify/remote.conf`
184
+ 3. `~/.ssh/config`
185
+ 4. Built-in defaults
186
+
187
+ See the [SSH Remote Server Guide](#ssh-remote-server-options) below for all options.
188
+
123
189
  ### ⚠️ PII Masking (Deprecated)
124
190
 
125
191
  The `--mask` flag is deprecated and will be ignored in this version. PII masking functionality was available in older versions using a custom runtime but is not supported with the current docling-serve backend.
@@ -158,6 +224,24 @@ The first conversion takes longer (~30-60s) as the container loads ML models int
158
224
  | `--check-update` | Check for available updates and exit |
159
225
  | `--version` | Show version and exit |
160
226
 
227
+ ### SSH Remote Server Options
228
+
229
+ | Option | Description |
230
+ | ------ | ----------- |
231
+ | `--remote-host HOST` | SSH hostname or IP (required for remote mode) |
232
+ | `--remote-port PORT` | SSH port (default: 22) |
233
+ | `--remote-user USER` | SSH username (uses ~/.ssh/config or current user) |
234
+ | `--remote-key PATH` | SSH private key file path |
235
+ | `--remote-key-passphrase PASS` | SSH key passphrase |
236
+ | `--remote-timeout SEC` | SSH connection timeout in seconds (default: 30) |
237
+ | `--remote-work-dir DIR` | Remote working directory (default: /tmp/mdify-remote) |
238
+ | `--remote-runtime RT` | Remote container runtime: docker or podman (auto-detected) |
239
+ | `--remote-config PATH` | Path to mdify remote config file (default: ~/.mdify/remote.conf) |
240
+ | `--remote-skip-ssh-config` | Don't load settings from ~/.ssh/config |
241
+ | `--remote-skip-validation` | Skip remote resource validation (not recommended) |
242
+ | `--remote-validate-only` | Validate remote server and exit (dry run) |
243
+ | `--remote-debug` | Enable detailed SSH debug logging |
244
+
161
245
  ### Container Runtime Selection
162
246
 
163
247
  mdify automatically detects and uses the best available container runtime. The detection order differs by platform:
@@ -292,6 +376,110 @@ Or if installed via pip:
292
376
  pip uninstall mdify-cli
293
377
  ```
294
378
 
379
+ ## Troubleshooting
380
+
381
+ ### SSH Remote Server Issues
382
+
383
+ **Connection Refused**
384
+
385
+ ```
386
+ Error: SSH connection failed: Connection refused (host:22)
387
+ ```
388
+
389
+ - Verify SSH server is running on remote: `ssh user@host`
390
+ - Check firewall allows port 22 (or custom SSH port)
391
+ - Verify hostname/IP is correct
392
+
393
+ **Authentication Failed**
394
+
395
+ ```
396
+ Error: SSH authentication failed
397
+ ```
398
+
399
+ - Use SSH key authentication (password auth not supported)
400
+ - Verify key file exists: `ls -l ~/.ssh/id_rsa`
401
+ - Check key permissions: `chmod 600 ~/.ssh/id_rsa`
402
+ - Test SSH manually: `ssh -i ~/.ssh/id_rsa user@host`
403
+ - Add key to ssh-agent: `ssh-add ~/.ssh/id_rsa`
404
+
405
+ **Remote Container Runtime Not Found**
406
+
407
+ ```
408
+ Error: Container runtime not available: docker/podman
409
+ ```
410
+
411
+ - Install Docker on remote: `sudo apt install docker.io` (Ubuntu/Debian)
412
+ - Or install Podman: `sudo dnf install podman` (Fedora/RHEL)
413
+ - Add user to docker group: `sudo usermod -aG docker $USER`
414
+ - Verify remote Docker running: `ssh user@host docker ps`
415
+
416
+ **Insufficient Remote Resources**
417
+
418
+ ```
419
+ Warning: Less than 5GB available on remote
420
+ ```
421
+
422
+ - Free up disk space on remote server
423
+ - Use `--remote-work-dir` to specify different partition
424
+ - Use `--remote-skip-validation` to bypass check (not recommended)
425
+
426
+ **File Transfer Timeout**
427
+
428
+ ```
429
+ Error: File transfer timeout
430
+ ```
431
+
432
+ - Increase timeout: `--remote-timeout 120`
433
+ - Check network bandwidth and stability
434
+ - Try smaller files first to verify connection
435
+
436
+ **Container Health Check Fails**
437
+
438
+ ```
439
+ Error: Container failed to become healthy within 60 seconds
440
+ ```
441
+
442
+ - Check remote Docker logs: `ssh user@host docker logs mdify-remote-<id>`
443
+ - Verify port 5001 not in use: `ssh user@host netstat -tuln | grep 5001`
444
+ - Try different port: `--port 5002`
445
+
446
+ **SSH Config Not Loaded**
447
+
448
+ If using SSH config alias but getting connection errors:
449
+
450
+ ```bash
451
+ # Verify SSH config is valid
452
+ cat ~/.ssh/config
453
+
454
+ # Test SSH config works
455
+ ssh your-alias
456
+
457
+ # Use explicit connection if needed
458
+ mdify doc.pdf --remote-host 192.168.1.100 --remote-user admin
459
+ ```
460
+
461
+ **Permission Denied on Remote**
462
+
463
+ ```
464
+ Error: Work directory not writable: /tmp/mdify-remote
465
+ ```
466
+
467
+ - SSH to remote and check permissions: `ssh user@host ls -ld /tmp`
468
+ - Use directory in your home: `--remote-work-dir ~/mdify-temp`
469
+ - Fix permissions: `ssh user@host chmod 777 /tmp/mdify-remote`
470
+
471
+ **Debug Mode**
472
+
473
+ Enable detailed logging for troubleshooting:
474
+
475
+ ```bash
476
+ # Debug SSH operations
477
+ mdify doc.pdf --remote-host server --remote-debug
478
+
479
+ # Debug local operations
480
+ MDIFY_DEBUG=1 mdify doc.pdf
481
+ ```
482
+
295
483
  ## Development
296
484
 
297
485
  ### Task automation
@@ -0,0 +1,17 @@
1
+ assets/mdify.png,sha256=qUj7WXWqNwpI2KNXOW79XJwqFqa-UI0JEkmt1mmy4Rg,1820418
2
+ mdify/__init__.py,sha256=kKpiV423cq23fbsR9zhi3l8ZpZmWGRJsjyMG9G5v670,90
3
+ mdify/__main__.py,sha256=bhpJ00co6MfaVOdH4XLoW04NtLYDa_oJK7ODzfLrn9M,143
4
+ mdify/cli.py,sha256=xU_HWrOVYB67GRGz77l2ToLWlh2OM1FPgZjZDLT3MVk,73705
5
+ mdify/container.py,sha256=BjL5ZR__n1i_WHifXKllTPoqO7IuOUdPDo5esuNg0Iw,8213
6
+ mdify/docling_client.py,sha256=xuQR6sC1v3EPloOSwExoHCqT4uUxE8myYq-Yeby3C2I,7975
7
+ mdify/ssh/__init__.py,sha256=SmRWgwEvAQZ_ARHlKTb9QDPwVAcz6dvPUks2pZFWLAU,271
8
+ mdify/ssh/client.py,sha256=MNMBrL5Xk2rFo28Ytw80hWX2vQ3_CXlIL4VathNtK-I,14873
9
+ mdify/ssh/models.py,sha256=IGAf5EfpZuBS2lIGzxmIsl8f44bXg4a8wk4BW9JWKEQ,17275
10
+ mdify/ssh/remote_container.py,sha256=kmScAlmHI9rJLKliYcYQXDZHF7PYYiD-_rRV-S0fffM,8462
11
+ mdify/ssh/transfer.py,sha256=aZZgylDjoqx6PEpaMu2zxkDF04w7btiOnMExmtt922A,10574
12
+ mdify_cli-3.0.0.dist-info/licenses/LICENSE,sha256=NWM66Uv-XuSMKaU-gaPmvfyk4WgE6zcIPr78wyg6GAo,1065
13
+ mdify_cli-3.0.0.dist-info/METADATA,sha256=HAui-xr9LYP9yJGtFT0g9oVk3NGJnOmo5pyvQx0qb-Q,14766
14
+ mdify_cli-3.0.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
15
+ mdify_cli-3.0.0.dist-info/entry_points.txt,sha256=0Xki8f5lADQUtwdt6Eq_FEaieI6Byhk8UE7BuDhChMg,41
16
+ mdify_cli-3.0.0.dist-info/top_level.txt,sha256=qltzf7h8owHq7dxCdfCkSHY8gT21hn1_E8P-VWS_OKM,6
17
+ mdify_cli-3.0.0.dist-info/RECORD,,
@@ -1,12 +0,0 @@
1
- assets/mdify.png,sha256=qUj7WXWqNwpI2KNXOW79XJwqFqa-UI0JEkmt1mmy4Rg,1820418
2
- mdify/__init__.py,sha256=x2PxT1laVq9WFwgXBDy1TJ_qCOBN4cxlmLYbSBcb7qA,91
3
- mdify/__main__.py,sha256=bhpJ00co6MfaVOdH4XLoW04NtLYDa_oJK7ODzfLrn9M,143
4
- mdify/cli.py,sha256=Mv3ClwC84fkorZgwM1IqGMvZ0-hT_V77qhHo2p0ueCU,49638
5
- mdify/container.py,sha256=ARdFs-TOSh5vHGtBJ0CppfpZFaiprIuRdQ5wDH0NfrY,8377
6
- mdify/docling_client.py,sha256=xuQR6sC1v3EPloOSwExoHCqT4uUxE8myYq-Yeby3C2I,7975
7
- mdify_cli-2.11.9.dist-info/licenses/LICENSE,sha256=NWM66Uv-XuSMKaU-gaPmvfyk4WgE6zcIPr78wyg6GAo,1065
8
- mdify_cli-2.11.9.dist-info/METADATA,sha256=NHwtbgGo2CAPqZIOT7ebPk5mTwsmxRBo5pg0l71xenE,9623
9
- mdify_cli-2.11.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
10
- mdify_cli-2.11.9.dist-info/entry_points.txt,sha256=0Xki8f5lADQUtwdt6Eq_FEaieI6Byhk8UE7BuDhChMg,41
11
- mdify_cli-2.11.9.dist-info/top_level.txt,sha256=qltzf7h8owHq7dxCdfCkSHY8gT21hn1_E8P-VWS_OKM,6
12
- mdify_cli-2.11.9.dist-info/RECORD,,