Skip to content

Commit 19666a9

Browse files
committed
Merge remote-tracking branch 'upstream/master' into enh/complex_realimag
2 parents 152fc2d + 2eb5291 commit 19666a9

File tree

15 files changed

+399
-73
lines changed

15 files changed

+399
-73
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ jobs:
2121
- '3.9'
2222
- '3.10'
2323
- '3.11'
24+
- '3.12'
25+
# Seems needs work in traits: https://github.com/nipy/heudiconv/pull/799#issuecomment-2447298795
26+
# - '3.13'
2427
steps:
2528
- name: Check out repository
2629
uses: actions/checkout@v4

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,36 @@
1+
# v1.3.1 (Fri Oct 25 2024)
2+
3+
#### 🐛 Bug Fix
4+
5+
- Fix assignment of sensitive git-annex metadata data via glob patterns (regression introduced by #739) [#793](https://github.com/nipy/heudiconv/pull/793) ([@bpinsard](https://github.com/bpinsard))
6+
7+
#### Authors: 1
8+
9+
- Basile ([@bpinsard](https://github.com/bpinsard))
10+
11+
---
12+
13+
# v1.3.0 (Wed Oct 02 2024)
14+
15+
#### 🚀 Enhancement
16+
17+
- timezone aware [#780](https://github.com/nipy/heudiconv/pull/780) ([@AlanKuurstra](https://github.com/AlanKuurstra) [@yarikoptic](https://github.com/yarikoptic))
18+
19+
#### 🐛 Bug Fix
20+
21+
- BF(workaround): if heuristic provided just a string and not list of types -- make it into a tuple [#787](https://github.com/nipy/heudiconv/pull/787) ([@yarikoptic](https://github.com/yarikoptic))
22+
- Refactor create_seqinfo tiny bit to avoid duplication and add logging; and in tests to reuse list of dicom paths [#785](https://github.com/nipy/heudiconv/pull/785) ([@yarikoptic](https://github.com/yarikoptic))
23+
- extract sequence_name from PulseSequenceName on Siemens XA** data [#753](https://github.com/nipy/heudiconv/pull/753) ([@bpinsard](https://github.com/bpinsard))
24+
- Just INFO not WARNING if heuristic is missing intotoids [#784](https://github.com/nipy/heudiconv/pull/784) ([@yarikoptic](https://github.com/yarikoptic))
25+
26+
#### Authors: 3
27+
28+
- [@AlanKuurstra](https://github.com/AlanKuurstra)
29+
- Basile ([@bpinsard](https://github.com/bpinsard))
30+
- Yaroslav Halchenko ([@yarikoptic](https://github.com/yarikoptic))
31+
32+
---
33+
134
# v1.2.0 (Fri Sep 13 2024)
235

336
#### 🚀 Enhancement

Dockerfile

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Generated by Neurodocker and Reproenv.
22

3-
FROM neurodebian:bullseye
4-
ENV PATH="/opt/dcm2niix-v1.0.20220720/bin:$PATH"
3+
FROM neurodebian:bookworm
4+
ENV PATH="/opt/dcm2niix-v1.0.20240202/bin:$PATH"
55
RUN apt-get update -qq \
66
&& apt-get install -y -q --no-install-recommends \
77
ca-certificates \
@@ -16,10 +16,10 @@ RUN apt-get update -qq \
1616
&& git clone https://github.com/rordenlab/dcm2niix /tmp/dcm2niix \
1717
&& cd /tmp/dcm2niix \
1818
&& git fetch --tags \
19-
&& git checkout v1.0.20220720 \
19+
&& git checkout v1.0.20240202 \
2020
&& mkdir /tmp/dcm2niix/build \
2121
&& cd /tmp/dcm2niix/build \
22-
&& cmake -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON -DCMAKE_INSTALL_PREFIX:PATH=/opt/dcm2niix-v1.0.20220720 .. \
22+
&& cmake -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON -DCMAKE_INSTALL_PREFIX:PATH=/opt/dcm2niix-v1.0.20240202 .. \
2323
&& make -j1 \
2424
&& make install \
2525
&& rm -rf /tmp/dcm2niix
@@ -87,19 +87,19 @@ RUN printf '{ \
8787
{ \
8888
"name": "from_", \
8989
"kwds": { \
90-
"base_image": "neurodebian:bullseye" \
90+
"base_image": "neurodebian:bookworm" \
9191
} \
9292
}, \
9393
{ \
9494
"name": "env", \
9595
"kwds": { \
96-
"PATH": "/opt/dcm2niix-v1.0.20220720/bin:$PATH" \
96+
"PATH": "/opt/dcm2niix-v1.0.20240202/bin:$PATH" \
9797
} \
9898
}, \
9999
{ \
100100
"name": "run", \
101101
"kwds": { \
102-
"command": "apt-get update -qq\\napt-get install -y -q --no-install-recommends \\\\\\n ca-certificates \\\\\\n cmake \\\\\\n g++ \\\\\\n gcc \\\\\\n git \\\\\\n make \\\\\\n pigz \\\\\\n zlib1g-dev\\nrm -rf /var/lib/apt/lists/*\\ngit clone https://github.com/rordenlab/dcm2niix /tmp/dcm2niix\\ncd /tmp/dcm2niix\\ngit fetch --tags\\ngit checkout v1.0.20220720\\nmkdir /tmp/dcm2niix/build\\ncd /tmp/dcm2niix/build\\ncmake -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON -DCMAKE_INSTALL_PREFIX:PATH=/opt/dcm2niix-v1.0.20220720 ..\\nmake -j1\\nmake install\\nrm -rf /tmp/dcm2niix" \
102+
"command": "apt-get update -qq\\napt-get install -y -q --no-install-recommends \\\\\\n ca-certificates \\\\\\n cmake \\\\\\n g++ \\\\\\n gcc \\\\\\n git \\\\\\n make \\\\\\n pigz \\\\\\n zlib1g-dev\\nrm -rf /var/lib/apt/lists/*\\ngit clone https://github.com/rordenlab/dcm2niix /tmp/dcm2niix\\ncd /tmp/dcm2niix\\ngit fetch --tags\\ngit checkout v1.0.20240202\\nmkdir /tmp/dcm2niix/build\\ncd /tmp/dcm2niix/build\\ncmake -DZLIB_IMPLEMENTATION=Cloudflare -DUSE_JPEGLS=ON -DUSE_OPENJPEG=ON -DCMAKE_INSTALL_PREFIX:PATH=/opt/dcm2niix-v1.0.20240202 ..\\nmake -j1\\nmake install\\nrm -rf /tmp/dcm2niix" \
103103
} \
104104
}, \
105105
{ \

README.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
:target: https://repology.org/project/python:heudiconv/versions
4141
:alt: PyPI
4242

43+
.. image:: https://img.shields.io/badge/RRID-SCR__017427-blue
44+
:target: https://identifiers.org/RRID:SCR_017427
45+
:alt: RRID
46+
4347
About
4448
-----
4549

heudiconv/bids.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
remove_suffix,
3232
save_json,
3333
set_readonly,
34-
strptime_micr,
34+
strptime_bids,
3535
update_json,
3636
)
3737

@@ -952,18 +952,16 @@ def select_fmap_from_compatible_groups(
952952
k for k, v in acq_times_fmaps.items() if v == first_acq_time
953953
][0]
954954
elif criterion == "Closest":
955-
json_acq_time = strptime_micr(
955+
json_acq_time = strptime_bids(
956956
acq_times[
957957
# remove session folder and '.json', add '.nii.gz':
958958
remove_suffix(remove_prefix(json_file, sess_folder + op.sep), ".json")
959959
+ ".nii.gz"
960-
],
961-
"%Y-%m-%dT%H:%M:%S[.%f]",
960+
]
962961
)
963962
# differences in acquisition time (abs value):
964963
diff_fmaps_acq_times = {
965-
k: abs(strptime_micr(v, "%Y-%m-%dT%H:%M:%S[.%f]") - json_acq_time)
966-
for k, v in acq_times_fmaps.items()
964+
k: abs(strptime_bids(v) - json_acq_time) for k, v in acq_times_fmaps.items()
967965
}
968966
min_diff_acq_times = sorted(diff_fmaps_acq_times.values())[0]
969967
selected_fmap_key = [

heudiconv/convert.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,15 @@ def convert(
579579

580580
for item in items:
581581
prefix, outtypes, item_dicoms = item
582+
if isinstance(outtypes, str): # type: ignore[unreachable]
583+
lgr.warning( # type: ignore[unreachable]
584+
"Provided output types %r of type 'str' instead "
585+
"of a tuple for prefix %r. Likely need to fix-up your heuristic. "
586+
"Meanwhile we are 'manually' converting to 'tuple'",
587+
outtypes,
588+
prefix,
589+
)
590+
outtypes = (outtypes,)
582591
prefix_dirname = op.dirname(prefix)
583592
outname_bids = prefix + ".json"
584593
bids_outfiles = []

heudiconv/dicoms.py

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
get_typed_attr,
3333
load_json,
3434
set_readonly,
35-
strptime_micr,
35+
strptime_dcm_da_tm,
36+
strptime_dcm_dt,
3637
)
3738

3839
if TYPE_CHECKING:
@@ -94,15 +95,19 @@ def create_seqinfo(
9495
series_desc = get_typed_attr(dcminfo, "SeriesDescription", str, "")
9596
protocol_name = get_typed_attr(dcminfo, "ProtocolName", str, "")
9697

97-
if dcminfo.get([0x18, 0x24]):
98-
# GE and Philips
99-
sequence_name = dcminfo[0x18, 0x24].value
100-
elif dcminfo.get([0x19, 0x109C]):
101-
# Siemens
102-
sequence_name = dcminfo[0x19, 0x109C].value
103-
elif dcminfo.get([0x18, 0x9005]):
104-
# Siemens XA
105-
sequence_name = dcminfo[0x18, 0x9005].value
98+
for k, m in (
99+
([0x18, 0x24], "GE and Philips"),
100+
([0x19, 0x109C], "Siemens"),
101+
([0x18, 0x9005], "Siemens XA"),
102+
):
103+
if v := dcminfo.get(k):
104+
sequence_name = v.value
105+
lgr.debug(
106+
"Identified sequence name as %s coming from the %r family of MR scanners",
107+
sequence_name,
108+
m,
109+
)
110+
break
106111
else:
107112
sequence_name = ""
108113

@@ -544,19 +549,16 @@ def get_datetime_from_dcm(dcm_data: dcm.FileDataset) -> Optional[datetime.dateti
544549
3. SeriesDate & SeriesTime (0008,0021); (0008,0031)
545550
546551
"""
547-
acq_date = dcm_data.get("AcquisitionDate", "").strip()
548-
acq_time = dcm_data.get("AcquisitionTime", "").strip()
549-
if acq_date and acq_time:
550-
return strptime_micr(acq_date + acq_time, "%Y%m%d%H%M%S[.%f]")
551-
552-
acq_dt = dcm_data.get("AcquisitionDateTime", "").strip()
553-
if acq_dt:
554-
return strptime_micr(acq_dt, "%Y%m%d%H%M%S[.%f]")
555-
556-
series_date = dcm_data.get("SeriesDate", "").strip()
557-
series_time = dcm_data.get("SeriesTime", "").strip()
558-
if series_date and series_time:
559-
return strptime_micr(series_date + series_time, "%Y%m%d%H%M%S[.%f]")
552+
553+
def check_tag(x: str) -> bool:
554+
return x in dcm_data and dcm_data[x].value.strip()
555+
556+
if check_tag("AcquisitionDate") and check_tag("AcquisitionTime"):
557+
return strptime_dcm_da_tm(dcm_data, "AcquisitionDate", "AcquisitionTime")
558+
if check_tag("AcquisitionDateTime"):
559+
return strptime_dcm_dt(dcm_data, "AcquisitionDateTime")
560+
if check_tag("SeriesDate") and check_tag("SeriesTime"):
561+
return strptime_dcm_da_tm(dcm_data, "SeriesDate", "SeriesTime")
560562
return None
561563

562564

heudiconv/external/dlad.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,12 +156,12 @@ def add_to_datalad(
156156

157157
# Provide metadata for sensitive information
158158
sensitive_patterns = [
159-
"sourcedata",
159+
"sourcedata/**",
160160
"*_scans.tsv", # top level
161161
"*/*_scans.tsv", # within subj
162162
"*/*/*_scans.tsv", # within sess/subj
163-
"*/anat", # within subj
164-
"*/*/anat", # within ses/subj
163+
"*/anat/*", # within subj
164+
"*/*/anat/*", # within ses/subj
165165
]
166166
for sp in sensitive_patterns:
167167
mark_sensitive(ds, sp, annexed_files)

heudiconv/parser.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import os.path as op
1010
import re
1111
import shutil
12+
import sys
1213
from types import ModuleType
1314
from typing import Optional
1415

@@ -22,7 +23,18 @@
2223

2324
_VCS_REGEX = r"%s\.(?:git|gitattributes|svn|bzr|hg)(?:%s|$)" % (op.sep, op.sep)
2425

25-
_UNPACK_FORMATS = tuple(sum((x[1] for x in shutil.get_unpack_formats()), []))
26+
27+
def _get_unpack_formats() -> dict[str, bool]:
28+
"""For each extension return if it is a tar"""
29+
out = {}
30+
for _, exts, d in shutil.get_unpack_formats():
31+
for e in exts:
32+
out[e] = bool(re.search(r"\btar\b", d.lower()))
33+
return out
34+
35+
36+
_UNPACK_FORMATS = _get_unpack_formats()
37+
_TAR_UNPACK_FORMATS = tuple(k for k, is_tar in _UNPACK_FORMATS.items() if is_tar)
2638

2739

2840
@docstring_parameter(_VCS_REGEX)
@@ -114,7 +126,7 @@ def get_extracted_dicoms(fl: Iterable[str]) -> ItemsView[Optional[str], list[str
114126

115127
# needs sorting to keep the generated "session" label deterministic
116128
for _, t in enumerate(sorted(fl)):
117-
if not t.endswith(_UNPACK_FORMATS):
129+
if not t.endswith(tuple(_UNPACK_FORMATS)):
118130
sessions[None].append(t)
119131
continue
120132

@@ -127,7 +139,14 @@ def get_extracted_dicoms(fl: Iterable[str]) -> ItemsView[Optional[str], list[str
127139

128140
# check content and sanitize permission bits before extraction
129141
os.chmod(tmpdir, mode=0o700)
130-
shutil.unpack_archive(t, extract_dir=tmpdir)
142+
# For tar (only!) starting with 3.12 we should provide filter
143+
# (enforced in 3.14) on how to filter/safe-guard filenames.
144+
kws: dict[str, str] = {}
145+
if sys.version_info >= (3, 12) and t.endswith(_TAR_UNPACK_FORMATS):
146+
# Allow for a user-workaround if would be desired
147+
# see e.g. https://docs.python.org/3.12/library/tarfile.html#extraction-filters
148+
kws["filter"] = os.environ.get("HEUDICONV_TAR_FILTER", "tar")
149+
shutil.unpack_archive(t, extract_dir=tmpdir, **kws) # type: ignore[arg-type]
131150

132151
archive_content = list(find_files(regex=".*", topdir=tmpdir))
133152

heudiconv/tests/test_dicoms.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
parse_private_csa_header,
2222
)
2323

24-
from .utils import TESTS_DATA_PATH
24+
from .utils import TEST_DICOM_PATHS, TESTS_DATA_PATH
2525

2626
# Public: Private DICOM tags
2727
DICOM_FIELDS_TO_TEST = {"ProtocolName": "tProtocolName"}
@@ -180,26 +180,17 @@ def test_get_datetime_from_dcm_wo_dt() -> None:
180180
assert get_datetime_from_dcm(XA30_enhanced_dcm) is None
181181

182182

183-
dicom_test_data = [
184-
(dw.wrapper_from_file(d_file), [d_file], op.basename(d_file))
185-
for d_file in glob(op.join(TESTS_DATA_PATH, "*.dcm"))
186-
]
187-
188-
189-
@pytest.mark.parametrize("mw,series_files,series_id", dicom_test_data)
183+
@pytest.mark.parametrize("dcmfile", TEST_DICOM_PATHS)
190184
def test_create_seqinfo(
191-
mw: dw.Wrapper,
192-
series_files: list[str],
193-
series_id: str,
185+
dcmfile: str,
194186
) -> None:
195-
seqinfo = create_seqinfo(mw, series_files, series_id)
196-
assert seqinfo.sequence_name != ""
197-
pass
198-
187+
mw = dw.wrapper_from_file(dcmfile)
188+
seqinfo = create_seqinfo(mw, [dcmfile], op.basename(dcmfile))
189+
assert seqinfo.sequence_name
199190

200-
def test_get_reproducible_int() -> None:
201-
dcmfile = op.join(TESTS_DATA_PATH, "phantom.dcm")
202191

192+
@pytest.mark.parametrize("dcmfile", TEST_DICOM_PATHS)
193+
def test_get_reproducible_int(dcmfile: str) -> None:
203194
assert type(get_reproducible_int([dcmfile])) is int
204195

205196

0 commit comments

Comments
 (0)