oceanproc 1.2.1__tar.gz → 1.3.0__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.
Files changed (21) hide show
  1. {oceanproc-1.2.1 → oceanproc-1.3.0}/PKG-INFO +1 -1
  2. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/bids_wrapper.py +40 -18
  3. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/events_long.py +6 -1
  4. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/group_series.py +8 -34
  5. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/ocean_proc.py +53 -25
  6. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/preprocessing_wrapper.py +56 -20
  7. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/segmentation_wrapper.py +32 -8
  8. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/utils.py +35 -7
  9. {oceanproc-1.2.1 → oceanproc-1.3.0}/pyproject.toml +1 -1
  10. {oceanproc-1.2.1 → oceanproc-1.3.0}/README.md +0 -0
  11. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/__init__.py +0 -0
  12. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/firstlevel/__init__.py +0 -0
  13. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/firstlevel/run_glm.py +0 -0
  14. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/firstlevel/tests/__init__.py +0 -0
  15. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/firstlevel/tests/test_run_glm.py +0 -0
  16. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/grouplevel/__init__.py +0 -0
  17. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/grouplevel/computations.py +0 -0
  18. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/grouplevel/design_mat_utils.py +0 -0
  19. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/grouplevel/dscalar_utils.py +0 -0
  20. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/grouplevel/utils.py +0 -0
  21. {oceanproc-1.2.1 → oceanproc-1.3.0}/oceanproc/oceanparse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: oceanproc
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: MRI preprocessing for WUSTL Ocean labs
5
5
  Author-Email: Ramone Agard <rhagard@wustl.edu>, Joey Scanga <joeyscanga92@gmail.com>
6
6
  Requires-Python: <3.13,>=3.10
@@ -4,14 +4,12 @@ from argparse import ArgumentParser
4
4
  from pathlib import Path
5
5
  import json
6
6
  import os
7
- import re
8
7
  import shutil
9
- import subprocess
10
8
  from textwrap import dedent
11
9
  import xml.etree.ElementTree as et
12
- from .utils import exit_program_early, prompt_user_continue, prepare_subprocess_logging, debug_logging, log_linebreak, flags, run_subprocess
10
+ from .utils import exit_program_early, prompt_user_continue, prepare_subprocess_logging, debug_logging, log_linebreak, flags, run_subprocess, update_permissions
13
11
  import logging
14
- from bids import BIDSLayout, BIDSLayoutIndexer
12
+ from bids import BIDSLayout
15
13
  from bisect import bisect
16
14
  module_logger = logging.getLogger("fsspec")
17
15
  module_logger.setLevel(logging.CRITICAL)
@@ -136,16 +134,16 @@ def run_dcm2bids(subject:str,
136
134
  logger.debug(f"deleting file: {f}")
137
135
  os.remove(f)
138
136
 
139
- except RuntimeError or subprocess.CalledProcessError as e:
137
+ except (Exception, KeyboardInterrupt) as e:
140
138
  prepare_subprocess_logging(logger, stop=True)
141
139
  logger.exception(e, stack_info=True)
142
- exit_program_early(f"Problem running '{title}'.")
140
+ return False
143
141
 
142
+ return True
144
143
 
145
144
  @debug_logging
146
145
  def run_dcm2niix(source_dir:Path,
147
- tmp_nifti_dir:Path,
148
- clean_up_func=None):
146
+ tmp_nifti_dir:Path):
149
147
  """
150
148
  Run dcm2niix with the given input and output directories.
151
149
 
@@ -176,11 +174,12 @@ def run_dcm2niix(source_dir:Path,
176
174
  log_linebreak()
177
175
  logger.info("####### Converting DICOM files into NIFTI #######\n")
178
176
  run_subprocess(helper_command, title=title)
179
- except RuntimeError or subprocess.CalledProcessError as e:
177
+ except (Exception, KeyboardInterrupt) as e:
180
178
  prepare_subprocess_logging(logger, stop=True)
181
179
  logger.exception(e, stack_info=True)
182
- exit_program_early(f"Problem running '{title}'.",
183
- exit_func=clean_up_func if clean_up_func else None)
180
+ return False
181
+
182
+ return True
184
183
 
185
184
 
186
185
  def get_usability_from_file(usability_file:Path):
@@ -322,7 +321,8 @@ def dicom_to_bids(subject:str,
322
321
  if not p.exists():
323
322
  exit_program_early(f"Path {str(p)} does not exist.")
324
323
 
325
- if (path_that_exists := bids_dir / f"sub-{subject}/ses-{session}").exists() and (not skip_prompt):
324
+ sub_ses_bids_dir = bids_dir / f"sub-{subject}/ses-{session}"
325
+ if sub_ses_bids_dir.exists() and (not skip_prompt):
326
326
  ans = prompt_user_continue(dedent("""
