remote-docker-cli 0.3.0__tar.gz → 0.3.1__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_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/PKG-INFO +25 -7
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/README.md +24 -6
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/src/remote_docker/__init__.py +1 -1
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/src/remote_docker/cli.py +24 -11
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/src/remote_docker/tests/test_cli_commands.py +1 -16
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/src/remote_docker/tests/test_kaggle.py +158 -0
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/src/remote_docker/tests/test_load.py +1 -6
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/.gitignore +0 -0
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/pyproject.toml +0 -0
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/src/remote_docker/__main__.py +0 -0
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/src/remote_docker/tests/__init__.py +0 -0
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/src/remote_docker/tests/test_config.py +0 -0
- {remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/src/remote_docker/tests/test_helpers.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: remote-docker-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: A CLI tool for managing remote Docker hosts
|
|
5
5
|
Project-URL: Homepage, https://gitee.com/kzhuo/remote-docker
|
|
6
6
|
Project-URL: Issues, https://gitee.com/kzhuo/remote-docker/issues
|
|
@@ -96,16 +96,16 @@ Description-Content-Type: text/markdown
|
|
|
96
96
|
### 使用 pip
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
|
-
pip install remote-docker
|
|
99
|
+
pip install remote-docker-cli
|
|
100
100
|
|
|
101
101
|
# 可选:启用 paramiko 后端(当前版本使用系统 ssh,可不装)
|
|
102
|
-
pip install "remote-docker[ssh]"
|
|
102
|
+
pip install "remote-docker-cli[ssh]"
|
|
103
103
|
```
|
|
104
104
|
|
|
105
105
|
### 从源码安装(推荐使用 uv)
|
|
106
106
|
|
|
107
107
|
```bash
|
|
108
|
-
git clone https://
|
|
108
|
+
git clone https://gitee.com/kzhuo/remote-docker.git
|
|
109
109
|
cd remote-docker
|
|
110
110
|
uv sync
|
|
111
111
|
uv pip install -e .
|
|
@@ -115,6 +115,7 @@ uv pip install -e .
|
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
117
|
rdocker --help
|
|
118
|
+
rdocker version # 查看版本
|
|
118
119
|
```
|
|
119
120
|
|
|
120
121
|
### 远端机器前置条件
|
|
@@ -234,7 +235,20 @@ $ rdocker_HOST=override.host rdocker config show
|
|
|
234
235
|
|
|
235
236
|
所有动作命令均支持 `--try` 干跑:仅打印将要执行的 ssh / scp / skopeo 命令,不实际执行。
|
|
236
237
|
|
|
237
|
-
|
|
238
|
+
### `rdocker version`
|
|
239
|
+
|
|
240
|
+
打印当前安装的 rdocker 版本:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
$ rdocker version
|
|
244
|
+
rdocker 0.3.1
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 通用选项
|
|
248
|
+
|
|
249
|
+
- `--host / --user / --port / --key` — 可在任意子命令上直接覆盖 SSH 连接参数(默认从配置文件/环境变量读取),优先级与配置一致。
|
|
250
|
+
|
|
251
|
+
### `--name` 缺省规则:
|
|
238
252
|
- **compose 模式**(`pull` / `all` / `save` / `fetch` / `cleanup`) → 当前目录最后一级目录名
|
|
239
253
|
- **image 模式**(`spull`) → 镜像去除 tag 后的最后一段(如 `nginx:1.25-alpine` → `nginx`,`registry:5000/myapp:v1` → `myapp`)
|
|
240
254
|
|
|
@@ -372,12 +386,14 @@ rdocker load ./myapp.tar.gz
|
|
|
372
386
|
```bash
|
|
373
387
|
rdocker kaggle upload docker-compose.yml # --name 缺省 = cwd 短名
|
|
374
388
|
rdocker kaggle upload docker-compose.yml --name prod --registry-auth admin:pass
|
|
389
|
+
rdocker kaggle upload docker-compose.yml --arch arm64 # 覆盖镜像架构
|
|
375
390
|
rdocker kaggle upload docker-compose.yml --try # 只打印不推送
|
|
376
391
|
```
|
|
377
392
|
|
|
378
393
|
选项:
|
|
379
394
|
- `--name, -n` — Bundle 名称(缺省 = cwd 短名)
|
|
380
395
|
- `--registry-auth, -a` — 镜像仓库认证 `user:pass`
|
|
396
|
+
- `--arch` — 覆盖镜像架构(如 `amd64`、`arm64`、`ppc64le`、`s390x`、`386`、`arm`;缺省 = 本地自动探测)
|
|
381
397
|
- `--try` — 干跑:只打印 notebook 内容
|
|
382
398
|
|
|
383
399
|
### `rdocker kaggle fetch [--name <bundle>] [--output-dir <dir>] [--try]`
|
|
@@ -406,7 +422,7 @@ rdocker kaggle fetch --name myapp --try # 只打印命令
|
|
|
406
422
|
|
|
407
423
|
- **kaggle CLI 缺失 / 未配置** → `pip install kaggle` 后 `kaggle config view` 验证。`rdocker` 会把错误指向这两步。
|
|
408
424
|
- **kernel 跑失败** → 在 https://www.kaggle.com/<username>/rdocker-<bundle> 看 stderr;常因镜像仓库私有需 `--registry-auth`,或 compose 中含 Kaggle 网络策略拉不到的镜像。
|
|
409
|
-
- **arch 不匹配** → `rdocker` 默认按本地 `platform.machine()` 注入 `--override-arch
|
|
425
|
+
- **arch 不匹配** → `rdocker` 默认按本地 `platform.machine()` 注入 `--override-arch`;如需拉取其他架构的镜像,使用 `rdocker kaggle upload --arch <arch>` 覆盖(如 `amd64`、`arm64`)。
|
|
410
426
|
- **output 还没生成** → `kaggle kernels output` 报错时直接看返回的 stderr;常见原因是 kernel 仍在排队(免费版 CPU 偶尔排队几分钟)或已失败。
|
|
411
427
|
- **`.kaggle/` 目录膨胀** → 该目录在 CWD 下,是 `kaggle kernels push` 的暂存区,可随时 `rm -rf .kaggle`。
|
|
412
428
|
|
|
@@ -460,7 +476,9 @@ remote-docker/
|
|
|
460
476
|
│ └── remote_docker/
|
|
461
477
|
│ ├── __init__.py # 包元信息 (__version__)
|
|
462
478
|
│ ├── __main__.py # python -m remote_docker 入口
|
|
463
|
-
│
|
|
479
|
+
│ ├── cli.py # Click CLI 主体
|
|
480
|
+
│ └── tests/ # pytest 测试
|
|
481
|
+
├── openspec/ # OpenSpec 变更规格(如 add-kaggle-support)
|
|
464
482
|
├── pyproject.toml # 项目与依赖配置(hatchling 构建)
|
|
465
483
|
├── uv.lock # uv 锁定文件
|
|
466
484
|
└── README.md
|
|
@@ -64,16 +64,16 @@
|
|
|
64
64
|
### 使用 pip
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
pip install remote-docker
|
|
67
|
+
pip install remote-docker-cli
|
|
68
68
|
|
|
69
69
|
# 可选:启用 paramiko 后端(当前版本使用系统 ssh,可不装)
|
|
70
|
-
pip install "remote-docker[ssh]"
|
|
70
|
+
pip install "remote-docker-cli[ssh]"
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
### 从源码安装(推荐使用 uv)
|
|
74
74
|
|
|
75
75
|
```bash
|
|
76
|
-
git clone https://
|
|
76
|
+
git clone https://gitee.com/kzhuo/remote-docker.git
|
|
77
77
|
cd remote-docker
|
|
78
78
|
uv sync
|
|
79
79
|
uv pip install -e .
|
|
@@ -83,6 +83,7 @@ uv pip install -e .
|
|
|
83
83
|
|
|
84
84
|
```bash
|
|
85
85
|
rdocker --help
|
|
86
|
+
rdocker version # 查看版本
|
|
86
87
|
```
|
|
87
88
|
|
|
88
89
|
### 远端机器前置条件
|
|
@@ -202,7 +203,20 @@ $ rdocker_HOST=override.host rdocker config show
|
|
|
202
203
|
|
|
203
204
|
所有动作命令均支持 `--try` 干跑:仅打印将要执行的 ssh / scp / skopeo 命令,不实际执行。
|
|
204
205
|
|
|
205
|
-
|
|
206
|
+
### `rdocker version`
|
|
207
|
+
|
|
208
|
+
打印当前安装的 rdocker 版本:
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
$ rdocker version
|
|
212
|
+
rdocker 0.3.1
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 通用选项
|
|
216
|
+
|
|
217
|
+
- `--host / --user / --port / --key` — 可在任意子命令上直接覆盖 SSH 连接参数(默认从配置文件/环境变量读取),优先级与配置一致。
|
|
218
|
+
|
|
219
|
+
### `--name` 缺省规则:
|
|
206
220
|
- **compose 模式**(`pull` / `all` / `save` / `fetch` / `cleanup`) → 当前目录最后一级目录名
|
|
207
221
|
- **image 模式**(`spull`) → 镜像去除 tag 后的最后一段(如 `nginx:1.25-alpine` → `nginx`,`registry:5000/myapp:v1` → `myapp`)
|
|
208
222
|
|
|
@@ -340,12 +354,14 @@ rdocker load ./myapp.tar.gz
|
|
|
340
354
|
```bash
|
|
341
355
|
rdocker kaggle upload docker-compose.yml # --name 缺省 = cwd 短名
|
|
342
356
|
rdocker kaggle upload docker-compose.yml --name prod --registry-auth admin:pass
|
|
357
|
+
rdocker kaggle upload docker-compose.yml --arch arm64 # 覆盖镜像架构
|
|
343
358
|
rdocker kaggle upload docker-compose.yml --try # 只打印不推送
|
|
344
359
|
```
|
|
345
360
|
|
|
346
361
|
选项:
|
|
347
362
|
- `--name, -n` — Bundle 名称(缺省 = cwd 短名)
|
|
348
363
|
- `--registry-auth, -a` — 镜像仓库认证 `user:pass`
|
|
364
|
+
- `--arch` — 覆盖镜像架构(如 `amd64`、`arm64`、`ppc64le`、`s390x`、`386`、`arm`;缺省 = 本地自动探测)
|
|
349
365
|
- `--try` — 干跑:只打印 notebook 内容
|
|
350
366
|
|
|
351
367
|
### `rdocker kaggle fetch [--name <bundle>] [--output-dir <dir>] [--try]`
|
|
@@ -374,7 +390,7 @@ rdocker kaggle fetch --name myapp --try # 只打印命令
|
|
|
374
390
|
|
|
375
391
|
- **kaggle CLI 缺失 / 未配置** → `pip install kaggle` 后 `kaggle config view` 验证。`rdocker` 会把错误指向这两步。
|
|
376
392
|
- **kernel 跑失败** → 在 https://www.kaggle.com/<username>/rdocker-<bundle> 看 stderr;常因镜像仓库私有需 `--registry-auth`,或 compose 中含 Kaggle 网络策略拉不到的镜像。
|
|
377
|
-
- **arch 不匹配** → `rdocker` 默认按本地 `platform.machine()` 注入 `--override-arch
|
|
393
|
+
- **arch 不匹配** → `rdocker` 默认按本地 `platform.machine()` 注入 `--override-arch`;如需拉取其他架构的镜像,使用 `rdocker kaggle upload --arch <arch>` 覆盖(如 `amd64`、`arm64`)。
|
|
378
394
|
- **output 还没生成** → `kaggle kernels output` 报错时直接看返回的 stderr;常见原因是 kernel 仍在排队(免费版 CPU 偶尔排队几分钟)或已失败。
|
|
379
395
|
- **`.kaggle/` 目录膨胀** → 该目录在 CWD 下,是 `kaggle kernels push` 的暂存区,可随时 `rm -rf .kaggle`。
|
|
380
396
|
|
|
@@ -428,7 +444,9 @@ remote-docker/
|
|
|
428
444
|
│ └── remote_docker/
|
|
429
445
|
│ ├── __init__.py # 包元信息 (__version__)
|
|
430
446
|
│ ├── __main__.py # python -m remote_docker 入口
|
|
431
|
-
│
|
|
447
|
+
│ ├── cli.py # Click CLI 主体
|
|
448
|
+
│ └── tests/ # pytest 测试
|
|
449
|
+
├── openspec/ # OpenSpec 变更规格(如 add-kaggle-support)
|
|
432
450
|
├── pyproject.toml # 项目与依赖配置(hatchling 构建)
|
|
433
451
|
├── uv.lock # uv 锁定文件
|
|
434
452
|
└── README.md
|
|
@@ -445,6 +445,12 @@ def _require_host(ctx: click.Context) -> str:
|
|
|
445
445
|
return host
|
|
446
446
|
|
|
447
447
|
|
|
448
|
+
@cli.command("version", help="Print rdocker version and exit")
|
|
449
|
+
def version_cmd():
|
|
450
|
+
"""Print the installed rdocker version."""
|
|
451
|
+
click.echo(f"rdocker {__version__}")
|
|
452
|
+
|
|
453
|
+
|
|
448
454
|
# ── config 命令组 ────────────────────────────
|
|
449
455
|
|
|
450
456
|
@cli.group(help="Manage rdocker configuration (~/.config/rdocker/config.yml)")
|
|
@@ -817,7 +823,7 @@ def cleanup(ctx: click.Context, name: str, dry_run: bool):
|
|
|
817
823
|
|
|
818
824
|
# ── all ───────────────────────────────────────
|
|
819
825
|
|
|
820
|
-
@cli.command(
|
|
826
|
+
@cli.command()
|
|
821
827
|
@click.argument("compose_file", type=click.Path(exists=True, dir_okay=False))
|
|
822
828
|
@click.option("--name", "-n", default="", help="Bundle name (default: current directory short name)")
|
|
823
829
|
@click.option("--registry-auth", "-a", default=lambda: get_config_value("registry_auth", ""), help="Registry credentials (user:pass)")
|
|
@@ -1011,7 +1017,7 @@ for img in IMAGES:
|
|
|
1011
1017
|
"docker://" + img,
|
|
1012
1018
|
"docker-archive:" + tar_path + ":" + img,
|
|
1013
1019
|
])
|
|
1014
|
-
print("[rdocker] skopeo copy " + img + " -> " + tar_path, flush=True)
|
|
1020
|
+
print("[rdocker] skopeo copy " + img + " {arch}-> " + tar_path, flush=True)
|
|
1015
1021
|
subprocess.run(cmd, check=True)
|
|
1016
1022
|
|
|
1017
1023
|
# --- Step 3: pack as tar.gz with . as root (compatible with rdocker load) ---
|
|
@@ -1136,7 +1142,7 @@ def build_kaggle_notebook_dir(name, images, arch, registry_auth, username):
|
|
|
1136
1142
|
return notebook_dir
|
|
1137
1143
|
|
|
1138
1144
|
|
|
1139
|
-
def do_kaggle_upload(name, compose_file, registry_auth):
|
|
1145
|
+
def do_kaggle_upload(name, compose_file, registry_auth, arch=None):
|
|
1140
1146
|
"""Build the Kaggle kernel from a compose file and push it via the kaggle CLI."""
|
|
1141
1147
|
compose_path = Path(compose_file)
|
|
1142
1148
|
click.echo(f"📦 Bundle name: {name}")
|
|
@@ -1151,8 +1157,11 @@ def do_kaggle_upload(name, compose_file, registry_auth):
|
|
|
1151
1157
|
click.echo("")
|
|
1152
1158
|
|
|
1153
1159
|
username = get_kaggle_username()
|
|
1154
|
-
arch
|
|
1155
|
-
|
|
1160
|
+
if arch is None:
|
|
1161
|
+
arch = local_arch()
|
|
1162
|
+
click.echo(f"🐳 Arch (from local): {arch}")
|
|
1163
|
+
else:
|
|
1164
|
+
click.echo(f"🐳 Arch (override): {arch}")
|
|
1156
1165
|
click.echo(f"👤 Kaggle username: {username}")
|
|
1157
1166
|
|
|
1158
1167
|
notebook_dir = build_kaggle_notebook_dir(
|
|
@@ -1176,7 +1185,7 @@ def do_kaggle_upload(name, compose_file, registry_auth):
|
|
|
1176
1185
|
click.echo(f" View at: https://www.kaggle.com/{slug}")
|
|
1177
1186
|
|
|
1178
1187
|
|
|
1179
|
-
def print_kaggle_upload_plan(name, compose_file, registry_auth):
|
|
1188
|
+
def print_kaggle_upload_plan(name, compose_file, registry_auth, arch=None):
|
|
1180
1189
|
"""Dry-run: resolve the real username, then print the notebook content (no writes, no push)."""
|
|
1181
1190
|
compose_path = Path(compose_file)
|
|
1182
1191
|
images = parse_compose_images(compose_path)
|
|
@@ -1192,9 +1201,12 @@ def print_kaggle_upload_plan(name, compose_file, registry_auth):
|
|
|
1192
1201
|
click.echo("")
|
|
1193
1202
|
|
|
1194
1203
|
username = get_kaggle_username()
|
|
1195
|
-
arch
|
|
1204
|
+
if arch is None:
|
|
1205
|
+
arch = local_arch()
|
|
1206
|
+
click.echo(f"🐳 Arch (from local): {arch}")
|
|
1207
|
+
else:
|
|
1208
|
+
click.echo(f"🐳 Arch (override): {arch}")
|
|
1196
1209
|
auth_display = registry_auth if registry_auth else "(none)"
|
|
1197
|
-
click.echo(f"🐳 Arch: {arch}")
|
|
1198
1210
|
click.echo(f"👤 Kaggle username: {username}")
|
|
1199
1211
|
click.echo(f"🔐 Registry auth: {auth_display}")
|
|
1200
1212
|
click.echo("")
|
|
@@ -1268,17 +1280,18 @@ def kaggle():
|
|
|
1268
1280
|
@click.argument("compose_file", type=click.Path(exists=True, dir_okay=False))
|
|
1269
1281
|
@click.option("--name", "-n", default="", help="Bundle name (default: current directory short name)")
|
|
1270
1282
|
@click.option("--registry-auth", "-a", default=lambda: get_config_value("registry_auth", ""), help="Registry credentials (user:pass)")
|
|
1283
|
+
@click.option("--arch", default=None, help="Override image architecture (e.g. amd64, arm64, ppc64le, s390x, 386, arm). Default: auto-detected from local machine.")
|
|
1271
1284
|
@click.option("--try", "dry_run", is_flag=True, help="Dry-run: print notebook content without pushing")
|
|
1272
1285
|
@click.pass_context
|
|
1273
|
-
def upload(ctx, compose_file, name, registry_auth, dry_run):
|
|
1286
|
+
def upload(ctx, compose_file, name, registry_auth, arch, dry_run):
|
|
1274
1287
|
"""Generate a Kaggle kernel from a compose file and push it."""
|
|
1275
1288
|
if not name:
|
|
1276
1289
|
name = default_bundle_name("compose")
|
|
1277
1290
|
auth = registry_auth or None
|
|
1278
1291
|
if dry_run:
|
|
1279
|
-
print_kaggle_upload_plan(name, compose_file, auth)
|
|
1292
|
+
print_kaggle_upload_plan(name, compose_file, auth, arch)
|
|
1280
1293
|
return
|
|
1281
|
-
do_kaggle_upload(name, compose_file, auth)
|
|
1294
|
+
do_kaggle_upload(name, compose_file, auth, arch)
|
|
1282
1295
|
|
|
1283
1296
|
|
|
1284
1297
|
@kaggle.command()
|
{remote_docker_cli-0.3.0 → remote_docker_cli-0.3.1}/src/remote_docker/tests/test_cli_commands.py
RENAMED
|
@@ -18,7 +18,6 @@ from unittest.mock import MagicMock, patch
|
|
|
18
18
|
import pytest
|
|
19
19
|
from click.testing import CliRunner
|
|
20
20
|
|
|
21
|
-
import remote_docker.cli as cli_mod
|
|
22
21
|
from remote_docker.cli import cli, do_load
|
|
23
22
|
|
|
24
23
|
|
|
@@ -43,24 +42,10 @@ def runner():
|
|
|
43
42
|
return CliRunner()
|
|
44
43
|
|
|
45
44
|
|
|
46
|
-
@pytest.fixture
|
|
47
|
-
def no_remote_config(monkeypatch, tmp_path):
|
|
48
|
-
"""Point CONFIG_FILE/CONFIG_DIR to an empty tmp location so tests that
|
|
49
|
-
exercise the "no host" branch are not poisoned by a real ~/.config/rdocker."""
|
|
50
|
-
cfg_dir = tmp_path / "rdocker-empty"
|
|
51
|
-
cfg_file = cfg_dir / "config.yml"
|
|
52
|
-
monkeypatch.setattr(cli_mod, "CONFIG_DIR", cfg_dir)
|
|
53
|
-
monkeypatch.setattr(cli_mod, "CONFIG_FILE", cfg_file)
|
|
54
|
-
# Also clear any rdocker_* env vars that might set a host.
|
|
55
|
-
for k in [k for k in os.environ if k.startswith("rdocker_")]:
|
|
56
|
-
monkeypatch.delenv(k, raising=False)
|
|
57
|
-
return cfg_dir, cfg_file
|
|
58
|
-
|
|
59
|
-
|
|
60
45
|
# ── pull ──────────────────────────────────────────────────────────────
|
|
61
46
|
|
|
62
47
|
class TestPullCommand:
|
|
63
|
-
def test_pull_requires_host(self, runner, compose_file
|
|
48
|
+
def test_pull_requires_host(self, runner, compose_file):
|
|
64
49
|
result = runner.invoke(cli, ["pull", str(compose_file)])
|
|
65
50
|
assert result.exit_code != 0
|
|
66
51
|
assert "Remote host not specified" in result.output
|
|
@@ -7,6 +7,8 @@ Covers:
|
|
|
7
7
|
rm-rf cleanup step, tar root '.'
|
|
8
8
|
- render_kaggle_kernel_metadata: id / title composition, default fields
|
|
9
9
|
- build_kaggle_notebook_dir: writes rdcontainer.py + kernel-metadata.json under .kaggle/<name>/
|
|
10
|
+
- --arch override: the CLI flag is wired through to the rendered notebook in both the
|
|
11
|
+
push and --try paths, and falls back to local_arch() when omitted
|
|
10
12
|
"""
|
|
11
13
|
import json
|
|
12
14
|
import subprocess
|
|
@@ -14,16 +16,36 @@ from unittest.mock import MagicMock, patch
|
|
|
14
16
|
|
|
15
17
|
import click
|
|
16
18
|
import pytest
|
|
19
|
+
from click.testing import CliRunner
|
|
17
20
|
|
|
18
21
|
from remote_docker.cli import (
|
|
19
22
|
KAGGLE_USERNAME_RE,
|
|
20
23
|
build_kaggle_notebook_dir,
|
|
24
|
+
cli,
|
|
25
|
+
do_kaggle_upload,
|
|
21
26
|
get_kaggle_username,
|
|
27
|
+
print_kaggle_upload_plan,
|
|
22
28
|
render_kaggle_kernel_metadata,
|
|
23
29
|
render_kaggle_notebook,
|
|
24
30
|
)
|
|
25
31
|
|
|
26
32
|
|
|
33
|
+
COMPOSE_BASIC = """\
|
|
34
|
+
services:
|
|
35
|
+
web:
|
|
36
|
+
image: nginx:1.25-alpine
|
|
37
|
+
cache:
|
|
38
|
+
image: redis:7
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.fixture
|
|
43
|
+
def compose_file(tmp_path):
|
|
44
|
+
p = tmp_path / "docker-compose.yml"
|
|
45
|
+
p.write_text(COMPOSE_BASIC, encoding="utf-8")
|
|
46
|
+
return p
|
|
47
|
+
|
|
48
|
+
|
|
27
49
|
# ── KAGGLE_USERNAME_RE ────────────────────────────────────────────────
|
|
28
50
|
|
|
29
51
|
class TestKaggleUsernameRegex:
|
|
@@ -254,3 +276,139 @@ class TestBuildKaggleNotebookDir:
|
|
|
254
276
|
meta = json.loads((result / "kernel-metadata.json").read_text(encoding="utf-8"))
|
|
255
277
|
assert meta["id"] == "alice/rdocker-myapp"
|
|
256
278
|
assert meta["title"] == "rdocker-myapp"
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ── --arch override (do_kaggle_upload / print_kaggle_upload_plan) ─────
|
|
282
|
+
|
|
283
|
+
class TestKaggleUploadArchOverride:
|
|
284
|
+
def _patch_all_subprocess_runs(self, monkeypatch):
|
|
285
|
+
"""Patch every subprocess.run so config-view returns a username, push succeeds."""
|
|
286
|
+
monkeypatch.setattr("shutil.which", lambda _: "/usr/local/bin/kaggle")
|
|
287
|
+
config_view = MagicMock(returncode=0, stdout="- username: alice\n", stderr="")
|
|
288
|
+
push = MagicMock(returncode=0, stdout="", stderr="")
|
|
289
|
+
return patch("subprocess.run", side_effect=[config_view, push])
|
|
290
|
+
|
|
291
|
+
def test_upload_uses_local_arch_when_no_override(self, compose_file, tmp_path, monkeypatch):
|
|
292
|
+
monkeypatch.chdir(tmp_path)
|
|
293
|
+
monkeypatch.setattr("platform.machine", lambda: "x86_64")
|
|
294
|
+
with self._patch_all_subprocess_runs(monkeypatch):
|
|
295
|
+
do_kaggle_upload(
|
|
296
|
+
name="myapp",
|
|
297
|
+
compose_file=str(compose_file),
|
|
298
|
+
registry_auth=None,
|
|
299
|
+
)
|
|
300
|
+
text = (tmp_path / ".kaggle" / "myapp" / "rdocker.py").read_text(encoding="utf-8")
|
|
301
|
+
assert 'ARCH = "amd64"' in text
|
|
302
|
+
|
|
303
|
+
def test_upload_arch_override_writes_override_into_notebook(self, compose_file, tmp_path, monkeypatch):
|
|
304
|
+
monkeypatch.chdir(tmp_path)
|
|
305
|
+
monkeypatch.setattr("platform.machine", lambda: "x86_64")
|
|
306
|
+
with self._patch_all_subprocess_runs(monkeypatch):
|
|
307
|
+
do_kaggle_upload(
|
|
308
|
+
name="myapp",
|
|
309
|
+
compose_file=str(compose_file),
|
|
310
|
+
registry_auth=None,
|
|
311
|
+
arch="arm64",
|
|
312
|
+
)
|
|
313
|
+
text = (tmp_path / ".kaggle" / "myapp" / "rdocker.py").read_text(encoding="utf-8")
|
|
314
|
+
assert 'ARCH = "arm64"' in text
|
|
315
|
+
assert 'ARCH = "amd64"' not in text
|
|
316
|
+
|
|
317
|
+
def test_upload_arch_override_accepts_exotic_values(self, compose_file, tmp_path, monkeypatch):
|
|
318
|
+
"""All standard OCI arch names should pass through verbatim."""
|
|
319
|
+
monkeypatch.chdir(tmp_path)
|
|
320
|
+
for exotic in ("ppc64le", "s390x", "386", "arm"):
|
|
321
|
+
with self._patch_all_subprocess_runs(monkeypatch):
|
|
322
|
+
do_kaggle_upload(
|
|
323
|
+
name="myapp",
|
|
324
|
+
compose_file=str(compose_file),
|
|
325
|
+
registry_auth=None,
|
|
326
|
+
arch=exotic,
|
|
327
|
+
)
|
|
328
|
+
text = (tmp_path / ".kaggle" / "myapp" / "rdocker.py").read_text(encoding="utf-8")
|
|
329
|
+
assert f'ARCH = "{exotic}"' in text
|
|
330
|
+
|
|
331
|
+
def test_plan_uses_local_arch_when_no_override(self, compose_file, tmp_path, monkeypatch, capsys):
|
|
332
|
+
monkeypatch.chdir(tmp_path)
|
|
333
|
+
monkeypatch.setattr("platform.machine", lambda: "x86_64")
|
|
334
|
+
config_view = MagicMock(returncode=0, stdout="- username: alice\n", stderr="")
|
|
335
|
+
with patch("subprocess.run", return_value=config_view):
|
|
336
|
+
print_kaggle_upload_plan(
|
|
337
|
+
name="myapp",
|
|
338
|
+
compose_file=str(compose_file),
|
|
339
|
+
registry_auth=None,
|
|
340
|
+
)
|
|
341
|
+
out = capsys.readouterr().out
|
|
342
|
+
assert "Arch (from local): amd64" in out
|
|
343
|
+
assert "Arch (override)" not in out
|
|
344
|
+
assert 'ARCH = "amd64"' in out
|
|
345
|
+
# Plan must not write any files.
|
|
346
|
+
assert not (tmp_path / ".kaggle").exists()
|
|
347
|
+
|
|
348
|
+
def test_plan_arch_override_replaces_local_arch(self, compose_file, tmp_path, monkeypatch, capsys):
|
|
349
|
+
monkeypatch.chdir(tmp_path)
|
|
350
|
+
monkeypatch.setattr("platform.machine", lambda: "x86_64")
|
|
351
|
+
config_view = MagicMock(returncode=0, stdout="- username: alice\n", stderr="")
|
|
352
|
+
with patch("subprocess.run", return_value=config_view):
|
|
353
|
+
print_kaggle_upload_plan(
|
|
354
|
+
name="myapp",
|
|
355
|
+
compose_file=str(compose_file),
|
|
356
|
+
registry_auth=None,
|
|
357
|
+
arch="arm64",
|
|
358
|
+
)
|
|
359
|
+
out = capsys.readouterr().out
|
|
360
|
+
assert "Arch (override): arm64" in out
|
|
361
|
+
assert "Arch (from local)" not in out
|
|
362
|
+
assert 'ARCH = "arm64"' in out
|
|
363
|
+
assert 'ARCH = "amd64"' not in out
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
# ── --arch CLI flag (Click command surface) ───────────────────────────
|
|
367
|
+
|
|
368
|
+
class TestKaggleUploadArchCliFlag:
|
|
369
|
+
def test_cli_flag_wires_arch_through_in_try_mode(self, compose_file, monkeypatch, tmp_path):
|
|
370
|
+
monkeypatch.chdir(tmp_path)
|
|
371
|
+
monkeypatch.setattr("platform.machine", lambda: "x86_64")
|
|
372
|
+
monkeypatch.setattr("shutil.which", lambda _: "/usr/local/bin/kaggle")
|
|
373
|
+
config_view = MagicMock(returncode=0, stdout="- username: alice\n", stderr="")
|
|
374
|
+
with patch("subprocess.run", return_value=config_view) as run:
|
|
375
|
+
runner = CliRunner()
|
|
376
|
+
result = runner.invoke(
|
|
377
|
+
cli,
|
|
378
|
+
[
|
|
379
|
+
"kaggle", "upload", str(compose_file),
|
|
380
|
+
"--name", "myapp",
|
|
381
|
+
"--arch", "arm64",
|
|
382
|
+
"--try",
|
|
383
|
+
],
|
|
384
|
+
)
|
|
385
|
+
assert result.exit_code == 0, result.output
|
|
386
|
+
assert "Arch (override): arm64" in result.output
|
|
387
|
+
assert 'ARCH = "arm64"' in result.output
|
|
388
|
+
assert 'ARCH = "amd64"' not in result.output
|
|
389
|
+
# No push should have happened (--try): only `kaggle config view` was invoked.
|
|
390
|
+
assert run.call_count == 1
|
|
391
|
+
assert run.call_args[0][0][0:2] == ["kaggle", "config"]
|
|
392
|
+
# No files should have been written.
|
|
393
|
+
assert not (tmp_path / ".kaggle").exists()
|
|
394
|
+
|
|
395
|
+
def test_cli_flag_absent_uses_local_arch_in_try_mode(self, compose_file, monkeypatch, tmp_path):
|
|
396
|
+
monkeypatch.chdir(tmp_path)
|
|
397
|
+
monkeypatch.setattr("platform.machine", lambda: "x86_64")
|
|
398
|
+
monkeypatch.setattr("shutil.which", lambda _: "/usr/local/bin/kaggle")
|
|
399
|
+
config_view = MagicMock(returncode=0, stdout="- username: alice\n", stderr="")
|
|
400
|
+
with patch("subprocess.run", return_value=config_view):
|
|
401
|
+
runner = CliRunner()
|
|
402
|
+
result = runner.invoke(
|
|
403
|
+
cli,
|
|
404
|
+
["kaggle", "upload", str(compose_file), "--name", "myapp", "--try"],
|
|
405
|
+
)
|
|
406
|
+
assert result.exit_code == 0, result.output
|
|
407
|
+
assert "Arch (from local): amd64" in result.output
|
|
408
|
+
assert "Arch (override)" not in result.output
|
|
409
|
+
|
|
410
|
+
def test_cli_flag_help_text_mentions_arch(self):
|
|
411
|
+
runner = CliRunner()
|
|
412
|
+
result = runner.invoke(cli, ["kaggle", "upload", "--help"])
|
|
413
|
+
assert result.exit_code == 0
|
|
414
|
+
assert "--arch" in result.output
|
|
@@ -42,12 +42,7 @@ class TestLoadCommand:
|
|
|
42
42
|
runner = CliRunner()
|
|
43
43
|
result = runner.invoke(cli, ["load", "--name", "myapp", str(fake)])
|
|
44
44
|
assert result.exit_code != 0
|
|
45
|
-
|
|
46
|
-
# `No such option: --name` (no quotes around the option name).
|
|
47
|
-
assert (
|
|
48
|
-
"No such option '--name'" in result.output
|
|
49
|
-
or "No such option: --name" in result.output
|
|
50
|
-
)
|
|
45
|
+
assert "No such option '--name'" in result.output
|
|
51
46
|
|
|
52
47
|
def test_dry_run_prints_extract_and_load_plan(self, tmp_path):
|
|
53
48
|
fake = tmp_path / "myapp.tar.gz"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|