Skip to content

Commit 8293c71

Browse files
committed
Adding BoshTask - Shell Command Task that uses boutiques descriptor [WIP]
1 parent bba381d commit 8293c71

File tree

4 files changed

+216
-0
lines changed

4 files changed

+216
-0
lines changed

pydra/engine/boutiques.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import typing as ty
2+
import json
3+
import attr
4+
import subprocess as sp
5+
import os
6+
from pathlib import Path
7+
8+
from ..utils.messenger import AuditFlag
9+
from ..engine import ShellCommandTask
10+
from ..engine.specs import (
11+
SpecInfo,
12+
ShellSpec,
13+
ShellOutSpec,
14+
File,
15+
Directory,
16+
attr_fields,
17+
)
18+
from .helpers import ensure_list, execute
19+
from .helpers_file import template_update, is_local_file
20+
21+
22+
class BoshTask(ShellCommandTask):
23+
"""Shell Command Task based on the Boutiques descriptor"""
24+
25+
def __init__(
26+
self,
27+
zenodo=None,
28+
bosh_file=None,
29+
audit_flags: AuditFlag = AuditFlag.NONE,
30+
cache_dir=None,
31+
input_spec: ty.Optional[SpecInfo] = None,
32+
messenger_args=None,
33+
messengers=None,
34+
name=None,
35+
output_spec: ty.Optional[SpecInfo] = None,
36+
rerun=False,
37+
strip=False,
38+
**kwargs,
39+
):
40+
"""
41+
Initialize this task.
42+
43+
Parameters
44+
----------
45+
zenodo: :obj: str
46+
Zenodo ID
47+
bosh_file : : str
48+
json file with the boutiques descriptors
49+
audit_flags : :obj:`pydra.utils.messenger.AuditFlag`
50+
Auditing configuration
51+
cache_dir : :obj:`os.pathlike`
52+
Cache directory
53+
input_spec : :obj:`pydra.engine.specs.SpecInfo`
54+
Specification of inputs.
55+
messenger_args :
56+
TODO
57+
messengers :
58+
TODO
59+
name : :obj:`str`
60+
Name of this task.
61+
output_spec : :obj:`pydra.engine.specs.BaseSpec`
62+
Specification of inputs.
63+
strip : :obj:`bool`
64+
TODO
65+
66+
"""
67+
if (bosh_file and zenodo) or not (bosh_file or zenodo):
68+
raise Exception("either bosh or zenodo has to be specified")
69+
elif zenodo:
70+
bosh_file = self._download_spec(zenodo)
71+
72+
with bosh_file.open() as f:
73+
self.bosh_spec = json.load(f)
74+
75+
if input_spec is None:
76+
input_spec = self._prepare_input_spec()
77+
self.input_spec = input_spec
78+
if output_spec is None:
79+
output_spec = self._prepare_output_spec()
80+
self.output_spec = output_spec
81+
82+
super(BoshTask, self).__init__(
83+
name=name,
84+
input_spec=input_spec,
85+
output_spec=output_spec,
86+
executable=["bosh", "exec", "launch", str(bosh_file)],
87+
args=["-s"],
88+
audit_flags=audit_flags,
89+
messengers=messengers,
90+
messenger_args=messenger_args,
91+
cache_dir=cache_dir,
92+
strip=strip,
93+
rerun=rerun,
94+
**kwargs,
95+
)
96+
self.strip = strip
97+
98+
def _download_spec(self, zenodo):
99+
""" usind bosh pull to download the zenodo file"""
100+
spec_file = (
101+
Path(os.environ["HOME"])
102+
/ ".cache/boutiques/production"
103+
/ (zenodo.replace(".", "-") + ".json")
104+
)
105+
if not spec_file.exists():
106+
sp.run(["bosh", "pull", zenodo])
107+
return spec_file
108+
109+
def _prepare_input_spec(self):
110+
""" creating input spec from the zenodo file"""
111+
binputs = self.bosh_spec["inputs"]
112+
fields = []
113+
for input in binputs:
114+
name = input["id"]
115+
if input["type"] == "File":
116+
tp = File
117+
elif input["type"] == "String":
118+
tp = str
119+
elif input["type"] == "Number":
120+
tp = float
121+
elif input["type"] == "Flag":
122+
tp = bool
123+
else:
124+
tp = None
125+
# adding list
126+
if tp and "list" in input and input["list"]:
127+
tp = ty.List[tp]
128+
129+
mdata = {
130+
"help_string": input["description"],
131+
"mandatory": not input["optional"],
132+
"argstr": input.get("command-line-flag", None),
133+
}
134+
fields.append((name, tp, mdata))
135+
136+
spec = SpecInfo(name="Inputs", fields=fields, bases=(ShellSpec,))
137+
return spec
138+
139+
def _prepare_output_spec(self):
140+
""" creating output spec from the zenodo file"""
141+
boutputs = self.bosh_spec["output-files"]
142+
fields = []
143+
for output in boutputs:
144+
name = output["id"]
145+
mdata = {
146+
"help_string": output["description"],
147+
"mandatory": not output["optional"],
148+
}
149+
fields.append((name, File, mdata))
150+
151+
spec = SpecInfo(name="Outputs", fields=fields, bases=(ShellOutSpec,))
152+
return spec
153+
154+
def _command_args_single(self, state_ind, ind=None):
155+
"""Get command line arguments for a single state"""
156+
input_filepath = self._input_file(state_ind=state_ind, ind=ind)
157+
cmd_list = self.inputs.executable + [input_filepath] + self.inputs.args
158+
return cmd_list
159+
160+
def _input_file(self, state_ind, ind=None):
161+
input_json = {}
162+
for f in attr_fields(self.inputs):
163+
if f.name in ["executable", "args"]:
164+
continue
165+
if self.state and f"{self.name}.{f.name}" in state_ind:
166+
value = getattr(self.inputs, f.name)[state_ind[f"{self.name}.{f.name}"]]
167+
else:
168+
value = getattr(self.inputs, f.name)
169+
if is_local_file(f):
170+
value = str(value)
171+
# adding to the json file if specified by the user
172+
if value is not attr.NOTHING and value != "NOTHING":
173+
input_json[f.name] = value
174+
175+
filename = self.cache_dir / f"{self.name}-{ind}.json"
176+
with open(filename, "w") as jsonfile:
177+
json.dump(input_json, jsonfile)
178+
179+
return str(filename)
180+
181+
def _run_task(self):
182+
self.output_ = None
183+
args = self.command_args
184+
if args:
185+
# removing empty strings
186+
args = [str(el) for el in args if el not in ["", " "]]
187+
keys = ["return_code", "stdout", "stderr"]
188+
values = execute(args, strip=self.strip)
189+
self.output_ = dict(zip(keys, values))
190+
if self.output_["return_code"]:
191+
if self.output_["stderr"]:
192+
raise RuntimeError(self.output_["stderr"])
193+
else:
194+
raise RuntimeError(self.output_["stdout"])
9.9 MB
Binary file not shown.

pydra/engine/tests/test_boutiques.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import os, shutil
2+
import subprocess as sp
3+
from pathlib import Path
4+
import pytest
5+
6+
from ..boutiques import BoshTask
7+
8+
need_bosh_docker = pytest.mark.skipif(
9+
shutil.which("docker") is None
10+
or sp.call(["docker", "info"] or sp.call(["bosh", "version"])),
11+
reason="requires docker and bosh",
12+
)
13+
14+
15+
@need_bosh_docker
16+
def test_boutiques():
17+
btask = BoshTask(name="NA", zenodo="zenodo.1482743")
18+
btask.inputs.infile = Path(__file__).resolve().parent / "data_tests" / "test.nii.gz"
19+
btask.inputs.maskfile = "test_brain.nii.gz"
20+
btask()

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ test_requires =
4141
notebook==5.7.8
4242
jupyter
4343
jupyter_contrib_nbextensions
44+
boutiques
4445
packages = find:
4546
include_package_data = True
4647

@@ -74,6 +75,7 @@ test =
7475
notebook==5.7.8
7576
jupyter
7677
jupyter_contrib_nbextensions
78+
boutiques
7779
tests =
7880
%(test)s
7981
dev =

0 commit comments

Comments
 (0)