327
327
  A raw data bids path for this subject and session already exists.
328
328
  Would you like to delete its contents and rerun dcm2bids? If not,
@@ -330,7 +330,7 @@ def dicom_to_bids(subject:str,
330
330
  """))
331
331
  if ans:
332
332
  logger.debug("removing the old BIDS raw data directory and its contents")
333
- shutil.rmtree(path_that_exists)
333
+ shutil.rmtree(sub_ses_bids_dir)
334
334
  else:
335
335
  return
336
336
 
@@ -345,16 +345,27 @@ def dicom_to_bids(subject:str,
345
345
  logger.warning(f"There was a problem deleting the temporary directory at {tmp_nifti_path}")
346
346
 
347
347
  clean_up(quiet=True)
348
+ nii_success = True
348
349
  if not nifti:
349
- run_dcm2niix(source_dir=source_dir,
350
- tmp_nifti_dir=tmp_nifti_path)
350
+ nii_success = run_dcm2niix(source_dir=source_dir,
351
+ tmp_nifti_dir=tmp_nifti_path)
351
352
  else:
352
353
  # copy current niftis to tmp_nifti_path
353
354
  logger.info(f"copying the source NIFTI files to tmp directory, {tmp_nifti_path}")
354
355
  shutil.copytree(source_dir.resolve(), tmp_nifti_path.resolve())
355
356
 
356
-
357
- # filter out know 'weird' files that can be created during conversion
357
+ # update the permissions on the nifti files
358
+ update_permissions(
359
+ permissions=flags.file_permissions,
360
+ path=tmp_nifti_path,
361
+ recursive=True,
362
+ group=flags.permissions_group
363
+ )
364
+
365
+ if not nii_success:
366
+ exit_program_early(f"Problem running 'dcm2niix'")
367
+
368
+ # filter out known 'weird' files that can be created during conversion
358
369
  if not keep_all_niftis:
359
370
  files_to_remove = []
360
371
  short_runs = list(tmp_nifti_path.glob("[!.]*[a-z].nii*")) + list(tmp_nifti_path.glob("[!.]*[a-z].json"))
@@ -396,13 +407,24 @@ def dicom_to_bids(subject:str,
396
407
  usability_file=usability_file,
397
408
  session_number=session_index)
398
409
 
399
- run_dcm2bids(subject=subject,
410
+ dcm2bids_success = run_dcm2bids(subject=subject,
400
411
  session=session,
401
412
  nifti_dir=tmp_nifti_path,
402
413
  bids_output_dir=bids_dir,
403
414
  config_file=bids_config,
404
415
  nordic_config=nordic_config,
405
416
  skip_validate=skip_validate)
417
+
418
+ update_permissions(
419
+ permissions=flags.file_permissions,
420
+ path=sub_ses_bids_dir,
421
+ recursive=True,
422
+ group=flags.permissions_group,
423
+ quiet=True
424
+ )
425
+
426
+ if not dcm2bids_success:
427
+ exit_program_early(f"Problem running 'dcm2bids'")
406
428
 
407
429
  if not flags.debug:
408
430
  clean_up()
@@ -2,7 +2,7 @@ import pandas as pd
2
2
  import numpy as np
3
3
  import nibabel as nib
4
4
  import json
5
- from .utils import exit_program_early, debug_logging, log_linebreak
5
+ from .utils import exit_program_early, debug_logging, log_linebreak, update_permissions, flags
6
6
  from pathlib import Path
7
7
  import logging
8
8
 
@@ -162,6 +162,11 @@ def create_events_and_confounds(bids_path:Path, derivs_path:Path, sub:str, ses:s
162
162
  output_file=event_file_out)
163
163
  append_to_confounds(confounds_file=confounds_file,
164
164
  fd_thresh=fd_thresh)
165
+ update_permissions(
166
+ permissions=flags.file_permissions,
167
+ path=event_file_out,
168
+ group=flags.permissions_group
169
+ )
165
170
 
166
171
 
167
172
 
@@ -3,53 +3,20 @@
3
3
  import json
4
4
  from datetime import datetime, timedelta
5
5
  import argparse
6
- import re
7
6
  from pathlib import Path
8
- import xml.etree.ElementTree as et
9
7
  from bids import BIDSLayout
10
8
  import logging
11
9
 
12
10
  from bids.layout import BIDSFile
13
- from .utils import exit_program_early, debug_logging, log_linebreak, flags
11
+ from .utils import exit_program_early, debug_logging, log_linebreak, flags, update_permissions
14
12
  import nibabel as nib
15
13
  import numpy as np
16
- import shutil
17
14
 
18
15
  module_logger = logging.getLogger("fsspec")
19
16
  module_logger.setLevel(logging.CRITICAL)
20
17
 
21
18
  logger = logging.getLogger(__name__)
22
19
 
23
-
24
- # @debug_logging
25
- # def get_locals_from_xml(xml_path: Path) -> tuple[set, str]:
26
- # """
27
- # Read in the xml file to find the localizers.
28
-
29
- # :param xml_path: Path to XML generated by XNAT
30
- # :type xml_path: pathlib.Path
31
- # :return: Set containing all localizer IDs
32
- # :rtype: set
33
- # """
34
- # tree = et.parse(xml_path)
35
- # prefix = "{" + str(tree.getroot()).split("{")[-1].split("}")[0] + "}"
36
- # scan_element_list = list(tree.iter(f"{prefix}scans"))
37
- # exp_element_list = list(tree.iter(f"{prefix}experiments"))
38
- # study_id = exp_element_list[0][0].get("study_id")
39
-
40
- # if len(scan_element_list) != 1:
41
- # exit_program_early(f"Error parsing the xml file provided. Found none or more than one scan groups")
42
-
43
- # scans = scan_element_list[0]
44
-
45
- # localizers = set()
46
- # for s in scans:
47
- # if re.match(r"Localizer.*", s.get("type")) and s.find(f'{prefix}quality').text == "usable":
48
- # acq_time = datetime.strptime(s.find(f"{prefix}startTime").text, "%H:%M:%S")
49
- # series_id = s.get("ID")
50
- # localizers.add((acq_time, series_id))
51
- # return (sorted(localizers), study_id)
52
-
53
20
  DEFAULT_LOCALIZER_LABEL = "00"
54
21
  EXTRACTED_ACQ_VALUE = "bestVol"
55
22
 
@@ -331,6 +298,13 @@ def extract_best_acqusition(fmap_file: str|BIDSFile, bids_layout: BIDSLayout) ->
331
298
  with open(best_acq_json, "w") as out_js:
332
299
  json.dump(jd, out_js, indent=4)
333
300
 
301
+ for fmap_out_path in [best_acq_fmap, best_acq_json]:
302
+ update_permissions(
303
+ permissions=flags.file_permissions,
304
+ path=fmap_out_path,
305
+ group=flags.permissions_group,
306
+ )
307
+
334
308
  return (BIDSFile(best_acq_fmap), BIDSFile(best_acq_json))
335
309
 
336
310
 
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env python3
2
2
 
3
3
  import argparse
4
+ from ast import arg
4
5
  import sys
5
6
  from pathlib import Path
6
7
  import logging
@@ -10,10 +11,12 @@ from oceanproc.group_series import map_fmap_to_func, map_fmap_to_func_with_pairi
10
11
  from oceanproc.preprocessing_wrapper import process_data, adult_defaults, infant_defaults, mount_opts, insert_dummy_frames
11
12
  from oceanproc.segmentation_wrapper import segmentation_args, segment_anatomical
12
13
  from oceanproc.events_long import create_events_and_confounds
13
- from oceanproc.utils import exit_program_early, prompt_user_continue, default_log_format, add_file_handler, export_args_to_file, flags, debug_logging, log_linebreak, extract_options, make_option
14
+ from oceanproc.utils import *
14
15
  from oceanproc.oceanparse import OceanParser
15
16
  from bids import BIDSLayout, BIDSLayoutIndexer
16
17
  import shutil
18
+ import grp
19
+ import os
17
20
  from textwrap import dedent
18
21
 
19
22
  logging.basicConfig(level=logging.INFO,
@@ -65,6 +68,32 @@ def main():
65
68
  f"path string <{path}> does not represent an existing file"
66
69
  )
67
70
  return p
71
+
72
+ def ValidPermissions(permissions):
73
+ try:
74
+ validate_permissions_string(permissions)
75
+ except ValueError:
76
+ raise argparse.ArgumentTypeError(
77
+ f"permissions string <{permissions}> does not represent a valid permissions string"
78
+ )
79
+ return permissions
80
+
81
+ def ValidGroup(group):
82
+ try:
83
+ group_id = grp.getgrnam(group).gr_gid
84
+ valid_groups = os.getgroups()
85
+ if group_id not in valid_groups:
86
+ raise argparse.ArgumentTypeError(
87
+ f"current user does not belong to group <{group}>"
88
+ )
89
+ except KeyError:
90
+ raise argparse.ArgumentTypeError(
91
+ f"group name <{group}> is not a valid system user group"
92
+ )
93
+ return group
94
+
95
+ default_user_group = grp.getgrgid(os.getgid()).gr_name
96
+ default_file_permissions = "771"
68
97
 
69
98
  parser = OceanParser(
70
99
  prog="oceanproc",
@@ -74,7 +103,7 @@ def main():
74
103
  epilog="An arguments file can be accepted with @FILEPATH.\nAny additional arguments not listed in the help message will be passed to the preprocessing subprocess.",
75
104
  allow_abbrev=False
76
105
  )
77
- parser.add_argument("--version", action="version", version='%(prog)s v1.1.8')
106
+ parser.add_argument("--version", action="version", version='%(prog)s v1.3.0')
78
107
  session_args = parser.add_argument_group("Session Specific")
79
108
  config_args = parser.add_argument_group("Configuration Arguments", "These arguments are saved to a file if the '--export_args' option is used")
80
109
  parser.register("type", "Full-Path", lambda p: Path(p).resolve())
@@ -155,19 +184,16 @@ def main():
155
184
  help="Path to the BIBSnet apptainer image to use for segmentation. If provided, BIBSnet segmentation will be run and the outputs will be used for preprocessing. (Must be used with the '--infant' flag)")
