petfit-docker 0.1.4__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.
- petfit_docker-0.1.4/PKG-INFO +126 -0
- petfit_docker-0.1.4/README.md +112 -0
- petfit_docker-0.1.4/petfit_docker/__init__.py +5 -0
- petfit_docker-0.1.4/petfit_docker/__main__.py +7 -0
- petfit_docker-0.1.4/petfit_docker/_version.py +3 -0
- petfit_docker-0.1.4/petfit_docker/cli.py +362 -0
- petfit_docker-0.1.4/petfit_docker.egg-info/PKG-INFO +126 -0
- petfit_docker-0.1.4/petfit_docker.egg-info/SOURCES.txt +13 -0
- petfit_docker-0.1.4/petfit_docker.egg-info/dependency_links.txt +1 -0
- petfit_docker-0.1.4/petfit_docker.egg-info/entry_points.txt +2 -0
- petfit_docker-0.1.4/petfit_docker.egg-info/top_level.txt +1 -0
- petfit_docker-0.1.4/pyproject.toml +30 -0
- petfit_docker-0.1.4/setup.cfg +4 -0
- petfit_docker-0.1.4/setup.py +6 -0
- petfit_docker-0.1.4/tests/test_cli.py +343 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: petfit-docker
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: A wrapper for generating Docker commands using regular PETFit syntax
|
|
5
|
+
Author: The PETFit developers
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# petfit-docker
|
|
16
|
+
|
|
17
|
+
`petfit-docker` is a lightweight Python wrapper that turns a BIDS-App-like
|
|
18
|
+
command line into the matching `docker run` invocation for PETFit.
|
|
19
|
+
Interactive Shiny mode is the default; use `--automatic` or
|
|
20
|
+
`--mode automatic` to run a non-interactive pipeline.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
24
|
+
--app modelling_plasma \
|
|
25
|
+
--blood-dir /path/to/blood \
|
|
26
|
+
--analysis-foldername Primary_Analysis
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The command above runs:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker run --rm -it \
|
|
33
|
+
-p 3838:3838 \
|
|
34
|
+
-v /path/to/bids:/data/bids_dir:ro \
|
|
35
|
+
-v /path/to/derivatives:/data/derivatives_dir:rw \
|
|
36
|
+
-v /path/to/blood:/data/blood_dir:ro \
|
|
37
|
+
mathesong/petfit:latest \
|
|
38
|
+
--func modelling_plasma --mode interactive
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install for development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd wrapper
|
|
45
|
+
python -m pip install -e .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
Launch the default region definition app:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
petfit-docker /path/to/bids /path/to/derivatives/petfit participant
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The three positional arguments follow the BIDS App convention:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
petfit-docker <bids_dir> <output_dir> participant
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Launch region definition:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
petfit-docker /path/to/bids /path/to/derivatives participant --app regiondef
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The positional `output_dir` can be either the derivatives root or the final
|
|
69
|
+
PETFit output directory. These are equivalent with the default output folder
|
|
70
|
+
name:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
petfit-docker /path/to/bids /path/to/derivatives/petfit participant
|
|
74
|
+
petfit-docker /path/to/bids /path/to/derivatives participant --app regiondef
|
|
75
|
+
petfit-docker /path/to/bids /path/to/derivatives/petfit participant --app regiondef
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Launch plasma-input modelling:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
82
|
+
--app modelling_plasma \
|
|
83
|
+
--blood-dir /path/to/blood
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Run plasma-input modelling automatically:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
90
|
+
--app modelling_plasma \
|
|
91
|
+
--blood-dir /path/to/blood \
|
|
92
|
+
--automatic
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Open a shell in the image:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
petfit-docker --shell -i mathesong/petfit:latest
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Patching a local petfit
|
|
102
|
+
|
|
103
|
+
Use `--patch` (or `-f`) to point the wrapper at a local petfit checkout and test
|
|
104
|
+
your local changes without rebuilding the image. The wrapper bind-mounts the
|
|
105
|
+
source into the container, where it is reinstalled from source at startup so it
|
|
106
|
+
overrides the petfit baked into the image:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
110
|
+
--app modelling_ref \
|
|
111
|
+
--patch /path/to/your/petfit/checkout
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This mirrors the `--patch` option of the PETPrep Docker wrapper. Because petfit
|
|
115
|
+
is an R package it is reinstalled (not run directly from source), so the first
|
|
116
|
+
few seconds of startup are spent installing the patched package. The patch
|
|
117
|
+
works with every mode, including `--shell`.
|
|
118
|
+
|
|
119
|
+
## Apple Silicon
|
|
120
|
+
|
|
121
|
+
The published PETFit Docker images are currently `linux/amd64` only. The
|
|
122
|
+
wrapper therefore requests `--platform linux/amd64` by default, which avoids
|
|
123
|
+
Docker's platform-mismatch warning on Apple Silicon while running under
|
|
124
|
+
emulation. If a native or multi-architecture image is published later, override
|
|
125
|
+
the platform with `--platform linux/arm64` or disable the explicit platform with
|
|
126
|
+
`--platform ""`.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# petfit-docker
|
|
2
|
+
|
|
3
|
+
`petfit-docker` is a lightweight Python wrapper that turns a BIDS-App-like
|
|
4
|
+
command line into the matching `docker run` invocation for PETFit.
|
|
5
|
+
Interactive Shiny mode is the default; use `--automatic` or
|
|
6
|
+
`--mode automatic` to run a non-interactive pipeline.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
10
|
+
--app modelling_plasma \
|
|
11
|
+
--blood-dir /path/to/blood \
|
|
12
|
+
--analysis-foldername Primary_Analysis
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
The command above runs:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
docker run --rm -it \
|
|
19
|
+
-p 3838:3838 \
|
|
20
|
+
-v /path/to/bids:/data/bids_dir:ro \
|
|
21
|
+
-v /path/to/derivatives:/data/derivatives_dir:rw \
|
|
22
|
+
-v /path/to/blood:/data/blood_dir:ro \
|
|
23
|
+
mathesong/petfit:latest \
|
|
24
|
+
--func modelling_plasma --mode interactive
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Install for development
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
cd wrapper
|
|
31
|
+
python -m pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
Launch the default region definition app:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
petfit-docker /path/to/bids /path/to/derivatives/petfit participant
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The three positional arguments follow the BIDS App convention:
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
petfit-docker <bids_dir> <output_dir> participant
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Launch region definition:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
petfit-docker /path/to/bids /path/to/derivatives participant --app regiondef
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The positional `output_dir` can be either the derivatives root or the final
|
|
55
|
+
PETFit output directory. These are equivalent with the default output folder
|
|
56
|
+
name:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
petfit-docker /path/to/bids /path/to/derivatives/petfit participant
|
|
60
|
+
petfit-docker /path/to/bids /path/to/derivatives participant --app regiondef
|
|
61
|
+
petfit-docker /path/to/bids /path/to/derivatives/petfit participant --app regiondef
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Launch plasma-input modelling:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
68
|
+
--app modelling_plasma \
|
|
69
|
+
--blood-dir /path/to/blood
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Run plasma-input modelling automatically:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
76
|
+
--app modelling_plasma \
|
|
77
|
+
--blood-dir /path/to/blood \
|
|
78
|
+
--automatic
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Open a shell in the image:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
petfit-docker --shell -i mathesong/petfit:latest
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Patching a local petfit
|
|
88
|
+
|
|
89
|
+
Use `--patch` (or `-f`) to point the wrapper at a local petfit checkout and test
|
|
90
|
+
your local changes without rebuilding the image. The wrapper bind-mounts the
|
|
91
|
+
source into the container, where it is reinstalled from source at startup so it
|
|
92
|
+
overrides the petfit baked into the image:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
96
|
+
--app modelling_ref \
|
|
97
|
+
--patch /path/to/your/petfit/checkout
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
This mirrors the `--patch` option of the PETPrep Docker wrapper. Because petfit
|
|
101
|
+
is an R package it is reinstalled (not run directly from source), so the first
|
|
102
|
+
few seconds of startup are spent installing the patched package. The patch
|
|
103
|
+
works with every mode, including `--shell`.
|
|
104
|
+
|
|
105
|
+
## Apple Silicon
|
|
106
|
+
|
|
107
|
+
The published PETFit Docker images are currently `linux/amd64` only. The
|
|
108
|
+
wrapper therefore requests `--platform linux/amd64` by default, which avoids
|
|
109
|
+
Docker's platform-mismatch warning on Apple Silicon while running under
|
|
110
|
+
emulation. If a native or multi-architecture image is published later, override
|
|
111
|
+
the platform with `--platform linux/arm64` or disable the explicit platform with
|
|
112
|
+
`--platform ""`.
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Command line wrapper for running PETFit in Docker."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
from ._version import __version__
|
|
13
|
+
|
|
14
|
+
DEFAULT_IMAGE = "mathesong/petfit:latest"
|
|
15
|
+
DEFAULT_PLATFORM = "linux/amd64"
|
|
16
|
+
DEFAULT_PORT = 3838
|
|
17
|
+
APPS = ("regiondef", "modelling_plasma", "modelling_ref")
|
|
18
|
+
MODES = ("automatic", "interactive")
|
|
19
|
+
STEPS = ("datadef", "weights", "delay", "reference_tac", "model1", "model2", "model3")
|
|
20
|
+
MISSING_IMAGE = "Image '{}' is missing\nWould you like to download? [Y/n] "
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PETFitHelpFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter):
|
|
24
|
+
"""Preserve paragraphs while still showing defaults."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PathAction(argparse.Action):
|
|
28
|
+
"""Expand user paths while preserving argparse's standard display."""
|
|
29
|
+
|
|
30
|
+
def __call__(self, parser, namespace, values, option_string=None):
|
|
31
|
+
if values is None:
|
|
32
|
+
setattr(namespace, self.dest, None)
|
|
33
|
+
return
|
|
34
|
+
if isinstance(values, list):
|
|
35
|
+
setattr(namespace, self.dest, [str(Path(value).expanduser()) for value in values])
|
|
36
|
+
else:
|
|
37
|
+
setattr(namespace, self.dest, str(Path(values).expanduser()))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _parser() -> argparse.ArgumentParser:
|
|
41
|
+
parser = argparse.ArgumentParser(
|
|
42
|
+
prog="petfit-docker",
|
|
43
|
+
formatter_class=PETFitHelpFormatter,
|
|
44
|
+
description=(
|
|
45
|
+
"The PETFit on Docker wrapper\n\n"
|
|
46
|
+
"This is a lightweight Python wrapper to run PETFit. Docker must be "
|
|
47
|
+
"installed and running. This can be checked running::\n\n"
|
|
48
|
+
" docker info\n\n"
|
|
49
|
+
"The wrapper accepts a BIDS-App-like command line and translates host "
|
|
50
|
+
"paths into Docker bind mounts before executing the PETFit image."
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
parser.add_argument("bids_dir", nargs="?", action=PathAction)
|
|
55
|
+
parser.add_argument("output_dir", nargs="?", action=PathAction)
|
|
56
|
+
parser.add_argument("analysis_level", nargs="?", choices=("participant",), default="participant")
|
|
57
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
58
|
+
parser.add_argument("-i", "--image", default=DEFAULT_IMAGE, help="image name")
|
|
59
|
+
|
|
60
|
+
wrapper = parser.add_argument_group(
|
|
61
|
+
"Wrapper options",
|
|
62
|
+
"Standard options that require mapping files into the container; see petfit usage for complete descriptions",
|
|
63
|
+
)
|
|
64
|
+
wrapper.add_argument(
|
|
65
|
+
"--app",
|
|
66
|
+
choices=APPS,
|
|
67
|
+
default="regiondef",
|
|
68
|
+
help=(
|
|
69
|
+
"PETFit app to run: "
|
|
70
|
+
"'regiondef' = define brain regions and build combined TACs; "
|
|
71
|
+
"'modelling_plasma' = invasive plasma-input models "
|
|
72
|
+
"(1TCM, 2TCM, 2TCM_irr, Logan, MA1, Patlak), requires blood data; "
|
|
73
|
+
"'modelling_ref' = non-invasive reference-tissue models "
|
|
74
|
+
"(SRTM, SRTM2, refLogan, MRTM1, MRTM2)"
|
|
75
|
+
),
|
|
76
|
+
)
|
|
77
|
+
wrapper.add_argument(
|
|
78
|
+
"--mode",
|
|
79
|
+
choices=MODES,
|
|
80
|
+
default="interactive",
|
|
81
|
+
help="execution mode; or use the --interactive / --automatic shorthands below",
|
|
82
|
+
)
|
|
83
|
+
wrapper.add_argument(
|
|
84
|
+
"--interactive",
|
|
85
|
+
dest="mode",
|
|
86
|
+
action="store_const",
|
|
87
|
+
const="interactive",
|
|
88
|
+
default=argparse.SUPPRESS,
|
|
89
|
+
help="shorthand for --mode interactive (launch the Shiny app in the browser)",
|
|
90
|
+
)
|
|
91
|
+
wrapper.add_argument(
|
|
92
|
+
"--automatic",
|
|
93
|
+
dest="mode",
|
|
94
|
+
action="store_const",
|
|
95
|
+
const="automatic",
|
|
96
|
+
default=argparse.SUPPRESS,
|
|
97
|
+
help="shorthand for --mode automatic (run the non-interactive pipeline)",
|
|
98
|
+
)
|
|
99
|
+
wrapper.add_argument("--step", choices=STEPS, help="single automatic modelling step to run")
|
|
100
|
+
wrapper.add_argument("--blood-dir", action=PathAction, help="blood data directory for plasma input models")
|
|
101
|
+
wrapper.add_argument("-w", "--work-dir", action=PathAction, help="working directory to mount in the container")
|
|
102
|
+
wrapper.add_argument("--petfit-output-foldername", default="petfit", help="petfit output folder within derivatives")
|
|
103
|
+
wrapper.add_argument(
|
|
104
|
+
"--analysis-foldername",
|
|
105
|
+
default="Primary_Analysis",
|
|
106
|
+
help=(
|
|
107
|
+
"name of the analysis subfolder holding this run's config and outputs; "
|
|
108
|
+
"multiple can sit side by side, e.g. 'Reference_Analysis', "
|
|
109
|
+
"'Baseline_Only'"
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
wrapper.add_argument("--cores", type=int, default=1, help="number of cores for parallel processing")
|
|
113
|
+
wrapper.add_argument(
|
|
114
|
+
"--ancillary-analysis-folder",
|
|
115
|
+
help="sibling analysis folder to inherit delay or k2prime estimates from",
|
|
116
|
+
)
|
|
117
|
+
wrapper.add_argument("--port", type=int, default=DEFAULT_PORT, help="host and container port for interactive Shiny apps")
|
|
118
|
+
|
|
119
|
+
developer = parser.add_argument_group("Developer options", "Tools for testing and debugging PETFit")
|
|
120
|
+
developer.add_argument("--shell", action="store_true", help="open shell in image instead of running PETFit")
|
|
121
|
+
developer.add_argument(
|
|
122
|
+
"-f",
|
|
123
|
+
"--patch",
|
|
124
|
+
action=PathAction,
|
|
125
|
+
help="local petfit checkout to install over the image's petfit at container "
|
|
126
|
+
"start (for testing local changes without rebuilding the image)",
|
|
127
|
+
)
|
|
128
|
+
developer.add_argument(
|
|
129
|
+
"-e",
|
|
130
|
+
"--env",
|
|
131
|
+
nargs=2,
|
|
132
|
+
action="append",
|
|
133
|
+
metavar=("ENV_VAR", "value"),
|
|
134
|
+
help="set custom environment variables within container",
|
|
135
|
+
)
|
|
136
|
+
developer.add_argument(
|
|
137
|
+
"-u",
|
|
138
|
+
"--user",
|
|
139
|
+
help="run container as a given user/uid. A group/gid can also be assigned, i.e. --user <UID>:<GID>",
|
|
140
|
+
)
|
|
141
|
+
developer.add_argument("--network", help='run container with a different network driver, e.g. "none"')
|
|
142
|
+
developer.add_argument(
|
|
143
|
+
"--platform",
|
|
144
|
+
default=DEFAULT_PLATFORM,
|
|
145
|
+
help="run Docker with a specific platform; PETFit images are currently published for linux/amd64",
|
|
146
|
+
)
|
|
147
|
+
developer.add_argument("--no-tty", action="store_true", help="run docker without TTY flag -it")
|
|
148
|
+
developer.add_argument("--dry-run", action="store_true", help="print the Docker command without executing it")
|
|
149
|
+
developer.add_argument(
|
|
150
|
+
"--skip-image-check",
|
|
151
|
+
action="store_true",
|
|
152
|
+
help="do not check whether the Docker image exists before running",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return parser
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _absolute_path(path: Optional[str], *, create: bool = False) -> Optional[str]:
|
|
159
|
+
if path is None:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
resolved = Path(path).expanduser().absolute()
|
|
163
|
+
if create:
|
|
164
|
+
resolved.mkdir(parents=True, exist_ok=True)
|
|
165
|
+
return str(resolved)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _derivatives_mount_from_output(output_dir: str, petfit_output_foldername: str) -> str:
|
|
169
|
+
"""Map BIDS-App-like output_dir to PETFit's derivatives mount point.
|
|
170
|
+
|
|
171
|
+
PETFit's container entry point expects the derivatives root and then appends
|
|
172
|
+
``petfit_output_foldername`` internally. If the wrapper user passes the final
|
|
173
|
+
PETFit output directory, such as ``derivatives/petfit``, mount its parent so
|
|
174
|
+
the container does not look for ``petfit/petfit``.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
output_path = Path(output_dir)
|
|
178
|
+
if output_path.name == petfit_output_foldername:
|
|
179
|
+
return str(output_path.parent)
|
|
180
|
+
return output_dir
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _mount_argument(host_path: str, container_path: str, mode: str) -> str:
|
|
184
|
+
return f"{host_path}:{container_path}:{mode}"
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def build_docker_command(opts: argparse.Namespace) -> List[str]:
|
|
188
|
+
"""Build the Docker command corresponding to parsed options."""
|
|
189
|
+
|
|
190
|
+
if opts.mode == "interactive" and (opts.port < 1 or opts.port > 65535):
|
|
191
|
+
raise SystemExit("--port must be between 1 and 65535")
|
|
192
|
+
if opts.cores < 1:
|
|
193
|
+
raise SystemExit("--cores must be at least 1")
|
|
194
|
+
if opts.app == "regiondef" and opts.step:
|
|
195
|
+
raise SystemExit("--step is only valid for modelling_plasma and modelling_ref")
|
|
196
|
+
|
|
197
|
+
bids_dir = _absolute_path(opts.bids_dir) if opts.bids_dir else None
|
|
198
|
+
output_dir = _absolute_path(opts.output_dir, create=not opts.shell) if opts.output_dir else None
|
|
199
|
+
derivatives_dir = (
|
|
200
|
+
_derivatives_mount_from_output(output_dir, opts.petfit_output_foldername)
|
|
201
|
+
if output_dir
|
|
202
|
+
else None
|
|
203
|
+
)
|
|
204
|
+
blood_dir = _absolute_path(opts.blood_dir) if opts.blood_dir else None
|
|
205
|
+
work_dir = _absolute_path(opts.work_dir, create=True) if opts.work_dir else None
|
|
206
|
+
|
|
207
|
+
if not opts.shell:
|
|
208
|
+
missing = []
|
|
209
|
+
if not bids_dir:
|
|
210
|
+
missing.append("bids_dir")
|
|
211
|
+
if not output_dir:
|
|
212
|
+
missing.append("output_dir")
|
|
213
|
+
if missing:
|
|
214
|
+
raise SystemExit("the following arguments are required unless --shell is used: " + ", ".join(missing))
|
|
215
|
+
|
|
216
|
+
command = ["docker", "run", "--rm"]
|
|
217
|
+
if opts.platform:
|
|
218
|
+
command.extend(["--platform", opts.platform])
|
|
219
|
+
if not opts.no_tty:
|
|
220
|
+
command.append("-it")
|
|
221
|
+
|
|
222
|
+
if opts.user:
|
|
223
|
+
command.extend(["--user", opts.user])
|
|
224
|
+
if opts.network:
|
|
225
|
+
command.extend(["--network", opts.network])
|
|
226
|
+
|
|
227
|
+
env = list(opts.env or [])
|
|
228
|
+
if opts.mode == "interactive":
|
|
229
|
+
env.append(("PETFIT_SHINY_PORT", str(opts.port)))
|
|
230
|
+
env.append(("SHINY_SERVER_VERSION", ""))
|
|
231
|
+
command.extend(["-p", f"{opts.port}:{opts.port}"])
|
|
232
|
+
|
|
233
|
+
for key, value in env:
|
|
234
|
+
command.extend(["-e", f"{key}={value}"])
|
|
235
|
+
|
|
236
|
+
if bids_dir:
|
|
237
|
+
command.extend(["-v", _mount_argument(bids_dir, "/data/bids_dir", "ro")])
|
|
238
|
+
if derivatives_dir:
|
|
239
|
+
command.extend(["-v", _mount_argument(derivatives_dir, "/data/derivatives_dir", "rw")])
|
|
240
|
+
if blood_dir:
|
|
241
|
+
command.extend(["-v", _mount_argument(blood_dir, "/data/blood_dir", "ro")])
|
|
242
|
+
if work_dir:
|
|
243
|
+
command.extend(["-v", _mount_argument(work_dir, "/data/work_dir", "rw")])
|
|
244
|
+
|
|
245
|
+
patch_dir = _absolute_path(opts.patch) if opts.patch else None
|
|
246
|
+
if patch_dir:
|
|
247
|
+
command.extend(["-v", _mount_argument(patch_dir, "/patch/petfit", "ro")])
|
|
248
|
+
|
|
249
|
+
if opts.shell:
|
|
250
|
+
command.extend(["--entrypoint", "/bin/bash", opts.image])
|
|
251
|
+
return command
|
|
252
|
+
|
|
253
|
+
command.append(opts.image)
|
|
254
|
+
command.extend(
|
|
255
|
+
[
|
|
256
|
+
"--func",
|
|
257
|
+
opts.app,
|
|
258
|
+
"--mode",
|
|
259
|
+
opts.mode,
|
|
260
|
+
"--petfit_output_foldername",
|
|
261
|
+
opts.petfit_output_foldername,
|
|
262
|
+
"--analysis_foldername",
|
|
263
|
+
opts.analysis_foldername,
|
|
264
|
+
"--cores",
|
|
265
|
+
str(opts.cores),
|
|
266
|
+
]
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if opts.step:
|
|
270
|
+
command.extend(["--step", opts.step])
|
|
271
|
+
if opts.ancillary_analysis_folder:
|
|
272
|
+
command.extend(["--ancillary_analysis_folder", opts.ancillary_analysis_folder])
|
|
273
|
+
|
|
274
|
+
return command
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def check_docker() -> None:
|
|
278
|
+
"""Fail early if Docker is unavailable, preserving Docker's own message."""
|
|
279
|
+
|
|
280
|
+
try:
|
|
281
|
+
subprocess.run(["docker", "info"], capture_output=True, text=True, check=True)
|
|
282
|
+
except OSError as error:
|
|
283
|
+
raise SystemExit(f"Could not run Docker: {error}") from error
|
|
284
|
+
except subprocess.CalledProcessError as error:
|
|
285
|
+
docker_output = (error.stderr or error.stdout or "").strip()
|
|
286
|
+
if docker_output:
|
|
287
|
+
print(docker_output, file=sys.stderr)
|
|
288
|
+
print("Could not detect memory capacity of Docker container.", file=sys.stderr)
|
|
289
|
+
raise SystemExit("Do you have permission to run docker?") from error
|
|
290
|
+
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def image_exists(image: str) -> bool:
|
|
295
|
+
"""Return whether a Docker image is available locally."""
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
result = subprocess.run(["docker", "images", "-q", image], stdout=subprocess.PIPE)
|
|
299
|
+
except OSError:
|
|
300
|
+
return False
|
|
301
|
+
return bool(result.stdout)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def check_memory(image: str, platform: Optional[str] = DEFAULT_PLATFORM) -> int:
|
|
305
|
+
"""Return available container memory in MB, or -1 if Docker cannot report it."""
|
|
306
|
+
|
|
307
|
+
command = ["docker", "run", "--rm"]
|
|
308
|
+
if platform:
|
|
309
|
+
command.extend(["--platform", platform])
|
|
310
|
+
command.extend(["--entrypoint=free", image, "-m"])
|
|
311
|
+
|
|
312
|
+
try:
|
|
313
|
+
result = subprocess.run(command, stdout=subprocess.PIPE)
|
|
314
|
+
except OSError:
|
|
315
|
+
return -1
|
|
316
|
+
|
|
317
|
+
if result.returncode:
|
|
318
|
+
return -1
|
|
319
|
+
|
|
320
|
+
for line in result.stdout.splitlines():
|
|
321
|
+
if line.startswith(b"Mem:"):
|
|
322
|
+
return int(line.decode().split()[1])
|
|
323
|
+
return -1
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def maybe_pull_image(image: str) -> None:
|
|
327
|
+
"""Offer to download a missing image before Docker pulls via ``docker run``."""
|
|
328
|
+
|
|
329
|
+
if image_exists(image):
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
answer = input(MISSING_IMAGE.format(image))
|
|
333
|
+
if answer.strip().lower() not in ("", "y", "yes"):
|
|
334
|
+
raise SystemExit(f"Image '{image}' is required")
|
|
335
|
+
|
|
336
|
+
print("Downloading. This may take a while...", flush=True)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def ensure_image_ready(image: str, platform: Optional[str] = DEFAULT_PLATFORM) -> None:
|
|
340
|
+
"""Confirm image availability and that Docker can run a tiny memory probe."""
|
|
341
|
+
|
|
342
|
+
maybe_pull_image(image)
|
|
343
|
+
if check_memory(image, platform=platform) == -1:
|
|
344
|
+
print("Could not detect memory capacity of Docker container.", file=sys.stderr)
|
|
345
|
+
raise SystemExit("Do you have permission to run docker?")
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
349
|
+
parser = _parser()
|
|
350
|
+
opts = parser.parse_args(argv)
|
|
351
|
+
|
|
352
|
+
if not opts.dry_run:
|
|
353
|
+
check_docker()
|
|
354
|
+
if not opts.skip_image_check:
|
|
355
|
+
ensure_image_ready(opts.image, platform=opts.platform)
|
|
356
|
+
|
|
357
|
+
command = build_docker_command(opts)
|
|
358
|
+
print("RUNNING: " + shlex.join(command))
|
|
359
|
+
|
|
360
|
+
if opts.dry_run:
|
|
361
|
+
return 0
|
|
362
|
+
return subprocess.call(command)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: petfit-docker
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary: A wrapper for generating Docker commands using regular PETFit syntax
|
|
5
|
+
Author: The PETFit developers
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Intended Audience :: Science/Research
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
|
|
12
|
+
Requires-Python: >=3.8
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# petfit-docker
|
|
16
|
+
|
|
17
|
+
`petfit-docker` is a lightweight Python wrapper that turns a BIDS-App-like
|
|
18
|
+
command line into the matching `docker run` invocation for PETFit.
|
|
19
|
+
Interactive Shiny mode is the default; use `--automatic` or
|
|
20
|
+
`--mode automatic` to run a non-interactive pipeline.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
24
|
+
--app modelling_plasma \
|
|
25
|
+
--blood-dir /path/to/blood \
|
|
26
|
+
--analysis-foldername Primary_Analysis
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The command above runs:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
docker run --rm -it \
|
|
33
|
+
-p 3838:3838 \
|
|
34
|
+
-v /path/to/bids:/data/bids_dir:ro \
|
|
35
|
+
-v /path/to/derivatives:/data/derivatives_dir:rw \
|
|
36
|
+
-v /path/to/blood:/data/blood_dir:ro \
|
|
37
|
+
mathesong/petfit:latest \
|
|
38
|
+
--func modelling_plasma --mode interactive
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Install for development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
cd wrapper
|
|
45
|
+
python -m pip install -e .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
Launch the default region definition app:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
petfit-docker /path/to/bids /path/to/derivatives/petfit participant
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The three positional arguments follow the BIDS App convention:
|
|
57
|
+
|
|
58
|
+
```text
|
|
59
|
+
petfit-docker <bids_dir> <output_dir> participant
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Launch region definition:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
petfit-docker /path/to/bids /path/to/derivatives participant --app regiondef
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The positional `output_dir` can be either the derivatives root or the final
|
|
69
|
+
PETFit output directory. These are equivalent with the default output folder
|
|
70
|
+
name:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
petfit-docker /path/to/bids /path/to/derivatives/petfit participant
|
|
74
|
+
petfit-docker /path/to/bids /path/to/derivatives participant --app regiondef
|
|
75
|
+
petfit-docker /path/to/bids /path/to/derivatives/petfit participant --app regiondef
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Launch plasma-input modelling:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
82
|
+
--app modelling_plasma \
|
|
83
|
+
--blood-dir /path/to/blood
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Run plasma-input modelling automatically:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
90
|
+
--app modelling_plasma \
|
|
91
|
+
--blood-dir /path/to/blood \
|
|
92
|
+
--automatic
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Open a shell in the image:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
petfit-docker --shell -i mathesong/petfit:latest
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Patching a local petfit
|
|
102
|
+
|
|
103
|
+
Use `--patch` (or `-f`) to point the wrapper at a local petfit checkout and test
|
|
104
|
+
your local changes without rebuilding the image. The wrapper bind-mounts the
|
|
105
|
+
source into the container, where it is reinstalled from source at startup so it
|
|
106
|
+
overrides the petfit baked into the image:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
petfit-docker /path/to/bids /path/to/derivatives participant \
|
|
110
|
+
--app modelling_ref \
|
|
111
|
+
--patch /path/to/your/petfit/checkout
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
This mirrors the `--patch` option of the PETPrep Docker wrapper. Because petfit
|
|
115
|
+
is an R package it is reinstalled (not run directly from source), so the first
|
|
116
|
+
few seconds of startup are spent installing the patched package. The patch
|
|
117
|
+
works with every mode, including `--shell`.
|
|
118
|
+
|
|
119
|
+
## Apple Silicon
|
|
120
|
+
|
|
121
|
+
The published PETFit Docker images are currently `linux/amd64` only. The
|
|
122
|
+
wrapper therefore requests `--platform linux/amd64` by default, which avoids
|
|
123
|
+
Docker's platform-mismatch warning on Apple Silicon while running under
|
|
124
|
+
emulation. If a native or multi-architecture image is published later, override
|
|
125
|
+
the platform with `--platform linux/arm64` or disable the explicit platform with
|
|
126
|
+
`--platform ""`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
petfit_docker/__init__.py
|
|
5
|
+
petfit_docker/__main__.py
|
|
6
|
+
petfit_docker/_version.py
|
|
7
|
+
petfit_docker/cli.py
|
|
8
|
+
petfit_docker.egg-info/PKG-INFO
|
|
9
|
+
petfit_docker.egg-info/SOURCES.txt
|
|
10
|
+
petfit_docker.egg-info/dependency_links.txt
|
|
11
|
+
petfit_docker.egg-info/entry_points.txt
|
|
12
|
+
petfit_docker.egg-info/top_level.txt
|
|
13
|
+
tests/test_cli.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
petfit_docker
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "petfit-docker"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "A wrapper for generating Docker commands using regular PETFit syntax"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "The PETFit developers" }
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Science/Research",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Medical Science Apps."
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
petfit-docker = "petfit_docker.cli:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.dynamic]
|
|
27
|
+
version = { attr = "petfit_docker._version.__version__" }
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
include = ["petfit_docker*"]
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import sys
|
|
5
|
+
from io import StringIO
|
|
6
|
+
from unittest.mock import patch
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
10
|
+
|
|
11
|
+
from petfit_docker.cli import (
|
|
12
|
+
_parser,
|
|
13
|
+
build_docker_command,
|
|
14
|
+
check_docker,
|
|
15
|
+
check_memory,
|
|
16
|
+
ensure_image_ready,
|
|
17
|
+
main,
|
|
18
|
+
maybe_pull_image,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def abs_path(path):
|
|
23
|
+
return str(Path(path).expanduser().absolute())
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DockerCommandTests(unittest.TestCase):
|
|
27
|
+
def test_no_arguments_parse_without_path_traceback(self):
|
|
28
|
+
opts = _parser().parse_args([])
|
|
29
|
+
|
|
30
|
+
self.assertIsNone(opts.bids_dir)
|
|
31
|
+
self.assertIsNone(opts.output_dir)
|
|
32
|
+
|
|
33
|
+
def test_builds_bids_app_like_modelling_command(self):
|
|
34
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
35
|
+
root = Path(tmpdir)
|
|
36
|
+
bids = root / "bids"
|
|
37
|
+
derivatives = root / "derivatives"
|
|
38
|
+
blood = root / "blood"
|
|
39
|
+
bids.mkdir()
|
|
40
|
+
blood.mkdir()
|
|
41
|
+
|
|
42
|
+
opts = _parser().parse_args(
|
|
43
|
+
[
|
|
44
|
+
str(bids),
|
|
45
|
+
str(derivatives),
|
|
46
|
+
"participant",
|
|
47
|
+
"--app",
|
|
48
|
+
"modelling_plasma",
|
|
49
|
+
"--blood-dir",
|
|
50
|
+
str(blood),
|
|
51
|
+
"--step",
|
|
52
|
+
"weights",
|
|
53
|
+
"--automatic",
|
|
54
|
+
"--no-tty",
|
|
55
|
+
"--dry-run",
|
|
56
|
+
]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
command = build_docker_command(opts)
|
|
60
|
+
|
|
61
|
+
self.assertIn("docker", command)
|
|
62
|
+
self.assertIn("--func", command)
|
|
63
|
+
self.assertIn("modelling_plasma", command)
|
|
64
|
+
self.assertIn("--platform", command)
|
|
65
|
+
self.assertIn("linux/amd64", command)
|
|
66
|
+
self.assertIn("--step", command)
|
|
67
|
+
self.assertIn("weights", command)
|
|
68
|
+
self.assertIn(f"{abs_path(bids)}:/data/bids_dir:ro", command)
|
|
69
|
+
self.assertIn(f"{abs_path(derivatives)}:/data/derivatives_dir:rw", command)
|
|
70
|
+
self.assertIn(f"{abs_path(blood)}:/data/blood_dir:ro", command)
|
|
71
|
+
self.assertTrue(derivatives.exists())
|
|
72
|
+
|
|
73
|
+
def test_interactive_mode_is_default(self):
|
|
74
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
75
|
+
root = Path(tmpdir)
|
|
76
|
+
bids = root / "bids"
|
|
77
|
+
derivatives = root / "derivatives"
|
|
78
|
+
bids.mkdir()
|
|
79
|
+
|
|
80
|
+
opts = _parser().parse_args(
|
|
81
|
+
[
|
|
82
|
+
str(bids),
|
|
83
|
+
str(derivatives),
|
|
84
|
+
"participant",
|
|
85
|
+
"--app",
|
|
86
|
+
"regiondef",
|
|
87
|
+
"--no-tty",
|
|
88
|
+
"--dry-run",
|
|
89
|
+
]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
command = build_docker_command(opts)
|
|
93
|
+
|
|
94
|
+
mode_index = command.index("--mode")
|
|
95
|
+
self.assertEqual(command[mode_index + 1], "interactive")
|
|
96
|
+
self.assertIn("PETFIT_SHINY_PORT=3838", command)
|
|
97
|
+
self.assertIn("SHINY_SERVER_VERSION=", command)
|
|
98
|
+
|
|
99
|
+
def test_output_dir_can_be_final_petfit_directory(self):
|
|
100
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
101
|
+
root = Path(tmpdir)
|
|
102
|
+
bids = root / "bids"
|
|
103
|
+
derivatives = root / "derivatives"
|
|
104
|
+
petfit_output = derivatives / "petfit"
|
|
105
|
+
bids.mkdir()
|
|
106
|
+
|
|
107
|
+
opts = _parser().parse_args(
|
|
108
|
+
[
|
|
109
|
+
str(bids),
|
|
110
|
+
str(petfit_output),
|
|
111
|
+
"participant",
|
|
112
|
+
"--app",
|
|
113
|
+
"regiondef",
|
|
114
|
+
"--no-tty",
|
|
115
|
+
"--dry-run",
|
|
116
|
+
]
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
command = build_docker_command(opts)
|
|
120
|
+
|
|
121
|
+
self.assertIn(f"{abs_path(derivatives)}:/data/derivatives_dir:rw", command)
|
|
122
|
+
self.assertNotIn(f"{abs_path(petfit_output)}:/data/derivatives_dir:rw", command)
|
|
123
|
+
self.assertTrue(petfit_output.exists())
|
|
124
|
+
|
|
125
|
+
def test_interactive_mode_maps_port_and_env(self):
|
|
126
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
127
|
+
root = Path(tmpdir)
|
|
128
|
+
bids = root / "bids"
|
|
129
|
+
derivatives = root / "derivatives"
|
|
130
|
+
bids.mkdir()
|
|
131
|
+
|
|
132
|
+
opts = _parser().parse_args(
|
|
133
|
+
[
|
|
134
|
+
str(bids),
|
|
135
|
+
str(derivatives),
|
|
136
|
+
"participant",
|
|
137
|
+
"--mode",
|
|
138
|
+
"interactive",
|
|
139
|
+
"--port",
|
|
140
|
+
"3840",
|
|
141
|
+
"--no-tty",
|
|
142
|
+
"--dry-run",
|
|
143
|
+
]
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
command = build_docker_command(opts)
|
|
147
|
+
|
|
148
|
+
self.assertIn("-p", command)
|
|
149
|
+
self.assertIn("3840:3840", command)
|
|
150
|
+
self.assertIn("-e", command)
|
|
151
|
+
self.assertIn("PETFIT_SHINY_PORT=3840", command)
|
|
152
|
+
|
|
153
|
+
def test_patch_mounts_local_petfit(self):
|
|
154
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
155
|
+
root = Path(tmpdir)
|
|
156
|
+
bids = root / "bids"
|
|
157
|
+
derivatives = root / "derivatives"
|
|
158
|
+
patch = root / "petfit"
|
|
159
|
+
bids.mkdir()
|
|
160
|
+
patch.mkdir()
|
|
161
|
+
|
|
162
|
+
opts = _parser().parse_args(
|
|
163
|
+
[
|
|
164
|
+
str(bids),
|
|
165
|
+
str(derivatives),
|
|
166
|
+
"participant",
|
|
167
|
+
"--app",
|
|
168
|
+
"regiondef",
|
|
169
|
+
"--patch",
|
|
170
|
+
str(patch),
|
|
171
|
+
"--no-tty",
|
|
172
|
+
"--dry-run",
|
|
173
|
+
]
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
command = build_docker_command(opts)
|
|
177
|
+
|
|
178
|
+
self.assertIn(f"{abs_path(patch)}:/patch/petfit:ro", command)
|
|
179
|
+
|
|
180
|
+
def test_patch_works_with_shell(self):
|
|
181
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
182
|
+
patch = Path(tmpdir) / "petfit"
|
|
183
|
+
patch.mkdir()
|
|
184
|
+
|
|
185
|
+
opts = _parser().parse_args(
|
|
186
|
+
[
|
|
187
|
+
"--shell",
|
|
188
|
+
"--patch",
|
|
189
|
+
str(patch),
|
|
190
|
+
"--no-tty",
|
|
191
|
+
"--dry-run",
|
|
192
|
+
]
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
command = build_docker_command(opts)
|
|
196
|
+
|
|
197
|
+
self.assertIn(f"{abs_path(patch)}:/patch/petfit:ro", command)
|
|
198
|
+
|
|
199
|
+
def test_regiondef_rejects_step(self):
|
|
200
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
201
|
+
root = Path(tmpdir)
|
|
202
|
+
bids = root / "bids"
|
|
203
|
+
derivatives = root / "derivatives"
|
|
204
|
+
bids.mkdir()
|
|
205
|
+
|
|
206
|
+
opts = _parser().parse_args(
|
|
207
|
+
[
|
|
208
|
+
str(bids),
|
|
209
|
+
str(derivatives),
|
|
210
|
+
"participant",
|
|
211
|
+
"--app",
|
|
212
|
+
"regiondef",
|
|
213
|
+
"--step",
|
|
214
|
+
"weights",
|
|
215
|
+
]
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
with self.assertRaises(SystemExit):
|
|
219
|
+
build_docker_command(opts)
|
|
220
|
+
|
|
221
|
+
def test_check_docker_surfaces_daemon_error(self):
|
|
222
|
+
error = subprocess.CalledProcessError(
|
|
223
|
+
1,
|
|
224
|
+
["docker", "info"],
|
|
225
|
+
stderr="Cannot connect to the Docker daemon at unix:///tmp/docker.sock. Is the docker daemon running?\n",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
with patch("petfit_docker.cli.subprocess.run", side_effect=error):
|
|
229
|
+
with patch("sys.stderr", new_callable=StringIO) as stderr:
|
|
230
|
+
with self.assertRaises(SystemExit) as caught:
|
|
231
|
+
check_docker()
|
|
232
|
+
|
|
233
|
+
self.assertIn("Cannot connect to the Docker daemon", stderr.getvalue())
|
|
234
|
+
self.assertIn("Could not detect memory capacity", stderr.getvalue())
|
|
235
|
+
self.assertEqual(str(caught.exception), "Do you have permission to run docker?")
|
|
236
|
+
|
|
237
|
+
def test_main_checks_docker_and_image_before_required_paths(self):
|
|
238
|
+
with patch("petfit_docker.cli.check_docker") as docker_check:
|
|
239
|
+
with patch("petfit_docker.cli.ensure_image_ready") as ensure_image:
|
|
240
|
+
with self.assertRaises(SystemExit) as caught:
|
|
241
|
+
main([])
|
|
242
|
+
|
|
243
|
+
docker_check.assert_called_once()
|
|
244
|
+
ensure_image.assert_called_once_with("mathesong/petfit:latest", platform="linux/amd64")
|
|
245
|
+
self.assertIn("bids_dir", str(caught.exception))
|
|
246
|
+
|
|
247
|
+
def test_main_stops_on_docker_failure_before_image_check(self):
|
|
248
|
+
with patch("petfit_docker.cli.check_docker", side_effect=SystemExit("docker failed")):
|
|
249
|
+
with patch("petfit_docker.cli.ensure_image_ready") as ensure_image:
|
|
250
|
+
with self.assertRaises(SystemExit) as caught:
|
|
251
|
+
main([])
|
|
252
|
+
|
|
253
|
+
ensure_image.assert_not_called()
|
|
254
|
+
self.assertEqual(str(caught.exception), "docker failed")
|
|
255
|
+
|
|
256
|
+
def test_main_dry_run_skips_docker_and_image_checks(self):
|
|
257
|
+
with patch("petfit_docker.cli.check_docker") as docker_check:
|
|
258
|
+
with patch("petfit_docker.cli.ensure_image_ready") as ensure_image:
|
|
259
|
+
with self.assertRaises(SystemExit) as caught:
|
|
260
|
+
main(["--dry-run"])
|
|
261
|
+
|
|
262
|
+
docker_check.assert_not_called()
|
|
263
|
+
ensure_image.assert_not_called()
|
|
264
|
+
self.assertIn("bids_dir", str(caught.exception))
|
|
265
|
+
|
|
266
|
+
def test_main_skip_image_check_still_checks_docker(self):
|
|
267
|
+
with patch("petfit_docker.cli.check_docker") as docker_check:
|
|
268
|
+
with patch("petfit_docker.cli.ensure_image_ready") as ensure_image:
|
|
269
|
+
with self.assertRaises(SystemExit) as caught:
|
|
270
|
+
main(["--skip-image-check"])
|
|
271
|
+
|
|
272
|
+
docker_check.assert_called_once()
|
|
273
|
+
ensure_image.assert_not_called()
|
|
274
|
+
self.assertIn("bids_dir", str(caught.exception))
|
|
275
|
+
|
|
276
|
+
def test_missing_image_prompts_and_defers_pull_to_memory_probe(self):
|
|
277
|
+
with patch("petfit_docker.cli.image_exists", return_value=False):
|
|
278
|
+
with patch("builtins.input", return_value="y"):
|
|
279
|
+
with patch("sys.stdout", new_callable=StringIO) as stdout:
|
|
280
|
+
maybe_pull_image("mathesong/petfit:latest")
|
|
281
|
+
|
|
282
|
+
self.assertIn("Downloading. This may take a while...", stdout.getvalue())
|
|
283
|
+
|
|
284
|
+
def test_check_memory_reads_container_memory(self):
|
|
285
|
+
result = subprocess.CompletedProcess(
|
|
286
|
+
[
|
|
287
|
+
"docker",
|
|
288
|
+
"run",
|
|
289
|
+
"--rm",
|
|
290
|
+
"--platform",
|
|
291
|
+
"linux/amd64",
|
|
292
|
+
"--entrypoint=free",
|
|
293
|
+
"mathesong/petfit:latest",
|
|
294
|
+
"-m",
|
|
295
|
+
],
|
|
296
|
+
0,
|
|
297
|
+
stdout=b" total used free\nMem: 15999 1000 14999\n",
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
with patch("petfit_docker.cli.subprocess.run", return_value=result) as run:
|
|
301
|
+
self.assertEqual(check_memory("mathesong/petfit:latest"), 15999)
|
|
302
|
+
run.assert_called_once_with(
|
|
303
|
+
[
|
|
304
|
+
"docker",
|
|
305
|
+
"run",
|
|
306
|
+
"--rm",
|
|
307
|
+
"--platform",
|
|
308
|
+
"linux/amd64",
|
|
309
|
+
"--entrypoint=free",
|
|
310
|
+
"mathesong/petfit:latest",
|
|
311
|
+
"-m",
|
|
312
|
+
],
|
|
313
|
+
stdout=subprocess.PIPE,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def test_platform_can_be_disabled_for_native_multiarch_images(self):
|
|
317
|
+
result = subprocess.CompletedProcess(
|
|
318
|
+
["docker", "run", "--rm", "--entrypoint=free", "custom/petfit:arm64", "-m"],
|
|
319
|
+
0,
|
|
320
|
+
stdout=b"Mem: 32000 1000 31000\n",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
with patch("petfit_docker.cli.subprocess.run", return_value=result) as run:
|
|
324
|
+
self.assertEqual(check_memory("custom/petfit:arm64", platform=None), 32000)
|
|
325
|
+
|
|
326
|
+
run.assert_called_once_with(
|
|
327
|
+
["docker", "run", "--rm", "--entrypoint=free", "custom/petfit:arm64", "-m"],
|
|
328
|
+
stdout=subprocess.PIPE,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def test_failed_memory_probe_exits_without_traceback(self):
|
|
332
|
+
with patch("petfit_docker.cli.maybe_pull_image"):
|
|
333
|
+
with patch("petfit_docker.cli.check_memory", return_value=-1):
|
|
334
|
+
with patch("sys.stderr", new_callable=StringIO) as stderr:
|
|
335
|
+
with self.assertRaises(SystemExit) as caught:
|
|
336
|
+
ensure_image_ready("mathesong/petfit:latest")
|
|
337
|
+
|
|
338
|
+
self.assertIn("Could not detect memory capacity", stderr.getvalue())
|
|
339
|
+
self.assertEqual(str(caught.exception), "Do you have permission to run docker?")
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
if __name__ == "__main__":
|
|
343
|
+
unittest.main()
|