dockhand-cli 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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(uv run:*)",
5
+ "Bash(uv sync:*)",
6
+ "WebFetch(domain:github.com)"
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "sync": true,
3
+ "ssh": {
4
+ "hostname": "test.example.com",
5
+ "user": "testuser",
6
+ "identityfile": "~/.ssh/id_rsa"
7
+ },
8
+ "docker": {
9
+ "dockerfile": "Dockerfile",
10
+ "imagename": "test:latest",
11
+ "containerworkdir": "/workdir",
12
+ "volumes": []
13
+ }
14
+ }
@@ -0,0 +1,53 @@
1
+ # Project-specific
2
+ .dtu_hpc.json
3
+ .dtu_docker_history.json
4
+
5
+ # Python
6
+ __pycache__/
7
+ *.py[cod]
8
+ *$py.class
9
+ *.so
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # Virtual environments
31
+ venv/
32
+ ENV/
33
+ env/
34
+ .venv
35
+
36
+ # IDE
37
+ .vscode/
38
+ .idea/
39
+ *.swp
40
+ *.swo
41
+ *~
42
+ .DS_Store
43
+
44
+ # Testing
45
+ .pytest_cache/
46
+ .coverage
47
+ htmlcov/
48
+ .tox/
49
+
50
+ # MyPy
51
+ .mypy_cache/
52
+ .dmypy.json
53
+ dmypy.json
@@ -0,0 +1,309 @@
1
+ # AGENTS.md — dockhand Development Guide
2
+
3
+ ## Project Overview
4
+
5
+ **dockhand** is a standalone CLI tool, originally extracted from DTU-HPC-CLI, for managing Docker containers on remote machines (or locally). It provides a unified interface to build, run, queue, manage, and monitor Docker containers via SSH or locally.
6
+
7
+ **Key philosophy:** Simple, flat command structure with sensible SSH defaults. One `.dockhand.json` config file works everywhere.
8
+
9
+ Since extraction, dockhand has grown features DTU-HPC-CLI never had: a task-spooler-based job queue, a mount-vs-bake code delivery mode, a direct (non-queued) run transport, and baked-image pruning. See [Relationship to DTU-HPC-CLI](#relationship-to-dtu-hpc-cli).
10
+
11
+ ## Commands
12
+
13
+ **Prerequisites:**
14
+ - Install [uv](https://docs.astral.sh/uv/): `curl -LsSf https://astral.sh/uv/install.sh | sh`
15
+
16
+ **Installation & Setup:**
17
+ ```bash
18
+ # Install dependencies and create lock file
19
+ uv sync
20
+
21
+ # Update lock file with latest versions (if needed)
22
+ uv lock --upgrade
23
+ ```
24
+
25
+ **Run the CLI locally:**
26
+ ```bash
27
+ uv run dockhand --help
28
+ ```
29
+
30
+ **Lint:**
31
+ ```bash
32
+ uv run ruff check .
33
+ ```
34
+
35
+ **Format:**
36
+ ```bash
37
+ uv run ruff format .
38
+ ```
39
+
40
+ **Auto-fix lint issues:**
41
+ ```bash
42
+ uv run ruff check --fix .
43
+ ```
44
+
45
+ ## Architecture
46
+
47
+ ### Entry Point
48
+ `dockhand/__init__.py` — Defines the Typer CLI app. All commands are at the top level (flat structure, not nested under a `docker` sub-group). Each command validates config with `cli_config.check_docker()` and delegates to an `execute_*` function in the relevant module below.
49
+
50
+ **Commands at top level:**
51
+ - `dockhand submit` — sync code, then queue/run a container
52
+ - `dockhand run` — queue/run from an already-built image, no sync
53
+ - `dockhand install` — build the image only
54
+ - `dockhand logs`, `stop`, `remove`, `jobs`, `urgent`, `prune` — job/queue lifecycle (`manage.py` + `queue.py`)
55
+ - `dockhand history` — show past runs
56
+ - `dockhand volumes`, `download` — inspect and pull files from mounted volumes
57
+ - `dockhand resubmit` — rerun a past job with optional overrides
58
+ - `dockhand tunnel` — SSH-forward container ports to localhost
59
+
60
+ ### Configuration System
61
+ `dockhand/config.py` — The `CLIConfig` class is loaded at module import time (`cli_config = CLIConfig.load()`) by walking up the directory tree to find `.dockhand.json`. Contains:
62
+ - `SSHConfig` — hostname, user, identity file for SSH connections
63
+ - `DockerConfig` — dockerfile, imagename, volumes, ports, gpus, containerworkdir, preserve_paths, code_delivery
64
+ - `QueueConfig` — enabled, tool, slots (task-spooler queue settings)
65
+ - `CLIConfig` — top-level config holder (ssh, docker, queue, sync, remote_path, profiles)
66
+
67
+ Config file: `.dockhand.json` in the project root (must also contain a `.git` — `CLIConfig.load()` errors if no git repo is found). History file: `.dockhand_history.json` (same location by default, overridable via `history_path`).
68
+
69
+ ### Client Abstraction
70
+ `dockhand/client/` — `Client` (abstract base) has two implementations:
71
+ - `SSHClient` — connects via Fabric/Paramiko over SSH
72
+ - `LocalClient` — runs commands locally
73
+
74
+ `get_client()` picks `LocalClient` when `cli_config.ssh` is unset or the configured hostname resolves to loopback, otherwise `SSHClient`. `get_client_for_host(hostname)` does the same but for an explicit hostname (used by `logs`/`stop`/`remove`, which read the host from the job's history entry rather than current config).
75
+
76
+ ### Key Modules
77
+ - `submit.py` — Builds the `docker run` command (code mount vs. data volumes vs. GPU/port flags), resolves code delivery, and hands off to a transport to start the job.
78
+ - `build.py` — Runs `docker build` on the client (optionally syncing first).
79
+ - `manage.py` — Job lifecycle: `logs`, `stop`, `remove`, `jobs` (`execute_stats`), and `prune` (removes baked images no longer referenced by an active job).
80
+ - `resubmit.py` — Looks up a history entry and re-invokes `execute_submit` with overrides; pins to the original baked image when applicable.
81
+ - `queue.py` — Task spooler (`tsp`) integration: submit/list/promote/remove/kill, plus `ts -l` output parsing.
82
+ - `transport.py` — Abstracts "how a job runs": `TaskSpoolerTransport` (queue enabled) vs. `DockerTransport` (direct `docker run -d`). Both expose the same interface (`submit`, `list_jobs`, `logs`, `stop`, `remove`) so job-management commands don't care which backend created a job; the transport used is recorded per job in history.
83
+ - `tagging.py` — Resolves the image tag/ref for baked code delivery (content-addressed from git commit + dirty-state hash; unique per queued submit, reused for direct runs).
84
+ - `history.py` — Reads/writes `.dockhand_history.json`, reserves/looks up local job IDs.
85
+ - `volumes.py` — Lists the container filesystem as a tree (code mount + data volumes) and resolves a workdir-relative path back to its host path.
86
+ - `download.py` — Uses `volumes._resolve_to_host` + rsync to pull a file/directory from a volume.
87
+ - `tunnel.py` — SSH local port forwarding to container ports (via Fabric).
88
+ - `sync.py` — Uses `rsync` over SSH to copy local files to `remote_path`, respecting `.gitignore`; prompts to confirm when the worktree is dirty.
89
+ - `config.py` — Configuration loading and validation.
90
+ - `error.py` — Centralized error reporting with rich panels.
91
+ - `constants.py` — Config/history filenames.
92
+
93
+ > **Known inconsistency:** `tunnel.py` still looks up history entries by a `container_id` field (`entry["container_id"]`), but `history.py` no longer writes that field — entries are keyed by `local_id` with a transport `handle`. Passing an explicit `container_id` to `dockhand tunnel` will not resolve; only the no-argument (last job) path works reliably.
94
+
95
+ ### Docker History
96
+ Stores container runs in `.dockhand_history.json` as JSON. Each entry contains:
97
+ ```json
98
+ {
99
+ "local_id": 7,
100
+ "timestamp": 1234567890.123,
101
+ "config": {
102
+ "gpus": "all",
103
+ "volumes": [...],
104
+ "imagename": "my-image",
105
+ "commands": ["python", "train.py"],
106
+ "ports": ["6006:6006"],
107
+ "image_ref": "my-image:abc123def456", // present when built (baked delivery)
108
+ "branch": "main" // optional, detected from git
109
+ },
110
+ "transport": "task_spooler", // or "docker"
111
+ "handle": 12, // tsp job id, or container name for direct runs
112
+ "ts_job_id": 12, // task_spooler transport only
113
+ "host": "remote.example.com", // or "localhost"
114
+ "started_at": 1234567891.0, // optional, set the first time `jobs` observes it running
115
+ "ended_at": 1234567895.0 // optional, set the first time `jobs` observes it finished/failed/stopped
116
+ }
117
+ ```
118
+
119
+ Used by `resubmit` (look up a previous run and re-run with overrides, pinning the original baked image when unchanged), `logs`, `stop`, `remove`, `jobs`, `urgent`, `prune` (default to latest if no ID given). Job-management commands dispatch to the transport recorded on the entry (`transport_for_entry`), so `logs`/`stop`/`remove` work the same regardless of whether a job went through the queue or ran directly.
120
+
121
+ `started_at`/`ended_at` aren't queried from tsp/docker (neither exposes exact start/end timestamps cheaply for both transports) — `jobs` and `logs` each stamp them lazily the first time they happen to observe a job in the running/terminal state, so a job never checked on while running will show no start time once it finishes. `dockhand jobs` displays these as Started/Ended (absolute, `%Y-%m-%d %H:%M:%S`) and Duration (elapsed while running, total once finished) columns; `dockhand logs` prints a one-line "running for Xm" / "finished in Xm" header before the log output.
122
+
123
+ ### Design Patterns
124
+
125
+ **DockerDefault factory:**
126
+ In `__init__.py`, `DockerDefault` is a callable factory that pulls defaults from `cli_config.docker` at call time. Enables Typer's `default_factory` to respect active profiles.
127
+
128
+ **Transport abstraction:**
129
+ `get_transport()` in `transport.py` picks `TaskSpoolerTransport` or `DockerTransport` based on `cli_config.queue.enabled` at submit time. Every job-management command re-derives the correct transport per-job from the history entry (`transport_for_entry`) rather than from current config, so switching `queue.enabled` doesn't strand in-flight jobs from the other mode.
130
+
131
+ **Code delivery resolution:**
132
+ `DockerConfig.resolve_code_delivery(queue_enabled)` picks `mount` or `bake` when `code_delivery` isn't explicitly set: `bake` when the queue is enabled (avoids code drift while a job waits in queue), `mount` otherwise (zero-rebuild iteration). See README's [Code delivery](README.md#code-delivery-mount-vs-bake) section for the full rationale.
133
+
134
+ **Profile support:**
135
+ `cli_config.load_profile(name)` loads overrides from `.dockhand.json` profiles section (currently merges `history_path`, `remote_path`, `ssh`). All config classes support `validate()` for safe merging.
136
+
137
+ **Command structure:**
138
+ Each command:
139
+ 1. Calls `cli_config.check_docker()` to validate config
140
+ 2. Calls an `execute_*` function from the relevant module
141
+ 3. Passes `cli_config.docker` (and often `cli_config.queue`) as config arguments
142
+
143
+ ## Relationship to DTU-HPC-CLI
144
+
145
+ - **Flat commands** — no `docker` sub-group. Commands are top-level.
146
+ - **No HPC support** — SubmitConfig, InstallConfig, and all LSF/job submission code removed
147
+ - **Docker-focused** — all UI/UX optimized for docker workflows
148
+ - **Config-compatible in spirit, not in filename** — dockhand originally reused DTU-HPC-CLI's `.dtu_hpc.json`/`.dtu_docker_history.json` names; both have since been renamed to `.dockhand.json`/`.dockhand_history.json` (see `constants.py`). Old DTU-HPC-CLI config files need renaming (or a `history_path` override) to work with dockhand.
149
+ - **Diverged, not just trimmed** — dockhand has added its own features DTU-HPC-CLI doesn't have: a task-spooler queue (`queue.py`/`transport.py`), slot reservations, `mount`/`bake` code delivery (`tagging.py`), a direct-run transport for when the queue is off, `urgent`/`prune`/`tunnel` commands. Maintained independently; version/release cadence is separate from DTU-HPC-CLI.
150
+
151
+ ## Key Files and Their Responsibilities
152
+
153
+ | File | Purpose |
154
+ |------|---------|
155
+ | `__init__.py` | CLI app definition, command routing, config defaults |
156
+ | `submit.py` | Builds the `docker run` command, resolves code delivery, submits via transport |
157
+ | `build.py` | `docker build` execution |
158
+ | `manage.py` | `logs`, `stop`, `remove`, `jobs`, `prune` |
159
+ | `resubmit.py` | Rerun a past job with overrides |
160
+ | `queue.py` | Task spooler (`tsp`) integration |
161
+ | `transport.py` | Task-spooler vs. direct-docker execution backends |
162
+ | `tagging.py` | Image tag resolution for baked code delivery |
163
+ | `history.py` | Read/write `.dockhand_history.json`, job ID allocation |
164
+ | `volumes.py` | Container filesystem tree, path resolution |
165
+ | `download.py` | rsync-based file download from volumes |
166
+ | `tunnel.py` | SSH port forwarding to container ports |
167
+ | `config.py` | Config loading, validation, profile support |
168
+ | `client/__init__.py` | Auto-detect and return appropriate client |
169
+ | `client/base.py` | Abstract Client interface |
170
+ | `client/local.py` | Local command execution |
171
+ | `client/ssh.py` | Remote execution via SSH (Fabric/Paramiko) |
172
+ | `sync.py` | rsync-based code synchronization |
173
+ | `error.py` | Error reporting |
174
+ | `constants.py` | Config/history file names |
175
+
176
+ ## Configuration Format
177
+
178
+ `.dockhand.json` in project root (project root is also where `CLIConfig.load()` expects to find a `.git` directory):
179
+
180
+ ```json
181
+ {
182
+ "sync": true,
183
+ "ssh": {
184
+ "user": "your_username",
185
+ "identityfile": "~/.ssh/id_rsa",
186
+ "hostname": "remote.example.com"
187
+ },
188
+ "queue": {
189
+ "enabled": true,
190
+ "slots": 1
191
+ },
192
+ "docker": {
193
+ "dockerfile": "Dockerfile",
194
+ "imagename": "my-image",
195
+ "volumes": [
196
+ {
197
+ "hostpath": "/local/data",
198
+ "containerpath": "/data",
199
+ "permissions": "rw"
200
+ }
201
+ ],
202
+ "ports": ["8080:80"],
203
+ "gpus": "all",
204
+ "containerworkdir": "/",
205
+ "preserve_paths": [".venv"],
206
+ "code_delivery": null
207
+ },
208
+ "remote_path": "~/my-project",
209
+ "profiles": {
210
+ "dev": {
211
+ "docker": {
212
+ "gpus": "1",
213
+ "dockerfile": "Dockerfile.dev"
214
+ }
215
+ }
216
+ }
217
+ }
218
+ ```
219
+
220
+ All options are optional except `dockerfile`, `imagename`, and `volumes` within docker config. `queue.slots` may also be set as `docker.slots` for back-compat (deprecation warning printed). Full option tables and defaults are documented in README.md's [Configuration](README.md#configuration) section — keep both in sync when config shape changes.
221
+
222
+ ## Common Workflows
223
+
224
+ **First time setting up:**
225
+ 1. Create `.dockhand.json` with `docker` and `ssh` sections
226
+ 2. Run `dockhand install` to build the image
227
+ 3. Run `dockhand submit 'command'` to run a container
228
+
229
+ **Development iteration:**
230
+ ```bash
231
+ uv run dockhand submit --gpus 1 'python train.py' # Sync + queue/run with 1 GPU
232
+ uv run dockhand logs --n 50 # Check last 50 log lines
233
+ uv run dockhand resubmit --gpus 2 # Re-run with 2 GPUs
234
+ uv run dockhand download results/model.pth # Get results back
235
+ ```
236
+
237
+ **Quick rebuild:**
238
+ ```bash
239
+ uv run dockhand install --dockerfile Dockerfile.dev # Rebuild only
240
+ uv run dockhand run 'bash' # Run interactively
241
+ ```
242
+
243
+ **Using profiles:**
244
+ ```bash
245
+ uv run dockhand --profile dev submit 'python train.py' # Use dev profile
246
+ uv run dockhand --profile prod install # Build prod image
247
+ ```
248
+
249
+ **Queue management:**
250
+ ```bash
251
+ uv run dockhand jobs # List active jobs
252
+ uv run dockhand urgent 5 # Promote job #5 to front of queue
253
+ uv run dockhand prune --dry-run # Preview unused baked images before removing
254
+ ```
255
+
256
+ ## Testing & Validation
257
+
258
+ No automated tests yet. Manual validation:
259
+ ```bash
260
+ uv run dockhand --help # Check CLI structure
261
+ uv run dockhand submit --help # Check submit options
262
+ uv run ruff check . # Lint
263
+ uv run ruff format --check . # Format check
264
+ ```
265
+
266
+ ## Important Design Decisions
267
+
268
+ 1. **Flat commands** — No `docker` sub-group. Simpler for a docker-only tool.
269
+ 2. **SSH by default** — Assumes docker host is remote. Auto-detects local by resolving the configured hostname to a loopback address.
270
+ 3. **History in JSON** — Simple, human-readable, easily editable.
271
+ 4. **Profile support** — Allows per-project config variants.
272
+ 5. **Minimal dependencies** — typer, fabric, paramiko, gitpython, rich.
273
+ 6. **Transport abstraction over queue state** — job-management commands read the transport from each job's own history entry rather than current `queue.enabled`, so toggling the queue mid-flight doesn't orphan existing jobs.
274
+ 7. **Code-delivery default follows queue mode** — `bake` when queued (avoids code drift while waiting), `mount` when not (zero-rebuild iteration); explicit `code_delivery` overrides either way.
275
+
276
+ ## Extraction History
277
+
278
+ - **Source:** DTU-HPC-CLI (github.com/ChrisFugl/DTU-HPC-CLI)
279
+ - **Extracted:** 2026-04-09
280
+ - **Initial changes:**
281
+ - Removed: SubmitConfig, InstallConfig, types.py, all HPC modules
282
+ - Modified: config.py (trimmed), __init__.py (flat commands)
283
+ - Unchanged at the time: docker.py, sync.py, error.py, client/*, constants.py
284
+ - **Since extraction:** the original monolithic `docker.py` was split into `build.py`, `submit.py`, `manage.py`, `resubmit.py`, `history.py`, `download.py`, `volumes.py`, `tunnel.py`; `queue.py`, `transport.py`, and `tagging.py` were added for the task-spooler queue, transport abstraction, and baked code delivery. Config/history filenames moved from `.dtu_hpc.json`/`.dtu_docker_history.json` to `.dockhand.json`/`.dockhand_history.json`.
285
+
286
+ ## Future Enhancements
287
+
288
+ Potential improvements specific to dockhand (volume inspection is already implemented — see `volumes.py`):
289
+ - Docker Compose support (`dockhand compose up`)
290
+ - Kubernetes pod management
291
+ - Local-only mode (drop SSH dependency for pure local docker)
292
+ - Config auto-generation wizard
293
+ - Container registry integration (Docker Hub, ECR, GCR)
294
+ - Multi-container orchestration helpers
295
+ - Fix `tunnel.py`'s stale `container_id` history lookup (see [Known inconsistency](#key-modules) above)
296
+
297
+ ## Contributing
298
+
299
+ When working on dockhand:
300
+ 1. Keep the flat command structure — don't re-introduce `docker` sub-groups
301
+ 2. Maintain `.dockhand.json` compatibility
302
+ 3. Test manually with `uv run dockhand <command> --help` and actual docker operations
303
+ 4. Follow ruff lint/format rules (use `uv run ruff check . && uv run ruff format .`)
304
+ 5. Document new features in README.md and AGENTS.md
305
+ 6. Update this file's Architecture section when modules are added, split, or renamed — it has drifted from actual code structure before
306
+
307
+ ## Maintenance
308
+
309
+ dockhand is maintained independently from DTU-HPC-CLI. Version bumps and releases are separate. If you need HPC support, use the original DTU-HPC-CLI package.