156
185
  config_args.add_argument("--bibsnet_work", "-bw", type=ExistingDir,
157
186
  help="The path to the working directory used to store intermediate files for BIBSnet")
187
+ config_args.add_argument("--permissions_group", "-g", type=ValidGroup, default=default_user_group,
188
+ help=f"The user group that should own all of the created files. (Default is for the current user is '{default_user_group}'")
189
+ config_args.add_argument("--file_permissions", "-fp", type=ValidPermissions, default=default_file_permissions,
190
+ help=f"The file permissions that all output files should have, represented in numeric format. (Default permissions is '{default_file_permissions}'")
158
191
  args, unknown_args = parser.parse_known_args()
159
192
 
160
- # try:
161
- # assert args.derivs_path.is_dir(), "Derivatives directory must exist but it cannot be found"
162
- # assert args.bids_path.is_dir(), "Raw Bids directory must exist but it cannot be found"
163
- # if not args.skip_dcm2bids:
164
- # assert args.source_data.is_dir(), "Source data directory must exist but it cannot be found"
165
- # assert args.work_dir.is_dir(), "Work directroy must exist but it cannot be found"
166
- # except AssertionError as e:
167
- # logger.exception(e)
168
- # parser.error(e)
169
-
170
- defaults = infant_defaults if args.infant else adult_defaults
193
+ if args.infant:
194
+ flags.infant = True
195
+
196
+ defaults = infant_defaults if flags.infant else adult_defaults
171
197
  for k,v in defaults.__dict__.items():
