From 320f13137f51ba77b54e49f0ef26153f598663bb Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 8 May 2024 22:48:19 +0200 Subject: [PATCH 01/12] reformat --- .pre-commit-config.yaml | 16 ++++++ pyproject.toml | 14 +++++ run.py | 119 +++++++++++++++++++++++++++------------- 3 files changed, 110 insertions(+), 39 deletions(-) create mode 100644 pyproject.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b424f4..327808f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,5 +32,21 @@ repos: types: [dockerfile] entry: ghcr.io/hadolint/hadolint hadolint +# Aplly black formatting to python code +# https://github.com/psf/black +- repo: https://github.com/psf/black-pre-commit-mirror + rev: 24.4.2 + hooks: + - id: black + args: [--config, pyproject.toml] + +# Checks for spelling errors +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + args: [--toml, pyproject.toml] + additional_dependencies: [tomli] + ci: skip: [hadolint-docker] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c698b5a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.black] +# 'extend-exclude' excludes files or directories in addition to the defaults +line-length = 79 + +# [tool.codespell] +# ignore-words = ".github/codespell_ignore_words.txt" +# skip = "" + +[tool.isort] +combine_as_imports = true +line_length = 79 +profile = "black" +skip_gitignore = true + diff --git a/run.py b/run.py index fb1938d..012b2e8 100755 --- a/run.py +++ b/run.py @@ -6,55 +6,79 @@ import numpy from glob import glob -__version__ = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), - 'version')).read() +__version__ = open( + os.path.join(os.path.dirname(os.path.realpath(__file__)), "version") +).read() + def run(command, env={}): merged_env = os.environ merged_env.update(env) - process = subprocess.Popen(command, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, shell=True, - env=merged_env) + process = subprocess.Popen( + command, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + shell=True, + env=merged_env, + ) while True: line = process.stdout.readline() - line = str(line, 'utf-8')[:-1] + line = str(line, "utf-8")[:-1] print(line) - if line == '' and process.poll() != None: + if line == "" and process.poll() != None: break if process.returncode != 0: - raise Exception("Non zero return code: %d"%process.returncode) + raise Exception("Non zero return code: %d" % process.returncode) + -parser = argparse.ArgumentParser(description='Example BIDS App entrypoint script.') -parser.add_argument('bids_dir', help='The directory with the input dataset ' - 'formatted according to the BIDS standard.') -parser.add_argument('output_dir', help='The directory where the output files ' - 'should be stored. If you are running group level analysis ' - 'this folder should be prepopulated with the results of the' - 'participant level analysis.') -parser.add_argument('analysis_level', help='Level of the analysis that will be performed. ' - 'Multiple participant level analyses can be run independently ' - '(in parallel) using the same output_dir.', - choices=['participant', 'group']) -parser.add_argument('--participant_label', help='The label(s) of the participant(s) that should be analyzed. The label ' - 'corresponds to sub- from the BIDS spec ' - '(so it does not include "sub-"). If this parameter is not ' - 'provided all subjects should be analyzed. Multiple ' - 'participants can be specified with a space separated list.', - nargs="+") -parser.add_argument('--skip_bids_validator', help='Whether or not to perform BIDS dataset validation', - action='store_true') +parser = argparse.ArgumentParser( + description="Example BIDS App entrypoint script." +) +parser.add_argument( + "bids_dir", + help="The directory with the input dataset " + "formatted according to the BIDS standard.", +) +parser.add_argument( + "output_dir", + help="The directory where the output files " + "should be stored. If you are running group level analysis " + "this folder should be prepopulated with the results of the" + "participant level analysis.", +) +parser.add_argument( + "analysis_level", + help="Level of the analysis that will be performed. " + "Multiple participant level analyses can be run independently " + "(in parallel) using the same output_dir.", + choices=["participant", "group"], +) +parser.add_argument( + "--participant_label", + help="The label(s) of the participant(s) that should be analyzed. The label " + "corresponds to sub- from the BIDS spec " + '(so it does not include "sub-"). If this parameter is not ' + "provided all subjects should be analyzed. Multiple " + "participants can be specified with a space separated list.", + nargs="+", +) +parser.add_argument( + "--skip_bids_validator", + help="Whether or not to perform BIDS dataset validation", + action="store_true", +) parser.add_argument( - '-v', - '--version', - action='version', - version=f'BIDS-App example version {__version__}', + "-v", + "--version", + action="version", + version=f"BIDS-App example version {__version__}", ) args = parser.parse_args() if not args.skip_bids_validator: - run(f'bids-validator {args.bids_dir}') + run(f"bids-validator {args.bids_dir}") subjects_to_analyze = [] # only for a subset of subjects @@ -63,15 +87,28 @@ def run(command, env={}): # for all subjects else: subject_dirs = glob(os.path.join(args.bids_dir, "sub-*")) - subjects_to_analyze = [subject_dir.split("-")[-1] for subject_dir in subject_dirs] + subjects_to_analyze = [ + subject_dir.split("-")[-1] for subject_dir in subject_dirs + ] # running participant level if args.analysis_level == "participant": # find all T1s and skullstrip them for subject_label in subjects_to_analyze: - for T1_file in (glob(os.path.join(args.bids_dir, f"sub-{subject_label}", "anat", "*_T1w.nii*")) - + glob(os.path.join(args.bids_dir, f"sub-{subject_label}", "ses-*", "anat", "*_T1w.nii*"))): + for T1_file in glob( + os.path.join( + args.bids_dir, f"sub-{subject_label}", "anat", "*_T1w.nii*" + ) + ) + glob( + os.path.join( + args.bids_dir, + f"sub-{subject_label}", + "ses-*", + "anat", + "*_T1w.nii*", + ) + ): out_file = os.path.split(T1_file)[-1].replace("_T1w.", "_brain.") cmd = f"bet {T1_file} {os.path.join(args.output_dir, out_file)}" print(cmd) @@ -80,10 +117,14 @@ def run(command, env={}): elif args.analysis_level == "group": brain_sizes = [] for subject_label in subjects_to_analyze: - for brain_file in glob(os.path.join(args.output_dir, f"sub-{subject_label}*.nii*")): + for brain_file in glob( + os.path.join(args.output_dir, f"sub-{subject_label}*.nii*") + ): data = nibabel.load(brain_file).get_fdata() - # calcualte average mask size in voxels + # calculate average mask size in voxels brain_sizes.append((data != 0).sum()) - with open(os.path.join(args.output_dir, "avg_brain_size.txt"), 'w') as fp: - fp.write("Average brain size is %g voxels"%numpy.array(brain_sizes).mean()) + with open(os.path.join(args.output_dir, "avg_brain_size.txt"), "w") as fp: + fp.write( + "Average brain size is %g voxels" % numpy.array(brain_sizes).mean() + ) From 4fa005668f18b78237df515c7e6cd89bf4e427c4 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 8 May 2024 22:50:59 +0200 Subject: [PATCH 02/12] use functions --- run.py | 187 +++++++++++++++++++++++++++++---------------------------- 1 file changed, 96 insertions(+), 91 deletions(-) diff --git a/run.py b/run.py index 012b2e8..8cc13aa 100755 --- a/run.py +++ b/run.py @@ -11,7 +11,9 @@ ).read() -def run(command, env={}): +def run(command, env=None): + if env is None: + env = {} merged_env = os.environ merged_env.update(env) process = subprocess.Popen( @@ -30,101 +32,104 @@ def run(command, env={}): if process.returncode != 0: raise Exception("Non zero return code: %d" % process.returncode) +def return_parser(): + parser = argparse.ArgumentParser( + description="Example BIDS App entrypoint script." + ) + parser.add_argument( + "bids_dir", + help="The directory with the input dataset " + "formatted according to the BIDS standard.", + ) + parser.add_argument( + "output_dir", + help="The directory where the output files " + "should be stored. If you are running group level analysis " + "this folder should be prepopulated with the results of the" + "participant level analysis.", + ) + parser.add_argument( + "analysis_level", + help="Level of the analysis that will be performed. " + "Multiple participant level analyses can be run independently " + "(in parallel) using the same output_dir.", + choices=["participant", "group"], + ) + parser.add_argument( + "--participant_label", + help="The label(s) of the participant(s) that should be analyzed. The label " + "corresponds to sub- from the BIDS spec " + '(so it does not include "sub-"). If this parameter is not ' + "provided all subjects should be analyzed. Multiple " + "participants can be specified with a space separated list.", + nargs="+", + ) + parser.add_argument( + "--skip_bids_validator", + help="Whether or not to perform BIDS dataset validation", + action="store_true", + ) + parser.add_argument( + "-v", + "--version", + action="version", + version=f"BIDS-App example version {__version__}", + ) + return parser -parser = argparse.ArgumentParser( - description="Example BIDS App entrypoint script." -) -parser.add_argument( - "bids_dir", - help="The directory with the input dataset " - "formatted according to the BIDS standard.", -) -parser.add_argument( - "output_dir", - help="The directory where the output files " - "should be stored. If you are running group level analysis " - "this folder should be prepopulated with the results of the" - "participant level analysis.", -) -parser.add_argument( - "analysis_level", - help="Level of the analysis that will be performed. " - "Multiple participant level analyses can be run independently " - "(in parallel) using the same output_dir.", - choices=["participant", "group"], -) -parser.add_argument( - "--participant_label", - help="The label(s) of the participant(s) that should be analyzed. The label " - "corresponds to sub- from the BIDS spec " - '(so it does not include "sub-"). If this parameter is not ' - "provided all subjects should be analyzed. Multiple " - "participants can be specified with a space separated list.", - nargs="+", -) -parser.add_argument( - "--skip_bids_validator", - help="Whether or not to perform BIDS dataset validation", - action="store_true", -) -parser.add_argument( - "-v", - "--version", - action="version", - version=f"BIDS-App example version {__version__}", -) - +def main(): -args = parser.parse_args() + parser = return_parser() + args = parser.parse_args() -if not args.skip_bids_validator: - run(f"bids-validator {args.bids_dir}") + if not args.skip_bids_validator: + run(f"bids-validator {args.bids_dir}") -subjects_to_analyze = [] -# only for a subset of subjects -if args.participant_label: - subjects_to_analyze = args.participant_label -# for all subjects -else: - subject_dirs = glob(os.path.join(args.bids_dir, "sub-*")) - subjects_to_analyze = [ - subject_dir.split("-")[-1] for subject_dir in subject_dirs - ] + subjects_to_analyze = [] + # only for a subset of subjects + if args.participant_label: + subjects_to_analyze = args.participant_label + # for all subjects + else: + subject_dirs = glob(os.path.join(args.bids_dir, "sub-*")) + subjects_to_analyze = [ + subject_dir.split("-")[-1] for subject_dir in subject_dirs + ] -# running participant level -if args.analysis_level == "participant": + # running participant level + if args.analysis_level == "participant": - # find all T1s and skullstrip them - for subject_label in subjects_to_analyze: - for T1_file in glob( - os.path.join( - args.bids_dir, f"sub-{subject_label}", "anat", "*_T1w.nii*" - ) - ) + glob( - os.path.join( - args.bids_dir, - f"sub-{subject_label}", - "ses-*", - "anat", - "*_T1w.nii*", - ) - ): - out_file = os.path.split(T1_file)[-1].replace("_T1w.", "_brain.") - cmd = f"bet {T1_file} {os.path.join(args.output_dir, out_file)}" - print(cmd) - run(cmd) + # find all T1s and skullstrip them + for subject_label in subjects_to_analyze: + for T1_file in glob( + os.path.join( + args.bids_dir, f"sub-{subject_label}", "anat", "*_T1w.nii*" + ) + ) + glob( + os.path.join( + args.bids_dir, + f"sub-{subject_label}", + "ses-*", + "anat", + "*_T1w.nii*", + ) + ): + out_file = os.path.split(T1_file)[-1].replace("_T1w.", "_brain.") + cmd = f"bet {T1_file} {os.path.join(args.output_dir, out_file)}" + print(cmd) + run(cmd) -elif args.analysis_level == "group": - brain_sizes = [] - for subject_label in subjects_to_analyze: - for brain_file in glob( - os.path.join(args.output_dir, f"sub-{subject_label}*.nii*") - ): - data = nibabel.load(brain_file).get_fdata() - # calculate average mask size in voxels - brain_sizes.append((data != 0).sum()) + elif args.analysis_level == "group": + brain_sizes = [] + for subject_label in subjects_to_analyze: + for brain_file in glob( + os.path.join(args.output_dir, f"sub-{subject_label}*.nii*") + ): + data = nibabel.load(brain_file).get_fdata() + # calculate average mask size in voxels + brain_sizes.append((data != 0).sum()) - with open(os.path.join(args.output_dir, "avg_brain_size.txt"), "w") as fp: - fp.write( - "Average brain size is %g voxels" % numpy.array(brain_sizes).mean() - ) + with open(os.path.join(args.output_dir, "avg_brain_size.txt"), "w") as fp: + fp.write( + "Average brain size is %g voxels" % numpy.array(brain_sizes).mean() + ) From 7e5c9feb4a823f8b47fe196455ec1737945d10a5 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 8 May 2024 23:14:34 +0200 Subject: [PATCH 03/12] refactor --- run.py | 51 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/run.py b/run.py index 8cc13aa..d932786 100755 --- a/run.py +++ b/run.py @@ -4,11 +4,11 @@ import subprocess import nibabel import numpy +import sys from glob import glob +from pathlib import Path -__version__ = open( - os.path.join(os.path.dirname(os.path.realpath(__file__)), "version") -).read() +__version__ = open(Path(__file__).parent / "version").read() def run(command, env=None): @@ -38,35 +38,35 @@ def return_parser(): ) parser.add_argument( "bids_dir", - help="The directory with the input dataset " - "formatted according to the BIDS standard.", + help="The directory with the input dataset formatted according to the BIDS standard.", ) parser.add_argument( "output_dir", - help="The directory where the output files " - "should be stored. If you are running group level analysis " - "this folder should be prepopulated with the results of the" - "participant level analysis.", + help=""" +The directory where the output files should be stored. +If you are running group level analysis this folder should be prepopulated +with the results of the participant level analysis.""", ) parser.add_argument( "analysis_level", - help="Level of the analysis that will be performed. " - "Multiple participant level analyses can be run independently " - "(in parallel) using the same output_dir.", + help=""" +Level of the analysis that will be performed. +Multiple participant level analyses can be run independently +in parallel) using the same output_dir.""", choices=["participant", "group"], ) parser.add_argument( "--participant_label", - help="The label(s) of the participant(s) that should be analyzed. The label " - "corresponds to sub- from the BIDS spec " - '(so it does not include "sub-"). If this parameter is not ' - "provided all subjects should be analyzed. Multiple " - "participants can be specified with a space separated list.", + help=""" +The label(s) of the participant(s) that should be analyzed. +The label corresponds to sub- from the BIDS spec +(so it does not include "sub-"). If this parameter is not provided all subjects should be analyzed. +Multiple participants can be specified with a space separated list.""", nargs="+", ) parser.add_argument( "--skip_bids_validator", - help="Whether or not to perform BIDS dataset validation", + help="Whether or not to perform BIDS dataset validation.", action="store_true", ) parser.add_argument( @@ -77,10 +77,14 @@ def return_parser(): ) return parser -def main(): +def main(argv = sys.argv): parser = return_parser() - args = parser.parse_args() + args, unknowns = parser.parse_known_args(argv[1:]) + + if unknowns: + print(f"The following arguments are unknown: {unknowns}") + exit(1) if not args.skip_bids_validator: run(f"bids-validator {args.bids_dir}") @@ -119,6 +123,8 @@ def main(): print(cmd) run(cmd) + exit(0) + elif args.analysis_level == "group": brain_sizes = [] for subject_label in subjects_to_analyze: @@ -133,3 +139,8 @@ def main(): fp.write( "Average brain size is %g voxels" % numpy.array(brain_sizes).mean() ) + + exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file From b6c7843cdbe30bdb8ae34bf113f9364af65b03ce Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 21:16:28 +0000 Subject: [PATCH 04/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- pyproject.toml | 1 - run.py | 26 ++++++++++++++++++-------- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c698b5a..7b68a4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,4 +11,3 @@ combine_as_imports = true line_length = 79 profile = "black" skip_gitignore = true - diff --git a/run.py b/run.py index d932786..5bfc32c 100755 --- a/run.py +++ b/run.py @@ -32,6 +32,7 @@ def run(command, env=None): if process.returncode != 0: raise Exception("Non zero return code: %d" % process.returncode) + def return_parser(): parser = argparse.ArgumentParser( description="Example BIDS App entrypoint script." @@ -50,8 +51,8 @@ def return_parser(): parser.add_argument( "analysis_level", help=""" -Level of the analysis that will be performed. -Multiple participant level analyses can be run independently +Level of the analysis that will be performed. +Multiple participant level analyses can be run independently in parallel) using the same output_dir.""", choices=["participant", "group"], ) @@ -77,7 +78,8 @@ def return_parser(): ) return parser -def main(argv = sys.argv): + +def main(argv=sys.argv): parser = return_parser() args, unknowns = parser.parse_known_args(argv[1:]) @@ -118,8 +120,12 @@ def main(argv = sys.argv): "*_T1w.nii*", ) ): - out_file = os.path.split(T1_file)[-1].replace("_T1w.", "_brain.") - cmd = f"bet {T1_file} {os.path.join(args.output_dir, out_file)}" + out_file = os.path.split(T1_file)[-1].replace( + "_T1w.", "_brain." + ) + cmd = ( + f"bet {T1_file} {os.path.join(args.output_dir, out_file)}" + ) print(cmd) run(cmd) @@ -135,12 +141,16 @@ def main(argv = sys.argv): # calculate average mask size in voxels brain_sizes.append((data != 0).sum()) - with open(os.path.join(args.output_dir, "avg_brain_size.txt"), "w") as fp: + with open( + os.path.join(args.output_dir, "avg_brain_size.txt"), "w" + ) as fp: fp.write( - "Average brain size is %g voxels" % numpy.array(brain_sizes).mean() + "Average brain size is %g voxels" + % numpy.array(brain_sizes).mean() ) exit(0) + if __name__ == "__main__": - main() \ No newline at end of file + main() From f230cae7322537e5503365bd307d6bb3fa247770 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 8 May 2024 23:23:38 +0200 Subject: [PATCH 05/12] validate descriptor --- .github/workflows/test.yml | 28 ++++++++++++++++++++++++++++ boutiques/bids-app-example.json | 5 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..710a48b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,28 @@ +--- +name: validate boutiques descriptor + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [master] + pull_request: + branches: ['*'] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install + run: pip install boutiques + - name: Validate + run: bosh validate boutiques/bids-app-example.json diff --git a/boutiques/bids-app-example.json b/boutiques/bids-app-example.json index 2fbf4da..a0230e0 100644 --- a/boutiques/bids-app-example.json +++ b/boutiques/bids-app-example.json @@ -54,7 +54,8 @@ "participant", "group" ], - "value-key": "ANALYSIS_LEVEL" + "value-key": "ANALYSIS_LEVEL", + "optional": false }, { "command-line-flag": "--participant_label", @@ -168,4 +169,4 @@ ] }, "tool-version": "dev" -} +} \ No newline at end of file From b46974ef14193b96399627e43d16a169e361d0b9 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 8 May 2024 23:30:28 +0200 Subject: [PATCH 06/12] validate descriptor --- boutiques/{bids-app-example.json => descriptor.json} | 7 +++++-- run.py | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) rename boutiques/{bids-app-example.json => descriptor.json} (98%) diff --git a/boutiques/bids-app-example.json b/boutiques/descriptor.json similarity index 98% rename from boutiques/bids-app-example.json rename to boutiques/descriptor.json index a0230e0..2728e7f 100644 --- a/boutiques/bids-app-example.json +++ b/boutiques/descriptor.json @@ -1,15 +1,18 @@ { "author": "chrisfilo and others", + "custom": { + "BIDSApplicationVersion": "2.0" + }, "command-line": "mkdir -p OUTPUT_DIR; /run.py BIDS_DIR OUTPUT_DIR ANALYSIS_LEVEL PARTICIPANT_LABEL SESSION_LABEL", "container-image": { "image": "bids/example", "type": "docker" }, "description": "See https://github.com/BIDS-Apps/example", - "descriptor-url": "https://github.com/BIDS-Apps/example/blob/master/boutiques/bids-app-example.json", + "descriptor-url": "https://github.com/BIDS-Apps/example/blob/master/boutiques/descriptorjson", "groups": [ { - "description": "For a participants analysis, an output directory name must be specified. For a group analysis, a directory containing the output of participant-level analyses must be selected. ", + "description": "For a participants analysis, an output directory name must be specified. For a group analysis, a directory containing the output of participant-level analyses must be selected.", "id": "output_directory", "members": [ "output_dir_name", diff --git a/run.py b/run.py index 5bfc32c..03f1c7b 100755 --- a/run.py +++ b/run.py @@ -86,7 +86,7 @@ def main(argv=sys.argv): if unknowns: print(f"The following arguments are unknown: {unknowns}") - exit(1) + exit(64) if not args.skip_bids_validator: run(f"bids-validator {args.bids_dir}") From ba0b5ba25067079d765d07934fb78ef1ddd5fc34 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 8 May 2024 21:30:42 +0000 Subject: [PATCH 07/12] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/workflows/test.yml | 40 +++++++++++++++++++------------------- boutiques/descriptor.json | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 710a48b..cf56022 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,27 +2,27 @@ name: validate boutiques descriptor concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true on: - push: - branches: [master] - pull_request: - branches: ['*'] + push: + branches: [master] + pull_request: + branches: ['*'] jobs: - validate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - name: Install - run: pip install boutiques - - name: Validate - run: bosh validate boutiques/bids-app-example.json + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.12 + - name: Install + run: pip install boutiques + - name: Validate + run: bosh validate boutiques/bids-app-example.json diff --git a/boutiques/descriptor.json b/boutiques/descriptor.json index 2728e7f..cfbdc27 100644 --- a/boutiques/descriptor.json +++ b/boutiques/descriptor.json @@ -172,4 +172,4 @@ ] }, "tool-version": "dev" -} \ No newline at end of file +} From 52923329d062c736b4c58fe44f6871d255289ccd Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 8 May 2024 23:42:22 +0200 Subject: [PATCH 08/12] fix --- .github/workflows/{test.yml => validate.yml} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename .github/workflows/{test.yml => validate.yml} (89%) diff --git a/.github/workflows/test.yml b/.github/workflows/validate.yml similarity index 89% rename from .github/workflows/test.yml rename to .github/workflows/validate.yml index cf56022..d96f25e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/validate.yml @@ -25,4 +25,4 @@ jobs: - name: Install run: pip install boutiques - name: Validate - run: bosh validate boutiques/bids-app-example.json + run: bosh validate boutiques/descriptor.json From 00e339d0d9f6d77738a84126c289a29c7b5902a0 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 8 May 2024 23:47:56 +0200 Subject: [PATCH 09/12] typo --- boutiques/descriptor.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boutiques/descriptor.json b/boutiques/descriptor.json index cfbdc27..4a80ddb 100644 --- a/boutiques/descriptor.json +++ b/boutiques/descriptor.json @@ -9,7 +9,7 @@ "type": "docker" }, "description": "See https://github.com/BIDS-Apps/example", - "descriptor-url": "https://github.com/BIDS-Apps/example/blob/master/boutiques/descriptorjson", + "descriptor-url": "https://github.com/BIDS-Apps/example/blob/master/boutiques/descriptor.json", "groups": [ { "description": "For a participants analysis, an output directory name must be specified. For a group analysis, a directory containing the output of participant-level analyses must be selected.", From f9cb1d5690e44fc914c3dfd2e150abddb8c40e54 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 9 May 2024 11:50:31 -0400 Subject: [PATCH 10/12] pin dependencies and check output --- .circleci/config.yml | 17 ++++++----------- .pre-commit-config.yaml | 7 +++++-- Dockerfile | 5 +++-- pyproject.toml | 5 +---- requirements.in | 1 + requirements.txt | 12 ++++++++++++ run.py | 11 +++++++---- tox.ini | 33 +++++++++++++++++++++++++++++++++ 8 files changed, 68 insertions(+), 23 deletions(-) create mode 100644 requirements.in create mode 100644 requirements.txt create mode 100644 tox.ini diff --git a/.circleci/config.yml b/.circleci/config.yml index 055f384..1cda70b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -57,35 +57,30 @@ jobs: -v /tmp/workspace/data/ds114_test1:/bids_dataset \ bids/${CIRCLE_PROJECT_REPONAME,,} --version - # participant level tests for single session dataset - run: + name: participant level test command: | docker run -ti --rm --read-only \ -v /tmp/workspace/data/ds114_test<< parameters.dataset >>:/bids_dataset \ -v ${HOME}/outputs<< parameters.dataset >>:/outputs \ - bids/${CIRCLE_PROJECT_REPONAME,,} /bids_dataset /outputs participant --participant_label 01 - no_output_timeout: 6h - - run: - command: | - docker run -ti --rm --read-only \ - -v /tmp/workspace/data/ds114_test<< parameters.dataset >>:/bids_dataset \ - -v ${HOME}/outputs<< parameters.dataset >>:/outputs \ - bids/${CIRCLE_PROJECT_REPONAME,,} /bids_dataset /outputs participant --participant_label 02 + bids/${CIRCLE_PROJECT_REPONAME,,} /bids_dataset /outputs participant + tree ${HOME}/outputs<< parameters.dataset >> no_output_timeout: 6h - run: + name: group level test command: | docker run -ti --rm --read-only \ -v /tmp/workspace/data/ds114_test<< parameters.dataset >>:/bids_dataset \ - -v ${HOME}/outputs1:/outputs \ + -v ${HOME}/outputs<< parameters.dataset >>:/outputs \ bids/${CIRCLE_PROJECT_REPONAME,,} /bids_dataset /outputs group + tree ${HOME}/outputs<< parameters.dataset >> no_output_timeout: 6h - store_artifacts: path: ~/output<< parameters.dataset >> deploy: - machine: image: ubuntu-2204:2022.10.2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 327808f..cd4b32b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,14 +14,12 @@ repos: - id: check-case-conflict - id: check-merge-conflict - - repo: https://github.com/jumanjihouse/pre-commit-hook-yamlfmt rev: 0.2.3 hooks: - id: yamlfmt args: [--mapping, '2', --sequence, '2', --offset, '0'] - - repo: https://github.com/hadolint/hadolint rev: v2.13.0-beta hooks: @@ -32,6 +30,11 @@ repos: types: [dockerfile] entry: ghcr.io/hadolint/hadolint hadolint +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + # Aplly black formatting to python code # https://github.com/psf/black - repo: https://github.com/psf/black-pre-commit-mirror diff --git a/Dockerfile b/Dockerfile index 1e84b23..8d35fb0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,14 @@ -FROM bids/base_fsl:6.0.1 +FROM bids/base_fsl:6.0.1@sha256:b92b9b4a0642dfbe011c3827384f781ba7d377686e82edaf14f406d3eb906783 ARG DEBIAN_FRONTEND="noninteractive" +COPY requirements.txt /requirements.txt RUN apt-get update -qq && \ apt-get install -q -y --no-install-recommends \ python3 \ python3-pip && \ - pip3 install nibabel==5.1.0 && \ + pip3 install -r /requirements.txt && \ apt-get remove -y python3-pip && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/pyproject.toml b/pyproject.toml index 7b68a4d..4ebe735 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,7 @@ [tool.black] -# 'extend-exclude' excludes files or directories in addition to the defaults line-length = 79 -# [tool.codespell] -# ignore-words = ".github/codespell_ignore_words.txt" -# skip = "" +[tool.codespell] [tool.isort] combine_as_imports = true diff --git a/requirements.in b/requirements.in new file mode 100644 index 0000000..fa06aea --- /dev/null +++ b/requirements.in @@ -0,0 +1 @@ +nibabel diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c4a15ef --- /dev/null +++ b/requirements.txt @@ -0,0 +1,12 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --strip-extras requirements.in +# +nibabel==5.2.1 + # via -r requirements.in +numpy==1.26.4 + # via nibabel +packaging==24.0 + # via nibabel diff --git a/run.py b/run.py index 03f1c7b..2dfe87e 100755 --- a/run.py +++ b/run.py @@ -2,12 +2,13 @@ import argparse import os import subprocess -import nibabel -import numpy import sys from glob import glob from pathlib import Path +import nibabel +import numpy + __version__ = open(Path(__file__).parent / "version").read() @@ -145,8 +146,10 @@ def main(argv=sys.argv): os.path.join(args.output_dir, "avg_brain_size.txt"), "w" ) as fp: fp.write( - "Average brain size is %g voxels" - % numpy.array(brain_sizes).mean() + f"Average brain size is {numpy.array(brain_sizes).mean()} voxels" + ) + print( + f"Results were saved in {Path(args.output_dir) / 'avg_brain_size.txt'}" ) exit(0) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..017e43b --- /dev/null +++ b/tox.ini @@ -0,0 +1,33 @@ +; See https://tox.wiki/en +[tox] +requires = + tox>=4 +; run lint by default when just calling "tox" +env_list = lint + +; ENVIRONMENTS +; ------------ +[style] +description = common environment for style checkers (rely on pre-commit hooks) +skip_install = true +deps = + pre-commit + +; COMMANDS +; -------- +[testenv:lint] +description = install pre-commit hooks and run all linters and formatters +skip_install = true +deps = + {[style]deps} +commands = + pre-commit install + pre-commit run --all-files --show-diff-on-failure {posargs:} + +[testenv:update_dependencies] +description = update requirements.txt +skip_install = true +deps = + pip-tools +commands = + pip-compile --strip-extras requirements.in {posargs:} From cd9a0f93ab7dd7d74fae2fe976bde474ca98388c Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 9 May 2024 13:42:14 -0400 Subject: [PATCH 11/12] install tree --- .circleci/config.yml | 2 ++ Singularity | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1cda70b..bf9da26 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -50,6 +50,8 @@ jobs: - run: mkdir -p ${HOME}/outputs<< parameters.dataset >> + - run: sudo apt-get install tree + - run: name: print version command: | diff --git a/Singularity b/Singularity index af0f047..6f178ed 100644 --- a/Singularity +++ b/Singularity @@ -2,8 +2,8 @@ Bootstrap: docker From: bids/example %help -You are in the BIDS-example container. To see help run -singularity run BIDS-example.simg -h +You are in the BIDS-example container. +To see help run singularity run: BIDS-example.simg -h %runscript exec /run.py "$@" From 32d679cdf6cc06c144db15dfe95d3206b6dbbec6 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 9 May 2024 14:42:09 -0400 Subject: [PATCH 12/12] do not store artefacts --- .circleci/config.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index bf9da26..37ccfa4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,7 +67,6 @@ jobs: -v ${HOME}/outputs<< parameters.dataset >>:/outputs \ bids/${CIRCLE_PROJECT_REPONAME,,} /bids_dataset /outputs participant tree ${HOME}/outputs<< parameters.dataset >> - no_output_timeout: 6h - run: name: group level test @@ -77,10 +76,6 @@ jobs: -v ${HOME}/outputs<< parameters.dataset >>:/outputs \ bids/${CIRCLE_PROJECT_REPONAME,,} /bids_dataset /outputs group tree ${HOME}/outputs<< parameters.dataset >> - no_output_timeout: 6h - - - store_artifacts: - path: ~/output<< parameters.dataset >> deploy: machine: