xnatbidscli 2.0.0__py3-none-any.whl
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.
- xnatbidscli/__init__.py +6 -0
- xnatbidscli/archive.py +67 -0
- xnatbidscli/assets/mriconvert_qc.json +82 -0
- xnatbidscli/assets/physioconvert_qc.json +33 -0
- xnatbidscli/bidsmap.py +820 -0
- xnatbidscli/cli.py +525 -0
- xnatbidscli/cubids.py +149 -0
- xnatbidscli/download.py +1009 -0
- xnatbidscli/login.py +142 -0
- xnatbidscli/mriconfig.py +560 -0
- xnatbidscli/mriconvert.py +890 -0
- xnatbidscli/physioconvert.py +845 -0
- xnatbidscli/query.py +684 -0
- xnatbidscli/sysinfo.py +12 -0
- xnatbidscli-2.0.0.dist-info/METADATA +65 -0
- xnatbidscli-2.0.0.dist-info/RECORD +19 -0
- xnatbidscli-2.0.0.dist-info/WHEEL +4 -0
- xnatbidscli-2.0.0.dist-info/entry_points.txt +2 -0
- xnatbidscli-2.0.0.dist-info/licenses/LICENSE +21 -0
xnatbidscli/__init__.py
ADDED
xnatbidscli/archive.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import tarfile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
STATUS_COMPLETE = "COMPLETE"
|
|
6
|
+
STATUS_SKIPPED = "SKIPPED"
|
|
7
|
+
STATUS_FAILURE = "FAILURE"
|
|
8
|
+
STATUS_NONEXISTENT = "NONEXISTENT"
|
|
9
|
+
|
|
10
|
+
OK_STATUSES = {STATUS_COMPLETE, STATUS_SKIPPED}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def archive_path(
|
|
14
|
+
data_root: Path, project: str, subject: str, experiment: str
|
|
15
|
+
) -> Path:
|
|
16
|
+
return (
|
|
17
|
+
data_root
|
|
18
|
+
/ "archive"
|
|
19
|
+
/ f"PROJECT-{project}_SUBJECT-{subject}_EXPERIMENT-{experiment}.tar.gz"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def archive_experiment(
|
|
24
|
+
data_root: Path, project: str, subject: str, experiment: str
|
|
25
|
+
) -> tuple[str, str | None]:
|
|
26
|
+
"""Tar+gzip data_root/PROJECT/SUBJECT/EXPERIMENT into data_root/archive/.
|
|
27
|
+
|
|
28
|
+
Returns (status, detail). Existing archives are left untouched and reported
|
|
29
|
+
as SKIPPED; the tarball is written to a .tmp sibling and renamed into place
|
|
30
|
+
only on success so an interrupted run never leaves a partial file behind.
|
|
31
|
+
"""
|
|
32
|
+
src = data_root / project / subject / experiment
|
|
33
|
+
if not src.is_dir():
|
|
34
|
+
return STATUS_NONEXISTENT, f"source directory not found: {src}"
|
|
35
|
+
|
|
36
|
+
dest = archive_path(data_root, project, subject, experiment)
|
|
37
|
+
if dest.exists():
|
|
38
|
+
return STATUS_SKIPPED, f"archive already exists: {dest}"
|
|
39
|
+
|
|
40
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
41
|
+
tmp = dest.with_name(dest.name + ".tmp")
|
|
42
|
+
try:
|
|
43
|
+
with tarfile.open(tmp, "w:gz") as tar:
|
|
44
|
+
tar.add(src, arcname=experiment)
|
|
45
|
+
tmp.replace(dest)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
if tmp.exists():
|
|
48
|
+
try:
|
|
49
|
+
tmp.unlink()
|
|
50
|
+
except Exception:
|
|
51
|
+
pass
|
|
52
|
+
return STATUS_FAILURE, f"error creating archive: {e}"
|
|
53
|
+
return STATUS_COMPLETE, None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def delete_experiment_dir(
|
|
57
|
+
data_root: Path, project: str, subject: str, experiment: str
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Delete the EXPERIMENT dir and prune empty SUBJECT/PROJECT parents."""
|
|
60
|
+
exp_dir = data_root / project / subject / experiment
|
|
61
|
+
if exp_dir.exists():
|
|
62
|
+
shutil.rmtree(exp_dir, ignore_errors=True)
|
|
63
|
+
for parent in (data_root / project / subject, data_root / project):
|
|
64
|
+
try:
|
|
65
|
+
parent.rmdir()
|
|
66
|
+
except OSError:
|
|
67
|
+
return
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Dcm2BidsConfigPath": {
|
|
3
|
+
"Description": "Absolute path to the dcm2bids config JSON used to convert this project. Set via mriconvert's -c/--config; preserved from a prior run if omitted (e.g. under -m/--maps). LastModified is the timestamp (Python logging's default asctime format) of the last time -c/--config was passed and Value was updated.",
|
|
4
|
+
"Value": "",
|
|
5
|
+
"LastModified": ""
|
|
6
|
+
},
|
|
7
|
+
"PhysioParent": {
|
|
8
|
+
"Description": "Absolute path to the parent directory of all raw physio recordings for this project, stored in a flat directory structure. Set via mriconvert's -y/--physio; preserved from a prior run if omitted.",
|
|
9
|
+
"Value": ""
|
|
10
|
+
},
|
|
11
|
+
"filename": {
|
|
12
|
+
"Description": "Path to the NIfTI file relative to the root inside the PROJECT (BIDS dataset) directory, with no leading periods or slashes."
|
|
13
|
+
},
|
|
14
|
+
"acq_time": {
|
|
15
|
+
"Description": "Acquisition time of the scan, taken from the AcquisitionTime field of the file's accompanying JSON sidecar. Empty when the field or sidecar is absent."
|
|
16
|
+
},
|
|
17
|
+
"series_number": {
|
|
18
|
+
"Description": "DICOM study series number, taken from the SeriesNumber field of the file's accompanying JSON sidecar. Empty when the field or sidecar is absent."
|
|
19
|
+
},
|
|
20
|
+
"dimensions": {
|
|
21
|
+
"Description": "Image array shape as read by nibabel, given as x-joined dimension sizes and always including the 4th (time) dimension even when it is 1 (e.g. 256x256x170x1)."
|
|
22
|
+
},
|
|
23
|
+
"size_bytes": {
|
|
24
|
+
"Description": "Size of the NIfTI file on disk, in bytes."
|
|
25
|
+
},
|
|
26
|
+
"participant_id": {
|
|
27
|
+
"Description": "BIDS participant identifier (sub-<label>) parsed from the filename."
|
|
28
|
+
},
|
|
29
|
+
"session_id": {
|
|
30
|
+
"Description": "BIDS session identifier (ses-<label>) parsed from the filename."
|
|
31
|
+
},
|
|
32
|
+
"datatype": {
|
|
33
|
+
"Description": "BIDS datatype, taken from the name of the file's parent directory (e.g. anat, func, dwi, fmap)."
|
|
34
|
+
},
|
|
35
|
+
"suffix": {
|
|
36
|
+
"Description": "BIDS suffix, the portion of the basename after the last underscore and before the extension. Never empty."
|
|
37
|
+
},
|
|
38
|
+
"bids_name": {
|
|
39
|
+
"Description": "The portion of the file basename between the sub-<label>_ses-<label>_ prefix and the .nii.gz extension."
|
|
40
|
+
},
|
|
41
|
+
"rename": {
|
|
42
|
+
"Description": "Free-text field for an end-user to record a corrected bids_name when the file needs renaming. Empty by default."
|
|
43
|
+
},
|
|
44
|
+
"physio": {
|
|
45
|
+
"Description": "Free-text field for an end-user to record the basename (with extension) of a raw physio recording found under PhysioParent that was acquired alongside this scan. Consumed by xnatbidscli physioconvert to convert and place that recording directly, using this row's participant_id/session_id/datatype for placement and its bids_name/rename for naming. Empty by default; never auto-populated by mriconvert."
|
|
46
|
+
},
|
|
47
|
+
"recommend_for_use": {
|
|
48
|
+
"Description": "Whether the file is recommended for use. Empty until reviewed by an end-user.",
|
|
49
|
+
"Levels": {
|
|
50
|
+
"TRUE": "The file is recommended for use.",
|
|
51
|
+
"FALSE": "The file is not recommended for use."
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"complete": {
|
|
55
|
+
"Description": "Whether the file was acquired at its complete, as-intended length. Empty until reviewed by an end-user.",
|
|
56
|
+
"Levels": {
|
|
57
|
+
"TRUE": "The acquisition is complete.",
|
|
58
|
+
"FALSE": "The acquisition is incomplete."
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"usable": {
|
|
62
|
+
"Description": "Whether the file is usable at all. Empty until reviewed by an end-user.",
|
|
63
|
+
"Levels": {
|
|
64
|
+
"TRUE": "The file is usable.",
|
|
65
|
+
"FALSE": "The file is not usable."
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"qc_rating": {
|
|
69
|
+
"Description": "Acquisition quality-control rating. Empty until reviewed by an end-user.",
|
|
70
|
+
"Levels": {
|
|
71
|
+
"PASS": "The acquisition passed quality control.",
|
|
72
|
+
"FAIL": "The acquisition failed quality control.",
|
|
73
|
+
"UNCERTAIN": "The quality-control outcome is uncertain."
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"rating_reason": {
|
|
77
|
+
"Description": "Free-text reason for the qc_rating, open to any string of any length. Empty by default."
|
|
78
|
+
},
|
|
79
|
+
"qc_notes": {
|
|
80
|
+
"Description": "Free-text notes about the quality control not captured by other fields, open to any string of any length. Empty by default."
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"PhysioParent": {
|
|
3
|
+
"Description": "Absolute path to the parent directory of all raw physio recordings for this project, as resolved from mriconvert_qc.json at the time of this run.",
|
|
4
|
+
"Value": ""
|
|
5
|
+
},
|
|
6
|
+
"physio": {
|
|
7
|
+
"Description": "The raw physio basename (mriconvert_qc.tsv's physio column value) used to produce this row's output, as of the last conversion. Primary key of this table."
|
|
8
|
+
},
|
|
9
|
+
"status": {
|
|
10
|
+
"Description": "Per-association processing outcome, regenerated every run.",
|
|
11
|
+
"Levels": {
|
|
12
|
+
"CONVERTED": "phys2bids read the raw file and its BIDS _physio.tsv.gz/.json were written next to the paired NIfTI.",
|
|
13
|
+
"NOT_PHYSIO": "Matched a supported extension but could not be loaded as physiological data.",
|
|
14
|
+
"READER_MISSING": "The optional reader package phys2bids needs for this format is not installed.",
|
|
15
|
+
"CONVERT_ERROR": "phys2bids raised while converting, produced no .tsv.gz output, or its destination was already occupied by a different association.",
|
|
16
|
+
"SOURCE_MISSING": "PhysioParent was unset/not a directory, the named basename was not found under it, or the mriconvert_qc.tsv row is missing participant_id/datatype.",
|
|
17
|
+
"COLLISION": "This physio basename is referenced by more than one mriconvert_qc.tsv row; none of them were converted until resolved.",
|
|
18
|
+
"SKIPPED": "The association's _physio.tsv.gz already existed at its expected destination and was a valid (non-corrupt) GZIP file, so conversion was not attempted."
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"n_channels": {
|
|
22
|
+
"Description": "Number of channels phys2bids reported for the recording (its ch_amount, which includes the leading time channel). Blank for associations that were not loaded as physio."
|
|
23
|
+
},
|
|
24
|
+
"sampling_frequencies": {
|
|
25
|
+
"Description": "Unique channel sampling frequencies in hertz, ascending, comma-separated (e.g. 1000,500). A single value for the common single-frequency case. Aligned position-by-position with sample_count and duration_seconds."
|
|
26
|
+
},
|
|
27
|
+
"sample_count": {
|
|
28
|
+
"Description": "Number of samples per sampling frequency, comma-separated and aligned with sampling_frequencies (the longest channel recorded at each frequency). Blank for associations that were not loaded as physio."
|
|
29
|
+
},
|
|
30
|
+
"duration_seconds": {
|
|
31
|
+
"Description": "Acquisition duration in seconds at 0.001 s precision, computed as sample_count / sampling_frequency, comma-separated and aligned with sampling_frequencies. Blank for associations that were not loaded as physio."
|
|
32
|
+
}
|
|
33
|
+
}
|