cnda_dl 1.1.0__tar.gz → 1.2.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cnda_dl
3
- Version: 1.1.0
3
+ Version: 1.2.0
4
4
  Summary: A command line utility for downloading fMRI data from CNDA
5
5
  Author-Email: Ramone Agard <agardr@wustl.edu>, Joey Scanga <joeyscanga92@gmail.com>
6
6
  Requires-Python: <4.0,>=3.9
@@ -1,32 +1,28 @@
1
- #!/usr/bin/env python
2
-
3
1
  '''
4
2
  Script to download MRI sessions from the CNDA
5
3
  Authors:
6
4
  Joey Scanga (scanga@wustl.edu)
7
5
  Ramone Agard (rhagard@wustl.edu)
8
6
  '''
9
- from .formatters import ParensOnRightFormatter1, Colors
10
7
  from glob import glob
11
- from matplotlib.ticker import EngFormatter
12
8
  from pathlib import Path
13
- from pyxnat import Interface
14
- import concurrent.futures
15
- import pyxnat
9
+ import atexit
10
+ import re
16
11
  import argparse
17
12
  import logging
18
13
  import os
19
- import progressbar
20
14
  import shlex
21
15
  import shutil
22
- import hashlib
23
16
  import subprocess
24
17
  import sys
25
18
  import xml.etree.ElementTree as et
26
- import zipfile
27
19
  import datetime
28
20
 
29
- CONNECTION_POOL_SIZE = 10
21
+ import pyxnat as px
22
+ import progressbar as pb
23
+
24
+ from .formatters import ParensOnRightFormatter1
25
+ from .zip_utils import recursive_unzip, unzipped
30
26
 
31
27
  default_log_format = "%(levelname)s:%(funcName)s: %(message)s"
32
28
  sout_handler = logging.StreamHandler(stream=sys.stdout)
@@ -65,7 +61,7 @@ def handle_dir_creation(dir_path: Path):
65
61
  logger.info("Invalid response")
66
62
 
67
63
 
