dockhand-cli 0.3.1__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.
@@ -0,0 +1,478 @@
1
+ Metadata-Version: 2.5
2
+ Name: dockhand-cli
3
+ Version: 0.3.1
4
+ Summary: CLI for managing Docker containers on remote machines
5
+ Project-URL: Repository, https://github.com/NicholasPHansen/dockhand
6
+ Author: Nicholas P. Hansen
7
+ License: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: fabric>=3.2.2
11
+ Requires-Dist: gitpython>=3.1.43
12
+ Requires-Dist: paramiko>=3.4.1
13
+ Requires-Dist: typer>=0.15.4
14
+ Provides-Extra: dev
15
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Dockhand
19
+
20
+ ![](media/Gemini_Generated_Image_i700fvi700fvi700.png)
21
+
22
+ A CLI tool for managing Docker containers on remote machines. Build, run, and manage Docker containers with a single command from your local machine.
23
+
24
+ **Why this project?**
25
+
26
+ Managing Docker workloads across multiple machines is painful: keeping code in sync, remembering which version runs where, handling build/run/debug cycles remotely.
27
+
28
+ `dockhand` solves this by letting you manage everything from your laptop — code syncs automatically, builds happen on the remote, and results can be downloaded back. No more juggling multiple git repos or SSH sessions.
29
+
30
+ This project is *heavily* inspired by the awesome work from @ChrisFugl's [DTU-HPC-CLI](https://github.com/ChrisFugl/DTU-HPC-CLI).
31
+
32
+ ## Features
33
+
34
+ - **Fast Iteration**: Code is mounted into the container at runtime — no rebuild needed when your code changes
35
+ - **Job Queue**: All workloads are submitted through [task spooler](https://viric.name/soft/ts/) so jobs run in order without competing for resources
36
+ - **Slot Reservations**: Declare how many CPU slots a job needs so heavy jobs don't block each other
37
+ - **Remote Docker Management**: Build and run Docker containers on a remote machine via SSH
38
+ - **Local or Remote**: Automatically detect localhost vs remote, or explicitly configure
39
+ - **Port Forwarding**: Establish SSH tunnels to access container ports locally
40
+ - **File Syncing**: Automatically sync your local code to the remote before running
41
+ - **Volume Management**: Mount data directories into containers and download results back
42
+ - **Stable Job IDs**: Local job IDs increment independently of the host, so IDs are unambiguous across machines
43
+ - **Quick Resubmit**: Easily rerun previous jobs with the same or different parameters
44
+
45
+ ## Requirements
46
+
47
+ ### Local machine
48
+
49
+ - Python 3.10+
50
+ - [git](https://git-scm.com/) — used for branch tracking and code sync
51
+
52
+ ### Docker host (remote or local)
53
+
54
+ - [Docker](https://docs.docker.com/engine/install/) — container runtime
55
+ - [NVIDIA Container Toolkit](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/install-guide.html) (`nvidia-docker`) — required only if using GPUs (`--gpus`)
56
+ - [task spooler (`tsp`)](https://viric.name/soft/ts/) — job queue daemon
57
+
58
+ Installing task spooler on Ubuntu/Debian:
59
+
60
+ ```bash
61
+ sudo apt-get install task-spooler
62
+ ```
63
+
64
+ On other systems the binary may be called `ts` instead of `tsp` — check your package manager.
65
+
66
+ **Configuring the total slot count** (optional, recommended for multi-user setups):
67
+
68
+ ```bash
69
+ # Set the number of available slots to match CPU cores (run once on the host)
70
+ tsp -S 16
71
+ ```
72
+
73
+ Jobs default to 1 slot each. Use `--slots` to reserve more (see [Slot Reservations](#slot-reservations)).
74
+
75
+ ## Installation
76
+
77
+ **From source with uv (recommended):**
78
+
79
+ [Install uv](https://docs.astral.sh/uv/):
80
+
81
+ ```bash
82
+ curl -LsSf https://astral.sh/uv/install.sh | sh
83
+ ```
84
+
85
+ Then clone and sync:
86
+
87
+ ```bash
88
+ git clone https://github.com/nicholasphansen/dockhand.git
89
+ cd dockhand
90
+ uv sync
91
+ uv run dockhand --help
92
+ ```
93
+
94
+ **Or with pip:**
95
+
96
+ ```bash
97
+ pip install dockhand
98
+ ```
99
+
100
+ ## Usage
101
+
102
+ All commands work with the same `.dockhand.json` configuration file. See [Configuration](#configuration) below.
103
+
104
+ ### Available Commands
105
+
106
+ | Command | Description |
107
+ |---------|-------------|
108
+ | `submit` | Build the image and queue a container run |
109
+ | `run` | Queue a container run from an already-built image |
110
+ | `install` | Build the Docker image without running it |
111
+ | `jobs` | List active (running/queued) jobs, with start/end/duration — use `--all` for finished jobs too |
112
+ | `logs` | Show logs from a job, preceded by a running/finished duration line — use `--follow`/`-f` to stream live |
113
+ | `stop` | Stop a **running** job |
114
+ | `remove` | Remove a **queued** job before it starts |
115
+ | `urgent` | Promote a queued job to the front of the queue |
116
+ | `prune` | Remove baked images no longer used by an active job |
117
+ | `history` | Show history of all past runs |
118
+ | `volumes` | List the full container filesystem as a tree |
119
+ | `download` | Download a file from a docker volume |
120
+ | `resubmit` | Resubmit a previous job with optional overrides |
121
+ | `tunnel` | Forward container ports to localhost via SSH tunnel |
122
+
123
+ All commands that accept a job ID default to the most recent job if no ID is given.
124
+
125
+ ### Typical workflow
126
+
127
+ ```bash
128
+ # 1. Build the image once (or when dependencies change)
129
+ dockhand install
130
+
131
+ # 2. Iterate freely — submit syncs code and queues a run, no rebuild needed
132
+ dockhand submit 'python train.py --epochs 10'
133
+ dockhand submit 'python train.py --epochs 10' # code changed? just submit again
134
+ ```
135
+
136
+ ### Examples
137
+
138
+ ```bash
139
+ # Sync code and queue a run
140
+ dockhand submit 'python train.py --epochs 10'
141
+
142
+ # Reserve 4 CPU slots for a parallel job
143
+ dockhand submit --slots 4 'python train.py --workers 4'
144
+
145
+ # Queue a run with GPUs and port mappings
146
+ dockhand submit --gpus all -p 6006:6006 'python train.py'
147
+
148
+ # Queue a run without syncing (code already up to date)
149
+ dockhand submit --no-sync 'python train.py'
150
+
151
+ # Queue a run from an already-built image, skip sync
152
+ dockhand run 'python train.py --epochs 20'
153
+
154
+ # Build image (run once, or when dependencies change)
155
+ dockhand install
156
+ # Show full build output
157
+ dockhand install -v
158
+ # Build a specific Dockerfile
159
+ dockhand install --dockerfile Dockerfile.prod
160
+
161
+ # Check active jobs
162
+ dockhand jobs
163
+
164
+ # Check all jobs including finished
165
+ dockhand jobs --all
166
+
167
+ # Stream logs from the last job
168
+ dockhand logs --follow
169
+
170
+ # Show the last 50 lines from job #3
171
+ dockhand logs 3 --n 50
172
+
173
+ # Stop the last running job
174
+ dockhand stop
175
+
176
+ # Remove a queued job before it starts
177
+ dockhand remove 4
178
+
179
+ # Promote job #5 to the front of the queue
180
+ dockhand urgent 5
181
+
182
+ # Forward container ports to localhost
183
+ dockhand tunnel
184
+
185
+ # See all past runs
186
+ dockhand history
187
+
188
+ # Browse mounted volumes as a tree
189
+ dockhand volumes --depth 2
190
+
191
+ # Download results
192
+ dockhand download results/model.pth
193
+
194
+ # Resubmit the latest job with different GPUs
195
+ dockhand resubmit --gpus 2
196
+ ```
197
+
198
+ **Note:** If you've installed dockhand globally, you can omit `uv run`.
199
+
200
+ `jobs` shows Started/Ended/Duration for each job, and `logs` prints a one-line
201
+ `running for 5m30s` / `finished in 12m45s` header before the log output. Neither the
202
+ queue nor docker exposes exact start/end timestamps cheaply, so these are recorded the
203
+ first time `jobs` or `logs` happens to observe a job as running or finished — a job
204
+ never checked on while running will show no duration once it's done.
205
+
206
+ Resubmitting a job that ran in [`bake`](#code-delivery-mount-vs-bake) mode reruns the exact
207
+ image it originally built — recorded per job — rather than rebuilding from current code, so a
208
+ past run reproduces faithfully. (Overriding `--imagename` opts out and uses the new image.)
209
+
210
+ ## Slot Reservations
211
+
212
+ When multiple users share a Docker host, jobs may need different amounts of CPU resources. The `--slots` option maps to `tsp -N <n>`, which tells task spooler how many of the host's total slots a job should consume before it is allowed to start.
213
+
214
+ ```bash
215
+ # Lightweight job — uses 1 slot (default)
216
+ dockhand run 'python eval.py'
217
+
218
+ # Parallel job — blocks 8 slots while running
219
+ dockhand run --slots 8 'python train.py --workers 8'
220
+ ```
221
+
222
+ The total number of slots available on the host is set with `tsp -S <n>` (run once by the system admin). A job only starts when enough free slots are available, so heavy jobs naturally queue behind each other without manual coordination.
223
+
224
+ The default slot count can also be set in `.dockhand.json` so it applies to every submission without needing the flag:
225
+
226
+ ```json
227
+ {
228
+ "queue": {
229
+ "slots": 4
230
+ }
231
+ }
232
+ ```
233
+
234
+ ## Volumes
235
+
236
+ The `volumes` command lists the container filesystem as a unified tree rooted at `containerworkdir`. It searches the code mount (project root → `containerworkdir`) and all configured data volumes on the host directly — no running container required. Files appear at their container paths.
237
+
238
+ ```bash
239
+ # Show container filesystem (default depth: 5 levels)
240
+ dockhand volumes
241
+
242
+ # Expand to a specific depth; folders at the limit show as collapsed with ...
243
+ dockhand volumes --depth 3
244
+
245
+ # Show filesystem for a specific past job
246
+ dockhand volumes 42
247
+
248
+ # Same via download --list
249
+ dockhand download --list --depth 2
250
+ ```
251
+
252
+ The `download` command resolves any workdir-relative path (as shown by `volumes`) back to its host location and downloads it via rsync. Directory downloads preserve the full path structure locally.
253
+
254
+ ```bash
255
+ # Download a file
256
+ dockhand download results/model.pth
257
+
258
+ # Download a directory (trailing slash) — files land in reports/figures/ locally
259
+ dockhand download reports/figures/
260
+ ```
261
+
262
+ ## Configuration
263
+
264
+ Create a `.dockhand.json` file in your project root.
265
+
266
+ ### Minimal Configuration
267
+
268
+ ```json
269
+ {
270
+ "docker": {
271
+ "dockerfile": "Dockerfile",
272
+ "imagename": "my-image",
273
+ "volumes": []
274
+ }
275
+ }
276
+ ```
277
+
278
+ ### SSH Configuration
279
+
280
+ To run Docker commands on a remote machine:
281
+
282
+ ```json
283
+ {
284
+ "ssh": {
285
+ "user": "your_username",
286
+ "identityfile": "/path/to/private/key",
287
+ "hostname": "remote.example.com"
288
+ },
289
+ "docker": { "..." }
290
+ }
291
+ ```
292
+
293
+ Hostname detection:
294
+
295
+ - No `ssh` config, or hostname resolves to `127.0.0.1` → runs locally
296
+ - Otherwise → connects via SSH
297
+
298
+ ### Docker Configuration
299
+
300
+ ```json
301
+ {
302
+ "sync": true,
303
+ "ssh": { "..." },
304
+ "queue": {
305
+ "enabled": true,
306
+ "slots": 1
307
+ },
308
+ "docker": {
309
+ "dockerfile": "Dockerfile",
310
+ "imagename": "my-image",
311
+ "volumes": [
312
+ {
313
+ "hostpath": "/local/data",
314
+ "containerpath": "/data",
315
+ "permissions": "rw"
316
+ }
317
+ ],
318
+ "ports": ["8080:80"],
319
+ "gpus": "all",
320
+ "containerworkdir": "/",
321
+ "preserve_paths": [".venv"]
322
+ }
323
+ }
324
+ ```
325
+
326
+ **Top-level options:**
327
+
328
+ | Option | Description | Default |
329
+ |--------|-------------|---------|
330
+ | `sync` | Rsync local code to remote before building | `true` |
331
+
332
+ **Queue sub-config options:**
333
+
334
+ | Option | Description | Default |
335
+ |--------|-------------|---------|
336
+ | `enabled` | Submit jobs through the task-spooler queue | `false` |
337
+ | `tool` | Queue backend | `task_spooler` |
338
+ | `slots` | Default queue slots to reserve per job (overridden by `--slots`) | `1` |
339
+
340
+ When `enabled` is **true**, jobs are queued with `tsp` and run in submission order. When
341
+ **false**, there is no queue — each submit starts immediately as a detached container
342
+ (`docker run -d --name dockhand-<id>`). `logs`, `stop`, `jobs`, and `remove` work the same
343
+ in both modes; they transparently use `tsp` or `docker` per the job's recorded transport.
344
+ `--slots` and `--urgent` are queue-only and are ignored (with a warning) when the queue is off.
345
+
346
+ **Docker sub-config options:**
347
+
348
+ | Option | Description | Default |
349
+ |--------|-------------|---------|
350
+ | `dockerfile` | Path to the Dockerfile | required |
351
+ | `imagename` | Docker image name | required |
352
+ | `volumes` | List of volume mounts | required |
353
+ | `ports` | Port mappings | — |
354
+ | `gpus` | GPU flag passed to `docker run --gpus` | — |
355
+ | `containerworkdir` | Path inside the container where the project is mounted and commands run from | `/` |
356
+ | `preserve_paths` | Subpaths under `containerworkdir` to keep from the image instead of the bind-mounted host copy (e.g. `.venv`, `node_modules`) — only used in `mount` delivery | `[]` |
357
+ | `code_delivery` | How code reaches the container: `mount` or `bake` | derived from `queue.enabled` |
358
+
359
+ > **Note:** `slots` moved from the `docker` block to the `queue` block. A `docker.slots` value is still honored with a deprecation warning.
360
+
361
+ ### Code delivery: `mount` vs `bake`
362
+
363
+ `code_delivery` controls how your code gets into the container:
364
+
365
+ - **`mount`** — bind-mount the synced project over `containerworkdir` at run time (with
366
+ `preserve_paths` protecting image-built artifacts). No rebuild per submit, so it's ideal
367
+ for tight edit-run iteration. The code is read when the container *starts*.
368
+ - **`bake`** — build the code into the image and run that image with no code mount. Each
369
+ submit builds (Docker layer caching keeps this cheap when only source changed).
370
+
371
+ When unset it defaults by mode: **`bake` when `queue.enabled` is true, `mount` otherwise.**
372
+ The reason is drift. A queued job may sit in the queue before it runs; with `mount` it would
373
+ pick up whatever code is on disk when it *dequeues*, so editing code after submitting silently
374
+ changes an already-queued job. `bake` pins each queued submit to an immutable, content-addressed
375
+ image tag (derived from the git commit, plus a hash of uncommitted changes) so the job runs
376
+ exactly the code it was submitted with. Without a queue there's no drift window, so `mount`'s
377
+ zero-rebuild iteration wins. Set `code_delivery` explicitly to override the default either way.
378
+
379
+ > Data `volumes` are always mounted regardless of `code_delivery`; only the *code* mount differs.
380
+ > For guaranteed-immutable queued builds, commit before submitting — untracked file *contents*
381
+ > are not folded into the image tag.
382
+
383
+ Each distinct baked submit produces its own image tag, and `resubmit` reruns the exact tag a job
384
+ originally built. Those tags accumulate over time; run `dockhand prune` to remove baked images that
385
+ no running or queued job still references (`--dry-run` to preview, `-y` to skip the prompt).
386
+
387
+ ### Profiles
388
+
389
+ Use profiles to switch between different configurations:
390
+
391
+ ```json
392
+ {
393
+ "docker": {
394
+ "dockerfile": "Dockerfile",
395
+ "imagename": "my-image"
396
+ },
397
+ "profiles": {
398
+ "prod": {
399
+ "queue": { "slots": 8 },
400
+ "docker": {
401
+ "gpus": "all",
402
+ "dockerfile": "Dockerfile.prod"
403
+ }
404
+ },
405
+ "dev": {
406
+ "queue": { "slots": 2 },
407
+ "docker": {
408
+ "gpus": "1",
409
+ "dockerfile": "Dockerfile.dev"
410
+ }
411
+ }
412
+ }
413
+ }
414
+ ```
415
+
416
+ ```bash
417
+ dockhand --profile dev submit 'python train.py'
418
+ dockhand --profile prod submit 'python train.py'
419
+ ```
420
+
421
+ ### Complete Example
422
+
423
+ ```json
424
+ {
425
+ "sync": true,
426
+ "ssh": {
427
+ "user": "myuser",
428
+ "identityfile": "~/.ssh/id_rsa",
429
+ "hostname": "gpu-server.example.com"
430
+ },
431
+ "docker": {
432
+ "dockerfile": "Dockerfile",
433
+ "imagename": "my-training-app",
434
+ "volumes": [
435
+ {
436
+ "hostpath": "/local/data",
437
+ "containerpath": "/data",
438
+ "permissions": "rw"
439
+ },
440
+ {
441
+ "hostpath": "/local/models",
442
+ "containerpath": "/models",
443
+ "permissions": "rw"
444
+ }
445
+ ],
446
+ "ports": ["6006:6006"],
447
+ "gpus": "all",
448
+ "containerworkdir": "/app"
449
+ },
450
+ "queue": {
451
+ "enabled": true,
452
+ "slots": 4
453
+ },
454
+ "remote_path": "/home/myuser/projects/my-app",
455
+ "profiles": {
456
+ "quick": {
457
+ "queue": { "slots": 1 },
458
+ "docker": {
459
+ "gpus": "1"
460
+ }
461
+ }
462
+ }
463
+ }
464
+ ```
465
+
466
+ ## How It Works
467
+
468
+ 1. **Build** (`install`): Optionally syncs local code via rsync, then runs `docker build` on the host. Pass `-v` to stream build output; default shows a spinner only. Only needed when dependencies or the Dockerfile change.
469
+ 2. **Submit** (`submit`/`run`): Optionally syncs code to the remote, then starts a `docker run`. With the queue enabled it's submitted to task spooler (`tsp`); with the queue disabled it runs immediately as a detached container. How code reaches the container depends on [`code_delivery`](#code-delivery-mount-vs-bake): in `mount` mode the project directory is bind-mounted at `containerworkdir` (no rebuild; `preserve_paths` protects image-built artifacts like a `.venv` from being shadowed by the mount); in `bake` mode the code is built into an image — content-addressed and immutable for queued jobs — and run with no code mount.
470
+ 3. **Queue** (when enabled): tsp runs jobs one at a time in submission order. Jobs with `--slots N` only start when N free slots are available. With the queue disabled, jobs start on submit and this ordering doesn't apply.
471
+ 4. **Job IDs**: Each submission gets a local job ID (stable, auto-incrementing) stored alongside the host and the transport handle (tsp job number, or container name). The local ID is what all commands accept.
472
+ 5. **Logs** (`logs`): Reads from the tsp output file (queued jobs) or via `docker logs` (direct runs). Use `--follow`/`-f` to stream a running job live, `--n N` for the last N lines.
473
+ 6. **Port Forwarding** (`tunnel`): Establishes SSH local port forwards for container ports.
474
+ 7. **Download**: Maps containerworkdir-relative paths to host paths and uses rsync to fetch files.
475
+
476
+ ## License
477
+
478
+ MIT
@@ -0,0 +1,25 @@
1
+ dockhand/__init__.py,sha256=TYC9DcY9lbbHrdwO8tfpzGN1GVlD9Zz4EenrmgbbZY0,11961
2
+ dockhand/build.py,sha256=Qn8dgiVV6gnyDvrR2XcXgzQLjSED1wdNR0BjTVB5OQU,1270
3
+ dockhand/config.py,sha256=x-w2qNm_qmULYImm3g3_2_CXTbUEvIGfCbEivVdzfsc,12152
4
+ dockhand/constants.py,sha256=UhZG5-BQxYlVEKrT4SeZz58jDTCnswcBpdQ5DdmUmwg,79
5
+ dockhand/download.py,sha256=xqdPd5BMHK35fEwycHknhWohuEjSXVxw8cYYl2MtWrg,2097
6
+ dockhand/error.py,sha256=KC42dv4PkZ6_RwfZCgqkStd3IimJHiUXh0Xfsf6Bwhw,268
7
+ dockhand/history.py,sha256=u5jCIIHZTmEFB7Lw8nc2DbO9fpL8N5xbo_ELXMTHhq0,5442
8
+ dockhand/manage.py,sha256=9M3908uKQULpE8xrggjBbUkBPatsZ7YKLuK3VuZKPb8,18805
9
+ dockhand/queue.py,sha256=bdLy5pDsvpE3XKpxTzRcI8NHadrrWE0tBGjn1zqz1zg,6958
10
+ dockhand/resubmit.py,sha256=lR669pdCw0U-9sXhwaMqiG67ZT2aTqRgjiZdszxI3IU,2101
11
+ dockhand/submit.py,sha256=9YXdrEvzlhECkJN0P017y8TCWbS0Hh0XnzdsBhUfZow,5961
12
+ dockhand/sync.py,sha256=eFiGISfsS8k9BduC7CrIuWk3Nl1fsgeT5RiUiafFqk0,1602
13
+ dockhand/tagging.py,sha256=ra7uCMpS5FkTqdpcuwGU2gmPhwhtp8wEGeWx_yZR07M,1766
14
+ dockhand/transport.py,sha256=lFhwUnG4stSKeKPmUQc6dMilLlbi4uB0j0lHgqqk9q0,8205
15
+ dockhand/tunnel.py,sha256=TfAxsJyLKZAcAaWTeNb-NEi0zs_XKb7nAAHI3NJDOQI,2197
16
+ dockhand/volumes.py,sha256=HOSpadtdHJtSvrnoMoYQs_IbH91cRsbttWl3Mo8xLK0,6107
17
+ dockhand/client/__init__.py,sha256=rV_HIm6Rn5OKhC1getUcPayUvIP-GViMfEU2mqDkcNc,1252
18
+ dockhand/client/base.py,sha256=qq15iiJSpdVL3LEH2sWvoB4ckLI2iIwEXkIlylTDoeo,791
19
+ dockhand/client/local.py,sha256=545a6wwmd4wxhuaHY-K9xax5yk4J10cYUhZjH3m1Bs8,1239
20
+ dockhand/client/ssh.py,sha256=VT6VxSQAnBEO6QPDJFUc-zhDpdeAhEJTP8iy2ybferQ,1653
21
+ dockhand_cli-0.3.1.dist-info/METADATA,sha256=tYCEhakHA0UWZKEsF5wM3A2MATFAYA04Yi8Nio0BDrE,17133
22
+ dockhand_cli-0.3.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
23
+ dockhand_cli-0.3.1.dist-info/entry_points.txt,sha256=DXUII-tAUFd-fHvPXQCr1VMdzf9L44JjhoY4eCc1Wm8,42
24
+ dockhand_cli-0.3.1.dist-info/licenses/LICENSE,sha256=llmhN1yXJp25KpDwt4YycB4hsTUTF1uDT8ZtF39CfZM,1063
25
+ dockhand_cli-0.3.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dockhand = dockhand:cli
@@ -0,0 +1,7 @@
1
+ Copyright 2026 Nicholas Hansen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.