172
198
  if k in args.__dict__ and args.__dict__[k] is None:
173
199
  args.__dict__[k] = v
@@ -175,15 +201,12 @@ def main():
175
201
  unknown_args = extract_options(unknown_args)
176
202
 
177
203
  bibsnet_path = (args.derivs_path / "bibsnet").resolve()
178
- if args.infant and args.bibsnet_image_path:
204
+ if flags.infant and args.bibsnet_image_path:
179
205
  if args.derivatives is None:
180
206
  args.derivatives = [bibsnet_path]
181
207
  elif bibsnet_path not in args.derivatives:
182
208
  args.derivatives.append(bibsnet_path)
183
209
 
184
- # if args.bibsnet_work and (not args.bibsnet_work.exists()):
185
- # parser.error("BIBSnet working directory must exist but it cannot be found")
186
-
187
210
  if args.longitudinal:
188
211
  for lg_group in args.longitudinal:
189
212
  if not lg_group[0].is_dir():
@@ -195,11 +218,10 @@ def main():
195
218
  parser.error(f"usability file must be a .xml or .json file, and it must exist: {lg_group[1]}")
196
219
  else:
197
220
  parser.error(f"longitudinal argument cannot have more than 2 elements: {lg_group}")
221
+ flags.longitudinal = True
198
222
 
