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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: remote-docker-cli
3
- Version: 0.3.0
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://github.com/kzhuo/remote-docker.git
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
- `--name` 缺省规则:
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`;如需覆盖,临时改 `KAGGLE_NOTEBOOK_TEMPLATE` 中的 `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
- │ └── cli.py # Click CLI 主体
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://github.com/kzhuo/remote-docker.git
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
- `--name` 缺省规则:
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`;如需覆盖,临时改 `KAGGLE_NOTEBOOK_TEMPLATE` 中的 `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
- │ └── cli.py # Click CLI 主体
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
@@ -1,3 +1,3 @@
1
1
  """remote-docker: A CLI tool for managing remote Docker hosts."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.3.1"
@@ -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(name="all")
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 = local_arch()
1155
- click.echo(f"🐳 Arch (from local): {arch}")
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 = local_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()
@@ -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, no_remote_config):
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
- # Click 7.x prints `No such option '--name'`, Click 8.x prints
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"