68
- def download_xml(central: Interface,
64
+ def download_xml(central: px.Interface,
69
65
  subject_id: str,
70
66
  project_id: str,
71
67
  file_path: Path):
@@ -77,10 +73,10 @@ def download_xml(central: Interface,
77
73
  return True
78
74
 
79
75
 
80
- def retrieve_experiment(central: Interface,
76
+ def retrieve_experiment(central: px.Interface,
81
77
  session: str,
82
78
  experiment_id: bool = False,
83
- project_id: str = None) -> pyxnat.jsonutil.JsonTable:
79
+ project_id: str = None) -> px.jsonutil.JsonTable:
84
80
 
85
81
  query_params = {}
86
82
  if project_id:
@@ -93,171 +89,147 @@ def retrieve_experiment(central: Interface,
93
89
  return central.array.mrsessions(**query_params)
94
90
 
95
91
 
96
- def get_xml_scans(xml_file: Path,
97
- quality_pair: bool = False) -> dict:
92
+ def get_xml_scans(xml_file: Path) -> dict:
93
+ """
94
+ Create a map of downloaded scan IDs to UIDs to later match with the UIDs in the .dat files
98
95
 
96
+ :param xml_file: path to quality XML
97
+ :type xml_file: pathlib.Path
98
+ """
99
99
  xml_tree = et.parse(xml_file)
100
100
  prefix = "{" + str(xml_tree.getroot()).split("{")[-1].split("}")[0] + "}"
101
101
  scan_xml_entries = xml_tree.getroot().find(
102
102
  f"./{prefix}experiments/{prefix}experiment/{prefix}scans"
103
103
  )
104
- if quality_pair:
105
- return {s.get("ID"): s.find(f"{prefix}quality").text
106
- for s in scan_xml_entries}
107
104
  return scan_xml_entries
108
105
 
109
106
 
110
- def download_experiment_dicoms(session_experiment: pyxnat.jsonutil.JsonTable,
111
- central: Interface,
112
- session_dicom_dir: Path,
113
- xml_file_path: Path,
114
- scan_number_start: str = None,
115
- skip_unusable: bool = False):
116
- project_id = session_experiment["project"]
117
- exp_id = session_experiment['ID']
118
-
119
- # parse the xml file for the scan quality information
120
- quality_pairs = get_xml_scans(xml_file=xml_file_path,
121
- quality_pair=True)
122
-
123
- # retrieve the list of scans for this session
124
- scans = central.select(f"/projects/{project_id}/experiments/{exp_id}/scans/*/").get()
125
- scans.sort()
126
- logger.info(f"Found {len(scans)} scans for this session")
127
-
128
- # truncate the scan list if a starting point was given
129
- if scan_number_start:
130
- assert scan_number_start in scans, "Specified scan number does not exist for this session/experiment"
131
- sdex = scans.index(scan_number_start)
132
- scans = scans[sdex:]
133
- logger.info(f"Downloading scans for this session starting from series {scan_number_start}")
134
-
135
- # remove the unusable scans from the list if skipping is requested
136
- if skip_unusable:
137
- scans = [s for s in scans if quality_pairs[s] != "unusable"]
138
- logger.info(f"The following scans were marked 'unusable' and will not be downloaded: \n\t {[s for s,q in quality_pairs.items() if q=='unusable']}")
139
-
140
- # Get total number of files
141
- def _get_file_objects_in_scan(scan: str) -> dict:
142
- file_objects = central.select(f"/projects/{project_id}/experiments/{exp_id}/scans/{scan}/resources/files").get("")
143
- return {file_obj: scan for file_obj in file_objects}
144
-
145
- all_scan_file_objects = {}
146
- with concurrent.futures.ThreadPoolExecutor(max_workers=CONNECTION_POOL_SIZE) as executor:
147
- future_dicts = executor.map(_get_file_objects_in_scan, scans)
148
- for future_dict in future_dicts:
149
- all_scan_file_objects.update(future_dict)
150
-
151
- total_file_count = len(all_scan_file_objects.keys())
152
- logger.info(f"Total number of files: {total_file_count}")
153
-
154
- # Make DICOM directories for each series number
155
- for scan in scans:
156
- series_path = session_dicom_dir / scan / "DICOM"
157
- series_path.mkdir(parents=True, exist_ok=True)
158
-
159
- # So log message does not interfere with format of the progress bar
160
- logger.removeHandler(sout_handler)
161
- zero_size_files = set()
162
- fmt = EngFormatter('B')
163
-
164
- # Function assigned to threads
165
- def _download_session_file(f, scan):
166
- file_attrs = {}
167
- series_path = session_dicom_dir / scan / "DICOM"
168
- assert series_path.is_dir()
169
- file_attrs = {
170
- "name": series_path / f._uri.split("/")[-1],
171
- "size": fmt(int(f.size())) if f.size() else fmt(0),
172
- "isempty": True if not f.size() else False,
173
- "isdownloaded": False
107
+ def get_scan_types(xml_path):
108
+ # Get unique scan types to include in POST req
109
+ with open(xml_path, "r") as f:
110
+ xml_text = f.read()
111
+ return list(set(
112
+ re.findall(
113
+ r'ID="[\d\w_\-]+"\s+type="([a-zA-Z0-9\-_\.]+)"',
114
+ xml_text
115
+ )
116
+ ))
117
+
118
+
119
+ def get_resources(xml_path):
120
+ # Get "additional resources" that appear on CNDA for the session
121
+ # (usually NORDIC_VOLUMES)
122
+ with open(xml_path, "r") as f:
123
+ xml_text = f.read()
124
+ return list(set(
125
+ re.findall(
126
+ r'resource label="([a-zA-Z0-9\-_]+)"',
127
+ xml_text
128
+ )
129
+ ))
130
+
131
+
132
+ def download_experiment_zip(central: px.Interface,
133
+ exp: px.jsonutil.JsonTable,
134
+ dicom_dir: Path,
135
+ xml_file_path: Path,
136
+ keep_zip: bool = False):
137
+ '''
138
+ Download scan data as .zip from CNDA.
139
+
140
+ :param central: CNDA connection object
141
+ :type central: pyxnat.Interface
142
+ :param exp: object containing experiment information
143
+ :type exp: pyxnat.jsonutil.JsonTable
144
+ :param dicom_dir: Path to session-specific directory where DICOMs should be downloaded
145
+ :type dicom_dir: pathlib.Path
146
+ :param xml_file_path: Path to experiment XML
147
+ :type xml_file_path: pathlib.Path
148
+ :param keep_zip: Will not delete downloaded zip file after unzipping
149
+ :type keep_zip: bool
150
+ '''
151
+ sub_obj = central.select(f"/project/{exp['project']}/subjects/{exp['xnat:mrsessiondata/subject_id']}")
152
+ exp_obj = central.select(f"/project/{exp['project']}/subjects/{exp['xnat:mrsessiondata/subject_id']}/experiments/{exp['ID']}")
153
+ # Step 1: make POST request to prepare .zip download
154
+ res1 = central.post(
155
+ "/xapi/archive/downloadwithsize",
156
+ json={
157
+ "sessions": [f"{exp['project']}:{sub_obj.label()}:{exp_obj.label()}:{exp['ID']}"],
158
+ "projectIds": [exp['project']],
159
+ "scan_formats": ["DICOM"],
160
+ "scan_types": get_scan_types(xml_file_path),
161
+ "resources": get_resources(xml_file_path),
162
+ "options": ["simplified"]
174
163
  }
175
- if file_attrs["isempty"] and file_attrs["name"].exists():
176
- return file_attrs
177
- f.get(file_attrs["name"])
178
- file_attrs["isdownloaded"] = True
179
- return file_attrs
180
-
181
- # Download the session files
164
+ )
165
+ # Step 2: make GET request with created ID from POST
166
+ cur_bytes, total_bytes = 0, int(res1.json()["size"])
167
+
168
+ def _build_progress_bar():
169
+ widgets = [
170
+ pb.DataSize(), '/', pb.DataSize(variable='max_value'),
171
+ pb.Percentage(),
172
+ ' ',
173
+ pb.RotatingMarker(),
174
+ ' ',
175
+ pb.ETA(),
176
+ ' ',
177
+ pb.FileTransferSpeed()
178
+ ]
179
+ return pb.ProgressBar(
180
+ max_value=total_bytes,
181
+ widgets=widgets
182
+ )
183
+ logger.info("Downloading session .zip")
184
+ res2 = central.get(f"/xapi/archive/download/{res1.json()['id']}/zip", timeout=(60, 300))
185
+ res2.raise_for_status()
182
186
  with (
183
- progressbar.ProgressBar(max_value=total_file_count, redirect_stdout=True) as bar,
184
- concurrent.futures.ThreadPoolExecutor(max_workers=CONNECTION_POOL_SIZE) as executor
187
+ open(zip_path := (dicom_dir / f"{res1.json()['id']}.zip"), "wb") as f,
188
+ _build_progress_bar() as bar
185
189
  ):
186
- cur_file_count = 0
187
- zero_size_files = []
188
- futures = [executor.submit(_download_session_file, f, scan) for (f, scan) in all_scan_file_objects.items()]
189
- for future in concurrent.futures.as_completed(futures):
190
- try:
191
- file_attrs = future.result()
192
- cur_file_count += 1
193
- bar.update(cur_file_count)
194
- if file_attrs['isempty']:
195
- zero_size_files.append(file_attrs)
196
- if file_attrs['isdownloaded']:
197
- msg = f"Downloaded file {file_attrs['name']}, {file_attrs['size']} ({cur_file_count} out of {total_file_count})"
198
- if len(msg) > (tsize := os.get_terminal_size()[0]) - tsize // 5:
199
- msg = msg[:tsize // 2 - 4] + f"{Colors.DARK_GREY}.......{Colors.RESET}" + msg[tsize // 2 + tsize // 5:]
200
- # if len(msg) > (tsize := os.get_terminal_size()[0]) and tsize % 2 == 1:
201
- # msg = msg[:tsize // 2 - 4] + f"{Colors.DARK_GREY}.......{Colors.RESET}" + msg[tsize // 2 + tsize // 5:]
202
- logger.info(msg)
203
- # logger.info(f"\tDownloaded file {file_attrs['name']}, {file_attrs['size']} ({cur_file_count} out of {total_file_count})")
204
- print(msg)
205
- except Exception as exc:
206
- print(f"Task ended with an exception {exc}")
207
-
190
+ logger.info(f"Request headers: {res2.request.headers}")
191
+ logger.info(f"Response headers: {res2.headers}")
192
+ logger.removeHandler(sout_handler)
193
+ for chunk in res2.iter_content(chunk_size=(chunk_size := 1024)):
194
+ if chunk:
195
+ f.write(chunk)
196
+ cur_bytes += chunk_size
197
+ bar.update(cur_bytes)
208
198
  logger.addHandler(sout_handler)
209
- logger.info("DICOM download complete!")
210
- if len(zero_size_files) > 0:
211
- logger.warning(msg := f"The following downloaded files contained no data:\n{[file_attrs['name'] for file_attrs in zero_size_files]} \nCheck these files for unintended missing data!")
212
-
213
-
214
- def download_nordic_zips(session: str,
215
- central: Interface,
216
- session_experiment: pyxnat.jsonutil.JsonTable,
217
- session_dicom_dir: Path) -> list[Path]:
218
- dat_dir_list = []
219
- project_id = session_experiment["project"]
220
- exp_id = session_experiment['ID']
221
-
222
- def _digests_identical(zip_path: Path,
223
- cnda_file: pyxnat.core.resources.File):
224
- if zip_path.is_file(): # Compare digests of zip on CNDA to see if we need to redownload
225
- with zip_path.open("rb") as f:
226
- if hashlib.md5(f.read()).hexdigest() == cnda_file.attributes()['digest']: # digests match
227
- return True
228
- return False
229
-
230
- # check for zip file from NORDIC sessions
231
- nordic_volumes = central.select(f"/projects/{project_id}/experiments/{exp_id}/resources/NORDIC_VOLUMES/files").get("")
232
- logger.info(f"Found {len(nordic_volumes)} 'NORDIC_VOLUMES' for this session")
233
- for nv in nordic_volumes:
234
- zip_path = session_dicom_dir / nv._uri.split("/")[-1]
235
- if not _digests_identical(zip_path, nv):
236
- logger.info(f"Downloading {zip_path.name}")
237
- nv.get(zip_path)
238
- unzip_path = zip_path.parent / zip_path.stem
239
- with zipfile.ZipFile(zip_path, "r") as zip_ref:
240
- logger.info(f"Unzipping to {unzip_path}")
241
- zip_ref.extractall(unzip_path)
242
- dat_dir_list.append(unzip_path)
243
-
244
- return dat_dir_list
199
+ logger.info("Download complete!")
200
+ unzipped_dir = unzipped(zip_path, keep_zip=False)
201
+ recursive_unzip(unzipped_dir, keep_zip=False) # for NORDIC_VOLUMES already zipped up
245
202
 
246
203
 
247
204
  def dat_dcm_to_nifti(session: str,
248
205
  dat_directory: Path,
249
206
  xml_file_path: Path,
250
207
  session_dicom_dir: Path,
251
- nifti_path: Path,
208
+ session_nifti_dir: Path,
252
209
  skip_short_runs: bool = False):
253
- # check if the required program is on the current PATH
210
+ """
211
+ Pair .dcm/.dat files with dcmdat2niix
212
+
213
+ :param session: Session identifier
214
+ :type session: str
215
+ :param dat_directory: Directory with .dat files
216
+ :type dat_directory: pathlib.Path
217
+ :param xml_file_path: Path to session XML
218
+ :type xml_file_path: pathlib.Path
219
+ :param session_dicom_dir: Path to directory containing DICOM folders for each series
220
+ :type session_dicom_dir: pathlib.Path
221
+ :param session_nifti_dir: Path to directory containing all .dat files
222
+ :type session_nifti_dir: pathlib.Path
223
+ :param skip_short_runs: Flag which denotes we don't want to run dcmdat2niix on runs stopped short
224
+ :type skip_short_runs: bool
225
+ """
254
226
  can_convert = False
255
227
  unconverted_series = set()
256
228
  error_series = set()
257
229
  if shutil.which('dcmdat2niix') is not None:
258
230
  can_convert = True
259
- nifti_path.mkdir(parents=True, exist_ok=True)
260
- logger.info(f"Combined .dcm & .dat files (.nii.gz format) will be stored at: {nifti_path}")
231
+ session_nifti_dir.mkdir(parents=True, exist_ok=True)
232
+ logger.info(f"Combined .dcm & .dat files (.nii.gz format) will be stored at: {session_nifti_dir}")
261
233
  else:
262
234
  logger.warning("dcmdat2niix not installed or has not been added to the PATH. Cannot convert data files into NIFTI")
263
235
 
@@ -266,26 +238,22 @@ def dat_dcm_to_nifti(session: str,
266
238
  if (p / "DICOM").exists()]
267
239
  downloaded_scans.sort()
268
240
 
269
- # create a map of downloaded scan IDs to UIDs to later match with the UIDs in the .dat files
270
241
  xml_scans = get_xml_scans(xml_file=xml_file_path)
271
242
  # [:-6] is to ignore the trailing '.0.0.0' at the end of the UID string
272
243
  uid_to_id = {s.get("UID")[:-6]:s.get("ID") for s in xml_scans if s.get("ID") in downloaded_scans}
273
244
 
274
245
  # collect all of the .dat files and map them to their UIDs
275
- dat_files = list(dat_directory.glob("*.dat"))
246
+ dat_files = list(dat_directory.rglob("*.dat"))
276
247
  uid_to_dats = {uid: [d for d in dat_files if uid in d.name] for uid in uid_to_id.keys()}
277
248
 
278
249
  for uid, dats in uid_to_dats.items():
279
250
  series_id = uid_to_id[uid]
280
251
  series_path = session_dicom_dir / series_id / "DICOM"
281
252
  for dat in dats:
282
- # if (series_path / dat.name).is_file():
283
- # (series_path / dat.name).unlink()
284
253
  shutil.move(dat.resolve(), series_path.resolve())
285
254
 
286
- # Either there are no accompanying dats, or they were already in the series directory
287
255
  if len(dats) == 0:
288
- dats = list(series_path.glob("*.dat"))
256
+ dats = list(series_path.glob("*.dat")) # see if dats already in series dir
289
257
 
290
258
  dcms = list(series_path.glob("*.dcm"))
291
259
  logger.info(f"length of dats: {len(dats)}")
@@ -314,7 +282,7 @@ def dat_dcm_to_nifti(session: str,
314
282
 
315
283
  # run the dcmdat2niix subprocess
316
284
  logger.info(f"Running dcmdat2niix on series {series_id}")
317
- dcmdat2niix_cmd = shlex.split(f"dcmdat2niix -ba y -z o -w 1 -o {nifti_path} {series_path}")
285
+ dcmdat2niix_cmd = shlex.split(f"dcmdat2niix -ba y -z o -w 1 -o {session_nifti_dir} {series_path}")
318
286
  with subprocess.Popen(dcmdat2niix_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as p:
319
287
  while p.poll() is None:
320
288
  for line in p.stdout:
@@ -359,11 +327,9 @@ def main():
359
327
  help="Query by CNDA experiment identifier (default is to query by experiment 'label', which may be ambiguous)",
360
328
  action='store_true')
361
329
  parser.add_argument("-p", "--project_id",
362
- help="Specify the project ID to narrow down search. Recommended if the session list is not eperiment ids.")
363
- parser.add_argument("-s", "--scan_number",
364
- help="Select the scan number to start the download from (may be used when only ONE session/experiment is specified)")
365
- parser.add_argument("-n", "--ignore_nordic_volumes",
366
- help="Don't download a NORDIC_VOLUMES folder from CNDA if it exists.",
330
+ help="Specify the project ID to narrow down search. Recommended if the session list is not experiment ids.")
331
+ parser.add_argument("--skip_dcmdat2niix",
332
+ help="If NORDIC_VOLUMES folder is available, don't perform dcmdat2niix pairing step",
367
333
  action='store_true')
368
334
  parser.add_argument("--map_dats", type=Path,
369
335
  help="""The path to a directory containting .dat files you wish to pair with DICOM files. Using this argument
@@ -371,15 +337,15 @@ def main():
371
337
  run dcmdat2niix""")
372
338
  parser.add_argument("--log_dir", type=Path,
373
339
  help="Points to a specified directory that will store the log file. Will not make the directory if it doesn't exist.")
374
- parser.add_argument("-ssr","--skip_short_runs",
340
+ parser.add_argument("--skip_short_runs",
375
341
  action="store_true",
376
342
  help="Flag to indicate that runs stopped short should not be converted to NIFTI")
377
- parser.add_argument("--skip_unusable",
378
- help="Don't download any scans marked as 'unusable' in the XML",
379
- action='store_true')
380
343
  parser.add_argument("--dats_only",
381
344
  help="Skip downloading DICOMs, only try to pull .dat files",
382
345
  action='store_true')
346
+ parser.add_argument("--keep_zip",
347
+ help="Option to keep downloaded .zip file after unzipping",
348
+ action='store_true')
383
349
  args = parser.parse_args()
384
350
 
385
351
  # validate argument inputs
@@ -391,9 +357,6 @@ def main():
391
357
  args.log_dir.mkdir(parents=True, exist_ok=True)
392
358
  log_path = args.log_dir / f"cnda-dl_{datetime.datetime.now().strftime('%m-%d-%y_%I:%M%p')}.log"
393
359
 
394
- if args.scan_number and len(args.session_list) > 1:
395
- parser.error("'--scan_number' can only be specified when there is only one session/experiment to download")
396
-
397
360
  if args.map_dats and not args.map_dats.is_dir():
398
361
  parser.error(f"'--map_dats' directory does not exist: {args.map_dats}")
399
362
 
@@ -408,38 +371,38 @@ def main():
408
371
 
409
372
  # set up data paths
410
373
  session_list = args.session_list
411
- dicom_path = args.dicom_dir
374
+ dicom_dir = args.dicom_dir
412
375
  if hasattr(args, 'xml_dir') and args.xml_dir is not None:
413
376
  xml_path = args.xml_dir
414
377
  else:
415
- xml_path = dicom_path
378
+ xml_path = dicom_dir
416
379
 
417
- if not dicom_path.is_dir():
418
- handle_dir_creation(dicom_path)
380
+ if not dicom_dir.is_dir():
381
+ handle_dir_creation(dicom_dir)
419
382
  if not xml_path.is_dir():
420
383
  handle_dir_creation(xml_path)
421
384
 
422
385
  # set up CNDA connection
423
386
  central = None
424
387
  if not args.map_dats:
425
- central = Interface(server="https://cnda.wustl.edu/")
388
+ central = px.Interface(server="https://cnda.wustl.edu/")
389
+ atexit.register(central.disconnect)
426
390
 
427
391
  # main loop
428
392
  for session in session_list:
429
393
  download_success = False
430
394
  xml_file_path = xml_path / f"{session}.xml"
431
- session_dicom_dir = dicom_path / session
432
-
395
+ session_dicom_dir = dicom_dir / session
396
+ session_nifti_dir = dicom_dir / f"{session}_nii"
433
397
  # if only mapping is needed
434
398
  if args.map_dats:
435
399
  # map the .dat files to the correct scans and convert the files to NIFTI
436
- nifti_path = dicom_path / f"{session}_nii"
437
400
  try:
438
401
  dat_dcm_to_nifti(session=session,
439
402
  dat_directory=args.map_dats,
440
403
  xml_file_path=xml_file_path,
441
404
  session_dicom_dir=session_dicom_dir,
442
- nifti_path=nifti_path,
405
+ session_nifti_dir=session_nifti_dir,
443
406
  skip_short_runs=args.skip_short_runs)
444
407
  except Exception:
445
408
  logger.exception(f"Error moving the .dat files to the appropriate scan directories and converting to NIFTI for session: {session}")
@@ -466,66 +429,33 @@ def main():
466
429
  download_success = False
467
430
  continue
468
431
 
469
- # download the experiment data
470
- logger.info(f"Starting download of session {session}")
471
-
472
- # try to retrieve the experiment corresponding to this session
473
- exp = None
474
- try:
475
- exp = retrieve_experiment(central=central,
476
- session=session,
477
- experiment_id=args.experiment_id,
478
- project_id=args.project_id)
479
- if len(exp) == 0:
480
- raise RuntimeError("ERROR: CNDA query returned JsonTable object of length 0, meaning there were no results found with the given search parameters.")
481
- elif len(exp) > 1:
482
- raise RuntimeError("ERROR: CNDA query returned JsonTable object of length >1, meaning there were multiple results returned with the given search parameters.")
483
-
484
- except Exception:
485
-
486
- continue
487
-
488
- # download the xml for this session
489
432
  download_xml(central=central,
490
433
  subject_id=exp["xnat:mrsessiondata/subject_id"],
491
434
  project_id=exp["project"],
492
435
  file_path=xml_file_path)
493
- # try to download the files for this experiment
494
436
  if not args.dats_only:
495
437
  try:
496
- download_experiment_dicoms(session_experiment=exp,
497
- central=central,
498
- session_dicom_dir=session_dicom_dir,
499
- xml_file_path=xml_file_path,
500
- scan_number_start=args.scan_number,
501
- skip_unusable=args.skip_unusable)
502
- except Exception:
438
+ download_experiment_zip(central=central,
439
+ exp=exp,
440
+ dicom_dir=dicom_dir,
441
+ xml_file_path=xml_file_path,
442
+ keep_zip=args.keep_zip)
443
+ except Exception as e:
503
444
  logger.exception(f"Error downloading the experiment data from CNDA for session: {session}")
445
+ logger.exception(f"{e=}")
504
446
  download_success = False
505
447
  continue
506
448
 
507
- # exit if skipping the NORDIC files
508
- if args.ignore_nordic_volumes:
509
- continue
510
- # try to download NORDIC related files and convert raw data to NIFTI
511
- try:
512
- nordic_dat_dirs = download_nordic_zips(session=session,
513
- central=central,
514
- session_experiment=exp,
515
- session_dicom_dir=session_dicom_dir)
516
- nifti_path = dicom_path / f"{session}_nii"
517
- for nordic_dat_path in nordic_dat_dirs:
518
- dat_dcm_to_nifti(session=session,
519
- dat_directory=nordic_dat_path,
520
- xml_file_path=xml_file_path,
521
- session_dicom_dir=session_dicom_dir,
522
- nifti_path=nifti_path,
523
- skip_short_runs=args.skip_short_runs)
524
- except Exception:
525
- logger.exception(f"Error downloading 'NORDIC_VOLUMES' and converting to NIFTI for session: {session}")
526
- download_success = False
449
+ nordic_dat_dir = session_dicom_dir / "NORDIC_VOLUMES"
450
+ recursive_unzip(nordic_dat_dir)
451
+ if args.skip_dcmdat2niix or not nordic_dat_dir.is_dir():
527
452
  continue
528
-
453
+ dat_dcm_to_nifti(session=session,
454
+ dat_directory=nordic_dat_dir,
455
+ xml_file_path=xml_file_path,
456
+ session_dicom_dir=session_dicom_dir,
457
+ session_nifti_dir=session_nifti_dir,
458
+ skip_short_runs=args.skip_short_runs)
529
459
  if download_success:
530
460
  logger.info("\nDownloads Complete")
531
461
 
@@ -0,0 +1,28 @@
1
+ import zipfile
2
+ from pathlib import Path
3
+ import logging
4
+
5
+ logger = logging.getLogger()
6
+
7
+
8
+ def unzipped(zip_path: str | Path, keep_zip: bool = False):
9
+ if isinstance(zip_path, str):
10
+ zip_path = Path(zip_path)
11
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
12
+ logger.info(f"Unzipping {zip_path}...")
13
+ zip_ref.extractall(zip_path.parent)
14
+ if not keep_zip:
15
+ logger.info(f"Removing {zip_path}...")
16
+ zip_path.unlink()
17
+ return zip_path.with_suffix('')
18
+
19
+
20
+ def recursive_unzip(top_dir: str | Path, keep_zip: bool = False):
21
+ if isinstance(top_dir, str):
22
+ top_dir = Path(top_dir)
23
+ zips_exist = True
24
+ while zips_exist:
25
+ zips_exist = False
26
+ for zip_path in top_dir.rglob("*.zip"):
27
+ zips_exist = True
28
+ unzipped(zip_path)
@@ -25,7 +25,7 @@ dependencies = [
25
25
  "pyxnat>=1.6.2",
26
26
  ]
27
27
  name = "cnda_dl"
28
- version = "1.1.0"
28
+ version = "1.2.0"
29
29
  description = "A command line utility for downloading fMRI data from CNDA"
30
30
  readme = "README.md"
31
31
 
File without changes
File without changes
File without changes
File without changes