199
223
  preproc_image = f"{defaults.image_name}:{args.image_version}"
200
224
  if args.preproc_image_path:
201
- # if not args.preproc_image_path.exists():
202
- # parser.error(f"Cannot find the preprocessing apptainer image at the path: {args.preproc_image_path}")
203
225
  flags.apptainer = True
204
226
  preproc_image = args.preproc_image_path
205
227
 
@@ -212,6 +234,11 @@ def main():
212
234
  if args.debug_mode:
213
235
  flags.debug = True
214
236
  logger.setLevel(logging.DEBUG)
237
+
238
+ flags.file_permissions = args.file_permissions
239
+ flags.permissions_group = args.permissions_group
240
+ flags.gid = grp.getgrnam(flags.permissions_group).gr_gid
241
+ flags.uid = os.getuid()
215
242
 
216
243
  logger.info("Starting oceanproc...")
217
244
  logger.info(f"Log will be stored at {log_path}")
@@ -222,14 +249,11 @@ def main():
222
249
  assert args.export_args.parent.exists() and args.export_args.suffix, "Argument export path must be a file path in a directory that exists"
223
250
  logger.info(f"####### Exporting Configuration Arguments to: '{args.export_args}' #######")
224
251
  export_args_to_file(args, config_args, args.export_args, extra_args=unknown_args)
252
+ update_permissions(permissions=flags.file_permissions, path=args.export_args, group=flags.permissions_group)
225
253
  except Exception as e:
226
254
  logger.exception(e)
227
255
  exit_program_early(e)
228
256
 
229
- if args.longitudinal:
230
- flags.longitudinal = True
231
- if args.infant:
232
- flags.infant = True
233
257
 
234
258
  # log the input arguments
235
259
  for k,v in (dict(args._get_kwargs())).items():
@@ -275,6 +299,11 @@ def main():
275
299
  with open(bids_path / ".bidsignore", "r") as f:
276
300
  ignore_list.extend(f.readlines())
277
301
  ignore_list = list(set(ignore_list))
302
+ other_sub_paths = [
303
+ p.name for p in bids_path.glob("sub-*")
304
+ if not f"sub-{args.subject}".startswith(p.name) and p.is_dir() # 'startswith' to account for pybids regexifying after the end of subject name
305
+ ]
306
+ ignore_list.extend(other_sub_paths)
278
307
  bids_layout = BIDSLayout(args.bids_path, validate=False, indexer=BIDSLayoutIndexer(ignore=ignore_list, validate=False))
279
308
 
280
309
  ##### Remove the scans marked as 'unusable' #####
@@ -299,7 +328,6 @@ def main():
299
328
  subject=args.subject,
300
329
  session=args.session,
301
330
  layout=bids_layout,
302
- # xml_path=xml_data_path,
303
331
  allow_uneven_fmap_groups=args.allow_uneven_fmap_groups,
304
332
  extract_best_fmap=args.extract_best_fmap
305
333
  )
@@ -359,7 +387,7 @@ def main():
359
387
  )
360
388
 
361
389
  ##### Create long formatted event files #####
362
- if not args.skip_event_files:
390
+ if not args.skip_event_files and not args.anat_only:
363
391
  create_events_and_confounds(
364
392
  bids_path=args.bids_path,
365
393
  derivs_path=preproc_derivs_path,
@@ -3,9 +3,8 @@
3
3
  from pathlib import Path
4
4
  import logging
5
5
  from bids import BIDSLayout
6
- from .utils import exit_program_early, make_option, prepare_subprocess_logging, flags, debug_logging, log_linebreak, run_subprocess
6
+ from .utils import exit_program_early, make_option, prepare_subprocess_logging, flags, debug_logging, log_linebreak, run_subprocess, update_permissions
7
7
  import shutil
8
- from subprocess import Popen, PIPE
9
8
  from matplotlib import pyplot as plt
10
9
  from bs4 import BeautifulSoup as bsoup
11
10
  import pandas as pd
@@ -76,14 +75,17 @@ def run_preprocessing(subject:str,
76
75
  exit_program_early(f"Bids path {bids_path} does not exist.")
77
76
  elif not derivs_path.exists():
78
77
  exit_program_early(f"Derivatives path {derivs_path} does not exist.")
78
+
79
+ license_mount = "/opt/freesurfer/license.txt"
80
+ bids_mount = "/data"
81
+ derivs_mount = "/out"
82
+ work_mount = "/work"
79
83
 
80
84
  if flags.apptainer:
81
- cmd_prelude = "apptainer run --nv --cleanenv --no-mount cwd"
85
+ cmd_prelude = f"apptainer run --nv --cleanenv --no-mount cwd --pwd {work_mount}"
82
86
  mount_flag = "-B"
83
- else:
84
- uid = Popen(["id", "-u"], stdout=PIPE).stdout.read().decode("utf-8").strip()
85
- gid = Popen(["id", "-g"], stdout=PIPE).stdout.read().decode("utf-8").strip()
86
- cmd_prelude = f"docker run --rm -i -u {uid}:{gid}"
87
+ else:
88
+ cmd_prelude = f"docker run --rm -i -u {flags.uid}:{flags.gid}"
87
89
  mount_flag = "-v"
88
90
 
89
91
  additional_mount_paths = []
@@ -104,10 +106,6 @@ def run_preprocessing(subject:str,
104
106
  additional_mount_paths = " ".join(additional_mount_paths)
105
107
  additional_mount_args = " ".join(additional_mount_args)
106
108
 
107
- license_mount = "/opt/freesurfer/license.txt"
108
- bids_mount = "/data"
109
- derivs_mount = "/out"
110
- work_mount = "/work"
111
109
  preproc_command = f"""{cmd_prelude}
112
110
  {mount_flag} {license_file.resolve()}:{license_mount}:ro
113
111
  {mount_flag} {bids_path.resolve()}:{bids_mount}:ro
@@ -121,16 +119,18 @@ def run_preprocessing(subject:str,
121
119
  {additional_mount_args}
122
120
  {option_chain}"""
123
121
 
122
+ success = True
124
123
  try:
125
124
  run_subprocess(preproc_command, title=title)
126
- except RuntimeError as e:
125
+ except (Exception, KeyboardInterrupt) as e:
127
126
  prepare_subprocess_logging(logger, stop=True)
128
127
  logger.exception(e, stack_info=True)
129
- exit_program_early(f"Program '{title}' has run into an error.",
130
- None if flags.debug else clean_up)
128
+ success = False
129
+
131
130
  if not flags.debug:
132
131
  clean_up()
133
132
 
133
+ return success
134
134
 
135
135
  @debug_logging
136
136
  def add_fd_plot_to_report(subject:str,
@@ -355,7 +355,7 @@ def process_data(subject:str,
355
355
  option_chain = " ".join([make_option(v, key=k, delimeter=" ", convert_underscore=True) for k,v in kwargs.items()])
356
356
  preproc_title = "NiBabies" if flags.infant else "fMRIPrep"
357
357
 
358
- run_preprocessing(subject=subject,
358
+ preproc_success = run_preprocessing(subject=subject,
359
359
  session=session if flags.infant else None,
360
360
  bids_path=bids_path,
361
361
  derivs_path=derivs_path,
@@ -366,8 +366,44 @@ def process_data(subject:str,
366
366
  option_chain=option_chain,
367
367
  additional_mounts=additional_mounts,
368
368
  remove_work_folder=remove_work_folder)
369
-
370
- add_fd_plot_to_report(subject=subject,
371
- session=session,
372
- derivs_path=derivs_path,
373
- title=preproc_title)
369
+
370
+ if preproc_success:
371
+ add_fd_plot_to_report(subject=subject,
372
+ session=session,
373
+ derivs_path=derivs_path,
374
+ title=preproc_title)
375
+
376
+ paths_to_update = []
377
+ paths_to_update.extend(sorted(derivs_path.glob(f"sub-{subject}*")))
378
+ paths_to_update.extend(sorted(derivs_path.glob(f"sourcedata/*/sub-{subject}*")))
379
+ for out_path in paths_to_update:
380
+ # update file permissions for preprocessing outputs
381
+ update_permissions(
382
+ permissions=flags.file_permissions,
383
+ path=out_path,
384
+ recursive=True,
385
+ group=flags.permissions_group
386
+ )
387
+
388
+ # check other top-level files
389
+ other_paths = [derivs_path/"dataset_description.json", derivs_path/"logs"]
390
+ for op in other_paths:
391
+ if op.exists() and (op.stat().st_uid == flags.uid):
392
+ update_permissions(
393
+ permissions=flags.file_permissions,
394
+ path=op,
395
+ recursive=True,
396
+ group=flags.permissions_group
397
+ )
398
+
399
+ # update file permisions for working directory files
400
+ update_permissions(
401
+ permissions=flags.file_permissions,
402
+ path=work_path,
403
+ recursive=True,
404
+ group=flags.permissions_group,
405
+ quiet=True
406
+ )
407
+
408
+ if not preproc_success:
409
+ exit_program_early(f"Program '{preproc_title}' has run into an error.")
@@ -2,7 +2,8 @@
2
2
 
3
3
  from pathlib import Path
4
4
  import logging
5
- from .utils import exit_program_early, make_option, prepare_subprocess_logging, flags, debug_logging, log_linebreak, run_subprocess
5
+ from sre_constants import SUCCESS
6
+ from .utils import exit_program_early, make_option, prepare_subprocess_logging, flags, debug_logging, log_linebreak, run_subprocess, update_permissions
6
7
  import shutil
7
8
  from pathlib import Path
8
9
 
@@ -59,9 +60,6 @@ def run_bibsnet(subject:str,
59
60
  elif not bibsnet_image.exists():
60
61
  exit_program_early(f"BIBSnet image path {derivs_path} does not exist.")
61
62
 
62
- # uid = Popen(["id", "-u"], stdout=PIPE).stdout.read().decode("utf-8").strip()
63
- # gid = Popen(["id", "-g"], stdout=PIPE).stdout.read().decode("utf-8").strip()
64
-
65
63
  bids_mount = "/data"
66
64
  derivs_mount = "/out"
67
65
  work_mount = "/work"
@@ -75,16 +73,18 @@ def run_bibsnet(subject:str,
75
73
  -w {work_mount} --verbose
76
74
  {option_chain}"""
77
75
 
76
+ success = True
78
77
  try:
79
78
  run_subprocess(bibsnet_command, title="BIBSnet")
80
- except RuntimeError as e:
79
+ except (Exception, KeyboardInterrupt) as e:
81
80
  prepare_subprocess_logging(logger, stop=True)
82
81
  logger.exception(e, stack_info=True)
83
- exit_program_early(f"Program 'BIBSnet' has run into an error.",
84
- None if flags.debug else clean_up)
82
+ success = False
83
+
85
84
  if not flags.debug:
86
85
  clean_up()
87
86
 
87
+ return success
88
88
 
89
89
  @debug_logging
90
90
  def segment_anatomical(subject:str,
@@ -116,7 +116,7 @@ def segment_anatomical(subject:str,
116
116
  """
117
117
  option_chain = " ".join([make_option(v, key=k, delimeter=" ", convert_underscore=True) for k,v in kwargs.items()])
118
118
 
119
- run_bibsnet(subject=subject,
119
+ segment_success = run_bibsnet(subject=subject,
120
120
  session=session,
121
121
  bids_path=bids_path,
122
122
  derivs_path=derivs_path,
@@ -124,5 +124,29 @@ def segment_anatomical(subject:str,
124
124
  bibsnet_image=bibsnet_image,
125
125
  option_chain=option_chain,
126
126
  remove_work_folder=remove_work_folder)
127
+
128
+ paths_to_update = sorted(derivs_path.glob(f"bibsnet/sub-{subject}*"))
129
+ paths_to_update.extend(sorted(work_path.glob(f"*/sub-{subject}*")))
130
+ for out_path in paths_to_update:
131
+ # update file permissions for segmentation outputs and working files
132
+ update_permissions(
133
+ permissions=flags.file_permissions,
134
+ path=out_path,
135
+ recursive=True,
136
+ group=flags.permissions_group)
137
+
138
+ # check other top-level files
139
+ other_paths = [derivs_path/"dataset_description.json"]
140
+ for op in other_paths:
141
+ if op.exists() and (op.stat().st_uid == flags.uid):
142
+ update_permissions(
143
+ permissions=flags.file_permissions,
144
+ path=op,
145
+ recursive=True,
146
+ group=flags.permissions_group
147
+ )
148
+
149
+ if not segment_success:
150
+ exit_program_early(f"Program 'BIBSnet' has run into an error.")
127
151
 
128
152
 
@@ -239,20 +239,21 @@ def prepare_subprocess_logging(this_logger,
239
239
 
240
240
 
241
241
  @debug_logging
242
- def run_subprocess(cmd: str, title: str):
242
+ def run_subprocess(cmd: str, title: str, log_output=True):
243
243
  split_cmd = shlex.split(cmd)
244
244
  logger.info(f"running '{title}' with the following command: \n{' '.join(split_cmd)}\n")
245
245
  prepare_subprocess_logging(logger)
246
246
  with Popen(split_cmd, stdout=PIPE, stderr=PIPE) as p:
247
247
  while p.poll() == None:
248
- for line in p.stdout:
249
- logger.info(line.decode("utf-8", "ignore"))
250
- for line in p.stderr:
251
- logger.info(line.decode("utf-8", "ignore"))
248
+ if log_output:
249
+ for line in p.stdout:
250
+ logger.info(line.decode("utf-8", "ignore"))
251
+ for line in p.stderr:
252
+ logger.info(line.decode("utf-8", "ignore"))
252
253
  prepare_subprocess_logging(logger, stop=True)
253
254
  p.kill()
254
- if p.poll() != 0:
255
- raise RuntimeError(f"process -'{title}'- has ended with a non-zero exit code.")
255
+ if (ecode:=p.poll()) != 0:
256
+ raise RuntimeError(f"process -'{title}'- has ended with a non-zero exit code <{ecode}>.")
256
257
 
257
258
 
258
259
  def log_linebreak():
@@ -389,3 +390,30 @@ def parcellate_dtseries(dtseries_path: Path,
389
390
  exit_program_early(f"Program '{title}' has run into an error.")
390
391
 
391
392
  return ptseries_path
393
+
394
+ def validate_permissions_string(permissions:str):
395
+ valid = True
396
+ if (len(permissions) != 3) or not permissions.isnumeric():
397
+ valid = False
398
+ if valid:
399
+ for character in permissions:
400
+ if int(character) > 7 or int(character) < 0:
401
+ valid = False
402
+
403
+ if not valid:
404
+ raise ValueError(f"Did not receive a valid permissions string: <{permissions}>")
405
+
406
+
407
+ def update_permissions(permissions:str, path:Path, recursive=False, group=None, quiet=False):
408
+ validate_permissions_string(permissions)
409
+ if not path.exists():
410
+ if quiet:
411
+ return
412
+ raise ValueError(f"Did not receive a valid path: <{path}>")
413
+
414
+ chmod_cmd = f"chmod {'-R' if recursive else ''} {permissions} {path.resolve()}"
415
+ run_subprocess(cmd=chmod_cmd, title="chmod", log_output=False)
416
+
417
+ if group is not None:
418
+ chgrp_cmd = f"chgrp {'-R' if recursive else ''} {group} {path.resolve()}"
419
+ run_subprocess(cmd=chgrp_cmd, title="chgrp", log_output=False)
@@ -34,7 +34,7 @@ dependencies = [
34
34
  "pybids>=0.16.3",
35
35
  ]
36
36
  name = "oceanproc"
37
- version = "1.2.1"
37
+ version = "1.3.0"
38
38
  description = "MRI preprocessing for WUSTL Ocean labs"
39
39
  readme = "README.md"
40
40
 
File without changes