junifer 0.0.7.dev332__py3-none-any.whl → 0.0.8__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.
Files changed (293) hide show
  1. junifer/_version.py +9 -19
  2. junifer/api/__init__.pyi +17 -2
  3. junifer/api/functions.py +222 -39
  4. junifer/api/queue_context/__init__.pyi +11 -3
  5. junifer/api/queue_context/gnu_parallel_local_adapter.py +81 -120
  6. junifer/api/queue_context/htcondor_adapter.py +118 -181
  7. junifer/api/queue_context/queue_context_adapter.py +51 -7
  8. junifer/api/queue_context/tests/test_gnu_parallel_local_adapter.py +10 -35
  9. junifer/api/queue_context/tests/test_htcondor_adapter.py +13 -50
  10. junifer/api/res/fsl/run_fsl_docker.sh +4 -0
  11. junifer/api/tests/test_functions.py +863 -36
  12. junifer/cli/__init__.py +1 -7
  13. junifer/cli/cli.py +41 -22
  14. junifer/cli/parser.py +23 -147
  15. junifer/cli/tests/data/gmd_mean.yaml +6 -6
  16. junifer/cli/tests/data/gmd_mean_htcondor.yaml +6 -5
  17. junifer/cli/tests/data/partly_cloudy_agg_mean_tian.yml +1 -0
  18. junifer/cli/tests/test_cli.py +120 -4
  19. junifer/cli/tests/test_cli_utils.py +23 -6
  20. junifer/cli/utils.py +14 -9
  21. junifer/configs/__init__.py +5 -0
  22. junifer/configs/__init__.pyi +0 -0
  23. junifer/configs/juseless/datagrabbers/__init__.pyi +4 -2
  24. junifer/configs/juseless/datagrabbers/aomic_id1000_vbm.py +18 -24
  25. junifer/configs/juseless/datagrabbers/camcan_vbm.py +20 -27
  26. junifer/configs/juseless/datagrabbers/ixi_vbm.py +44 -52
  27. junifer/configs/juseless/datagrabbers/tests/test_ixi_vbm.py +0 -7
  28. junifer/configs/juseless/datagrabbers/tests/test_ucla.py +2 -19
  29. junifer/configs/juseless/datagrabbers/ucla.py +111 -104
  30. junifer/configs/juseless/datagrabbers/ukb_vbm.py +19 -25
  31. junifer/configs/py.typed +0 -0
  32. junifer/conftest.py +63 -24
  33. junifer/data/_dispatch.py +9 -19
  34. junifer/data/coordinates/_ants_coordinates_warper.py +4 -1
  35. junifer/data/coordinates/_coordinates.py +51 -6
  36. junifer/data/coordinates/_fsl_coordinates_warper.py +4 -1
  37. junifer/data/coordinates/tests/test_coordinates.py +15 -3
  38. junifer/data/coordinates/tests/test_coordinates_names.py +37 -0
  39. junifer/data/maps/_ants_maps_warper.py +18 -12
  40. junifer/data/maps/_fsl_maps_warper.py +7 -3
  41. junifer/data/maps/_maps.py +12 -8
  42. junifer/data/maps/tests/test_maps.py +13 -21
  43. junifer/data/masks/_ants_mask_warper.py +18 -12
  44. junifer/data/masks/_fsl_mask_warper.py +7 -3
  45. junifer/data/masks/_masks.py +15 -12
  46. junifer/data/masks/tests/test_masks.py +20 -17
  47. junifer/data/parcellations/_ants_parcellation_warper.py +18 -12
  48. junifer/data/parcellations/_fsl_parcellation_warper.py +7 -5
  49. junifer/data/parcellations/_parcellations.py +125 -91
  50. junifer/data/parcellations/tests/test_parcellations.py +122 -1
  51. junifer/data/template_spaces.py +117 -50
  52. junifer/data/tests/test_template_spaces.py +10 -7
  53. junifer/data/utils.py +10 -8
  54. junifer/datagrabber/__init__.pyi +38 -5
  55. junifer/datagrabber/aomic/__init__.pyi +8 -1
  56. junifer/datagrabber/aomic/_types.py +22 -0
  57. junifer/datagrabber/aomic/id1000.py +193 -183
  58. junifer/datagrabber/aomic/piop1.py +215 -259
  59. junifer/datagrabber/aomic/piop2.py +211 -244
  60. junifer/datagrabber/aomic/tests/test_id1000.py +70 -53
  61. junifer/datagrabber/aomic/tests/test_piop1.py +98 -102
  62. junifer/datagrabber/aomic/tests/test_piop2.py +88 -85
  63. junifer/datagrabber/base.py +253 -79
  64. junifer/datagrabber/datalad_base.py +127 -76
  65. junifer/datagrabber/dmcc13_benchmark.py +204 -289
  66. junifer/datagrabber/hcp1200/__init__.pyi +7 -2
  67. junifer/datagrabber/hcp1200/datalad_hcp1200.py +61 -40
  68. junifer/datagrabber/hcp1200/hcp1200.py +151 -164
  69. junifer/datagrabber/hcp1200/tests/test_hcp1200.py +116 -128
  70. junifer/datagrabber/multiple.py +183 -63
  71. junifer/datagrabber/pattern.py +358 -227
  72. junifer/datagrabber/pattern_datalad.py +84 -124
  73. junifer/datagrabber/pattern_validation_mixin.py +22 -50
  74. junifer/datagrabber/tests/test_base.py +17 -4
  75. junifer/datagrabber/tests/test_datalad_base.py +20 -27
  76. junifer/datagrabber/tests/test_dmcc13_benchmark.py +110 -122
  77. junifer/datagrabber/tests/test_multiple.py +293 -31
  78. junifer/datagrabber/tests/test_pattern.py +577 -18
  79. junifer/datagrabber/tests/test_pattern_datalad.py +177 -107
  80. junifer/datagrabber/tests/test_pattern_validation_mixin.py +9 -51
  81. junifer/datareader/default.py +8 -5
  82. junifer/datareader/tests/test_default_reader.py +1 -1
  83. junifer/external/BrainPrint/.codecov.yml +2 -0
  84. junifer/external/BrainPrint/.codespellignore +2 -0
  85. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/bug-report.md +45 -0
  86. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/documentation.md +12 -0
  87. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/feature-request.md +24 -0
  88. junifer/external/BrainPrint/.github/ISSUE_TEMPLATE/questions-help-support.md +28 -0
  89. junifer/external/BrainPrint/.github/dependabot.yml +6 -0
  90. junifer/external/BrainPrint/.github/workflows/build.yml +53 -0
  91. junifer/external/BrainPrint/.github/workflows/code-style.yml +44 -0
  92. junifer/external/BrainPrint/.github/workflows/doc.yml +65 -0
  93. junifer/external/BrainPrint/.github/workflows/publish.yml +31 -0
  94. junifer/external/BrainPrint/.github/workflows/pytest.yml +54 -0
  95. junifer/external/BrainPrint/.gitignore +123 -0
  96. junifer/external/BrainPrint/LICENSE +21 -0
  97. junifer/external/BrainPrint/README.md +108 -0
  98. junifer/external/BrainPrint/brainprint/cli/__init__.py +11 -0
  99. junifer/external/BrainPrint/brainprint/cli/help_text.py +169 -0
  100. junifer/external/BrainPrint/brainprint/cli/parser.py +150 -0
  101. junifer/external/BrainPrint/brainprint/cli/utils.py +14 -0
  102. junifer/external/BrainPrint/brainprint/commands/__init__.py +0 -0
  103. junifer/external/BrainPrint/brainprint/commands/sys_info.py +18 -0
  104. junifer/external/BrainPrint/brainprint/utils/tests/__init__.py +0 -0
  105. junifer/external/BrainPrint/brainprint/utils/tests/test_config.py +33 -0
  106. junifer/external/BrainPrint/doc/Makefile +20 -0
  107. junifer/external/BrainPrint/doc/_static/.gitkeep +0 -0
  108. junifer/external/BrainPrint/doc/_templates/autosummary/class.rst +10 -0
  109. junifer/external/BrainPrint/doc/_templates/autosummary/function.rst +8 -0
  110. junifer/external/BrainPrint/doc/_templates/autosummary/module.rst +6 -0
  111. junifer/external/BrainPrint/doc/api/index.rst +11 -0
  112. junifer/external/BrainPrint/doc/conf.py +272 -0
  113. junifer/external/BrainPrint/doc/index.rst +140 -0
  114. junifer/external/BrainPrint/doc/links.inc +36 -0
  115. junifer/external/BrainPrint/doc/make.bat +35 -0
  116. junifer/external/BrainPrint/doc/references.bib +23 -0
  117. junifer/external/BrainPrint/pyproject.toml +170 -0
  118. junifer/external/BrainPrint/setup.py +3 -0
  119. junifer/external/__init__.py +5 -0
  120. junifer/external/__init__.pyi +0 -0
  121. junifer/external/nilearn/junifer_connectivity_measure.py +95 -39
  122. junifer/external/nilearn/junifer_nifti_spheres_masker.py +226 -67
  123. junifer/external/nilearn/tests/test_junifer_connectivity_measure.py +39 -27
  124. junifer/external/nilearn/tests/test_junifer_nifti_spheres_masker.py +231 -21
  125. junifer/external/py.typed +0 -0
  126. junifer/markers/__init__.pyi +6 -3
  127. junifer/markers/base.py +126 -76
  128. junifer/markers/brainprint.py +30 -39
  129. junifer/markers/complexity/__init__.py +20 -3
  130. junifer/markers/complexity/complexity_base.py +40 -41
  131. junifer/markers/complexity/hurst_exponent.py +22 -35
  132. junifer/markers/complexity/multiscale_entropy_auc.py +53 -41
  133. junifer/markers/complexity/perm_entropy.py +22 -36
  134. junifer/markers/complexity/range_entropy.py +22 -35
  135. junifer/markers/complexity/range_entropy_auc.py +27 -36
  136. junifer/markers/complexity/sample_entropy.py +22 -36
  137. junifer/markers/complexity/tests/test_complexity_registry.py +48 -0
  138. junifer/markers/complexity/tests/test_hurst_exponent.py +47 -36
  139. junifer/markers/complexity/tests/test_multiscale_entropy_auc.py +50 -36
  140. junifer/markers/complexity/tests/test_perm_entropy.py +48 -36
  141. junifer/markers/complexity/tests/test_range_entropy.py +47 -36
  142. junifer/markers/complexity/tests/test_range_entropy_auc.py +47 -36
  143. junifer/markers/complexity/tests/test_sample_entropy.py +48 -36
  144. junifer/markers/complexity/tests/test_weighted_perm_entropy.py +50 -36
  145. junifer/markers/complexity/weighted_perm_entropy.py +22 -36
  146. junifer/markers/ets_rss.py +35 -31
  147. junifer/markers/falff/__init__.pyi +7 -1
  148. junifer/markers/falff/_afni_falff.py +22 -9
  149. junifer/markers/falff/_junifer_falff.py +47 -31
  150. junifer/markers/falff/falff_base.py +80 -60
  151. junifer/markers/falff/falff_maps.py +17 -41
  152. junifer/markers/falff/falff_parcels.py +26 -51
  153. junifer/markers/falff/falff_spheres.py +34 -61
  154. junifer/markers/falff/tests/test_falff_maps.py +20 -23
  155. junifer/markers/falff/tests/test_falff_mask.py +99 -0
  156. junifer/markers/falff/tests/test_falff_parcels.py +14 -10
  157. junifer/markers/falff/tests/test_falff_spheres.py +14 -10
  158. junifer/markers/functional_connectivity/crossparcellation_functional_connectivity.py +32 -34
  159. junifer/markers/functional_connectivity/edge_functional_connectivity_maps.py +12 -25
  160. junifer/markers/functional_connectivity/edge_functional_connectivity_parcels.py +19 -34
  161. junifer/markers/functional_connectivity/edge_functional_connectivity_spheres.py +24 -44
  162. junifer/markers/functional_connectivity/functional_connectivity_base.py +50 -40
  163. junifer/markers/functional_connectivity/functional_connectivity_maps.py +12 -25
  164. junifer/markers/functional_connectivity/functional_connectivity_parcels.py +19 -34
  165. junifer/markers/functional_connectivity/functional_connectivity_spheres.py +24 -45
  166. junifer/markers/functional_connectivity/tests/test_crossparcellation_functional_connectivity.py +21 -22
  167. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_maps.py +8 -7
  168. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_parcels.py +8 -4
  169. junifer/markers/functional_connectivity/tests/test_edge_functional_connectivity_spheres.py +8 -4
  170. junifer/markers/functional_connectivity/tests/test_functional_connectivity_maps.py +8 -7
  171. junifer/markers/functional_connectivity/tests/test_functional_connectivity_parcels.py +9 -5
  172. junifer/markers/functional_connectivity/tests/test_functional_connectivity_spheres.py +8 -12
  173. junifer/markers/maps_aggregation.py +77 -55
  174. junifer/markers/parcel_aggregation.py +91 -70
  175. junifer/markers/reho/__init__.pyi +7 -1
  176. junifer/markers/reho/_afni_reho.py +23 -14
  177. junifer/markers/reho/_junifer_reho.py +112 -123
  178. junifer/markers/reho/reho_base.py +115 -37
  179. junifer/markers/reho/reho_maps.py +18 -32
  180. junifer/markers/reho/reho_parcels.py +26 -41
  181. junifer/markers/reho/reho_spheres.py +33 -54
  182. junifer/markers/reho/tests/test_junifer_reho.py +178 -0
  183. junifer/markers/reho/tests/test_reho_maps.py +27 -25
  184. junifer/markers/reho/tests/test_reho_parcels.py +28 -22
  185. junifer/markers/reho/tests/test_reho_spheres.py +16 -12
  186. junifer/markers/sphere_aggregation.py +88 -72
  187. junifer/markers/temporal_snr/temporal_snr_base.py +32 -30
  188. junifer/markers/temporal_snr/temporal_snr_maps.py +9 -18
  189. junifer/markers/temporal_snr/temporal_snr_parcels.py +19 -27
  190. junifer/markers/temporal_snr/temporal_snr_spheres.py +27 -41
  191. junifer/markers/temporal_snr/tests/test_temporal_snr_base.py +2 -2
  192. junifer/markers/temporal_snr/tests/test_temporal_snr_maps.py +17 -22
  193. junifer/markers/temporal_snr/tests/test_temporal_snr_parcels.py +11 -3
  194. junifer/markers/temporal_snr/tests/test_temporal_snr_spheres.py +9 -9
  195. junifer/markers/tests/test_brainprint.py +10 -9
  196. junifer/markers/tests/test_ets_rss.py +7 -3
  197. junifer/markers/tests/test_lost_regions.py +279 -0
  198. junifer/markers/tests/test_maps_aggregation.py +67 -114
  199. junifer/markers/tests/test_marker_utils.py +40 -1
  200. junifer/markers/tests/test_markers_base.py +275 -6
  201. junifer/markers/tests/test_parcel_aggregation.py +91 -53
  202. junifer/markers/tests/test_sphere_aggregation.py +53 -47
  203. junifer/markers/utils.py +267 -5
  204. junifer/onthefly/__init__.py +2 -3
  205. junifer/onthefly/__init__.pyi +4 -0
  206. junifer/onthefly/_brainprint.py +3 -5
  207. junifer/onthefly/py.typed +0 -0
  208. junifer/onthefly/read_transform.py +11 -9
  209. junifer/onthefly/tests/test_read_transform.py +7 -7
  210. junifer/pipeline/__init__.pyi +4 -2
  211. junifer/pipeline/marker_collection.py +14 -10
  212. junifer/pipeline/pipeline_component_registry.py +25 -6
  213. junifer/pipeline/pipeline_step_mixin.py +5 -12
  214. junifer/pipeline/tests/test_data_object_dumper.py +10 -10
  215. junifer/pipeline/tests/test_marker_collection.py +145 -168
  216. junifer/pipeline/tests/test_pipeline_component_registry.py +52 -8
  217. junifer/pipeline/tests/test_pipeline_step_mixin.py +12 -26
  218. junifer/pipeline/tests/test_update_meta_mixin.py +4 -5
  219. junifer/pipeline/tests/test_workdir_manager.py +39 -0
  220. junifer/pipeline/update_meta_mixin.py +13 -10
  221. junifer/pipeline/utils.py +22 -17
  222. junifer/pipeline/workdir_manager.py +41 -32
  223. junifer/preprocess/__init__.pyi +9 -4
  224. junifer/preprocess/_temporal_filter.py +34 -33
  225. junifer/preprocess/_temporal_slicer.py +17 -30
  226. junifer/preprocess/base.py +99 -55
  227. junifer/preprocess/confounds/__init__.pyi +10 -2
  228. junifer/preprocess/confounds/fmriprep_confound_remover.py +87 -109
  229. junifer/preprocess/confounds/tests/test_fmriprep_confound_remover.py +64 -45
  230. junifer/preprocess/smoothing/__init__.pyi +2 -2
  231. junifer/preprocess/smoothing/_afni_smoothing.py +8 -7
  232. junifer/preprocess/smoothing/_fsl_smoothing.py +6 -4
  233. junifer/preprocess/smoothing/_nilearn_smoothing.py +7 -5
  234. junifer/preprocess/smoothing/smoothing.py +59 -40
  235. junifer/preprocess/smoothing/tests/test_smoothing.py +28 -8
  236. junifer/preprocess/tests/test_preprocess_base.py +17 -9
  237. junifer/preprocess/tests/test_temporal_filter.py +37 -16
  238. junifer/preprocess/tests/test_temporal_slicer.py +12 -12
  239. junifer/preprocess/warping/__init__.pyi +2 -2
  240. junifer/preprocess/warping/_ants_warper.py +56 -40
  241. junifer/preprocess/warping/_fsl_warper.py +49 -36
  242. junifer/preprocess/warping/_utils.py +106 -0
  243. junifer/preprocess/warping/space_warper.py +99 -98
  244. junifer/preprocess/warping/tests/test_space_warper.py +46 -41
  245. junifer/preprocess/warping/tests/test_warping_utils.py +163 -0
  246. junifer/stats.py +46 -34
  247. junifer/storage/__init__.pyi +8 -4
  248. junifer/storage/base.py +129 -64
  249. junifer/storage/hdf5.py +307 -199
  250. junifer/storage/pandas_base.py +46 -24
  251. junifer/storage/sqlite.py +99 -62
  252. junifer/storage/tests/data/collected_h5io_fork.hdf5 +0 -0
  253. junifer/storage/tests/test_hdf5.py +147 -12
  254. junifer/storage/tests/test_repeated_storage.py +293 -0
  255. junifer/storage/tests/test_sqlite.py +9 -20
  256. junifer/storage/tests/test_storage_base.py +18 -21
  257. junifer/storage/tests/test_utils.py +83 -22
  258. junifer/storage/utils.py +58 -38
  259. junifer/testing/__init__.pyi +2 -2
  260. junifer/testing/datagrabbers.py +480 -44
  261. junifer/testing/registry.py +12 -0
  262. junifer/testing/tests/test_adhdtesting_datagrabber.py +24 -0
  263. junifer/testing/tests/test_oasisvmbtesting_datagrabber.py +5 -0
  264. junifer/testing/tests/test_partlycloudytesting_datagrabber.py +52 -0
  265. junifer/testing/tests/test_spmauditory_datagrabber.py +16 -0
  266. junifer/testing/tests/test_testing_registry.py +31 -0
  267. junifer/testing/tests/test_testing_utils.py +35 -0
  268. junifer/testing/tests/test_uclacnpvbmtesting_datagrabber.py +81 -0
  269. junifer/testing/utils.py +33 -1
  270. junifer/tests/test_stats.py +1 -5
  271. junifer/typing/__init__.pyi +2 -0
  272. junifer/typing/_typing.py +34 -19
  273. junifer/typing/py.typed +0 -0
  274. junifer/utils/__init__.pyi +10 -1
  275. junifer/utils/_config.py +1 -2
  276. junifer/utils/helpers.py +68 -2
  277. junifer/utils/logging.py +31 -7
  278. junifer/utils/tests/test_helpers.py +36 -1
  279. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/METADATA +43 -35
  280. junifer-0.0.8.dist-info/RECORD +402 -0
  281. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/WHEEL +1 -1
  282. junifer/cli/tests/test_parser.py +0 -280
  283. junifer/external/h5io/h5io/__init__.py +0 -15
  284. junifer/external/h5io/h5io/_h5io.py +0 -767
  285. junifer/external/h5io/h5io/_version.py +0 -1
  286. junifer/external/h5io/h5io/chunked_array.py +0 -94
  287. junifer/external/h5io/h5io/chunked_list.py +0 -65
  288. junifer/markers/complexity/__init__.pyi +0 -17
  289. junifer-0.0.7.dev332.dist-info/RECORD +0 -353
  290. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/entry_points.txt +0 -0
  291. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/licenses/AUTHORS.rst +0 -0
  292. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/licenses/LICENSE.md +0 -0
  293. {junifer-0.0.7.dev332.dist-info → junifer-0.0.8.dist-info}/top_level.txt +0 -0
@@ -7,14 +7,41 @@
7
7
  # Synchon Mandal <s.mandal@fz-juelich.de>
8
8
  # License: AGPL
9
9
 
10
- from typing import Optional, Union
10
+ from collections.abc import Iterator
11
+ from pathlib import Path
11
12
 
12
13
  import pytest
14
+ from pydantic import AnyUrl
13
15
 
14
16
  from junifer.datagrabber import DataladAOMICPIOP2
17
+ from junifer.testing import config_override
15
18
 
16
19
 
17
- URI = "https://gin.g-node.org/juaml/datalad-example-aomicpiop2"
20
+ URI = AnyUrl(
21
+ "https://cerebra.fz-juelich.de/junifer/datalad-example-aomicpiop2"
22
+ )
23
+
24
+
25
+ @pytest.fixture(scope="module")
26
+ def aomic_datadir(tmp_path_factory) -> Iterator[Path]:
27
+ """Return the path to the AOMIC data directory.
28
+
29
+ Parameters
30
+ ----------
31
+ tmp_path_factory : pathlib.Path
32
+ The path to the test directory.
33
+
34
+ Returns
35
+ -------
36
+ pathlib.Path
37
+ The path to the AOMIC data directory.
38
+
39
+ """
40
+ datadir = tmp_path_factory.mktemp("aomic_data")
41
+ datadir.mkdir(parents=True, exist_ok=True)
42
+ dg = DataladAOMICPIOP2(uri=URI, types=["T1w"], datadir=datadir)
43
+ with config_override("datagrabber.skipidcheck", True), dg:
44
+ yield datadir
18
45
 
19
46
 
20
47
  @pytest.mark.parametrize(
@@ -23,18 +50,11 @@ URI = "https://gin.g-node.org/juaml/datalad-example-aomicpiop2"
23
50
  (
24
51
  "BOLD",
25
52
  ["confounds", "mask", "reference"],
26
- None,
53
+ "restingstate",
27
54
  "MNI152NLin2009cAsym",
28
55
  ),
29
- ("BOLD", ["confounds", "mask", "reference"], None, "native"),
30
56
  (
31
- "BOLD",
32
- ["confounds", "mask", "reference"],
33
- ["restingstate"],
34
- "MNI152NLin2009cAsym",
35
- ),
36
- (
37
- "BOLD",
57
+ ["BOLD"],
38
58
  ["confounds", "mask", "reference"],
39
59
  ["restingstate", "stopsignal"],
40
60
  "MNI152NLin2009cAsym",
@@ -46,72 +66,79 @@ URI = "https://gin.g-node.org/juaml/datalad-example-aomicpiop2"
46
66
  "MNI152NLin2009cAsym",
47
67
  ),
48
68
  (
49
- "BOLD",
69
+ ["BOLD"],
50
70
  ["confounds", "mask", "reference"],
51
- ["workingmemory"],
71
+ "workingmemory",
52
72
  "MNI152NLin2009cAsym",
53
73
  ),
54
- ("T1w", ["mask"], None, "MNI152NLin2009cAsym"),
55
- ("T1w", ["mask"], None, "native"),
56
- ("VBM_CSF", None, None, "MNI152NLin2009cAsym"),
57
- ("VBM_CSF", None, None, "native"),
58
- ("VBM_GM", None, None, "MNI152NLin2009cAsym"),
59
- ("VBM_GM", None, None, "native"),
60
- ("VBM_WM", None, None, "MNI152NLin2009cAsym"),
61
- ("VBM_WM", None, None, "native"),
62
- ("DWI", None, None, "MNI152NLin2009cAsym"),
63
- ("FreeSurfer", None, None, "MNI152NLin2009cAsym"),
74
+ (["T1w"], ["mask"], "restingstate", "MNI152NLin2009cAsym"),
75
+ ("T1w", ["mask"], ["restingstate"], "native"),
76
+ (["VBM_CSF"], None, "restingstate", "MNI152NLin2009cAsym"),
77
+ ("VBM_CSF", None, ["restingstate"], "native"),
78
+ (["VBM_GM"], None, "restingstate", "MNI152NLin2009cAsym"),
79
+ ("VBM_GM", None, ["restingstate"], "native"),
80
+ (["VBM_WM"], None, "restingstate", "MNI152NLin2009cAsym"),
81
+ ("VBM_WM", None, ["restingstate"], "native"),
82
+ (["DWI"], None, "restingstate", "MNI152NLin2009cAsym"),
83
+ (["FreeSurfer"], None, ["restingstate"], "MNI152NLin2009cAsym"),
64
84
  ],
65
85
  )
66
86
  def test_DataladAOMICPIOP2(
67
- type_: str,
68
- nested_types: Optional[list[str]],
69
- tasks: Optional[list[str]],
87
+ type_: str | list[str],
88
+ nested_types: list[str] | None,
89
+ tasks: str | list[str],
70
90
  space: str,
91
+ aomic_datadir: Path,
71
92
  ) -> None:
72
93
  """Test DataladAOMICPIOP2 DataGrabber.
73
94
 
74
95
  Parameters
75
96
  ----------
76
- type_ : str
97
+ type_ : str or list of str
77
98
  The parametrized type.
78
99
  nested_types : list of str or None
79
100
  The parametrized nested types.
80
- tasks : list of str or None
101
+ tasks : str or list of str
81
102
  The parametrized task values.
82
103
  space: str
83
104
  The parametrized space.
105
+ aomic_datadir: Path
106
+ The path to the AOMIC data directory.
84
107
 
85
108
  """
86
- dg = DataladAOMICPIOP2(types=type_, tasks=tasks, space=space)
87
- # Set URI to Gin
88
- dg.uri = URI
89
-
109
+ dg = DataladAOMICPIOP2(
110
+ uri=URI, types=type_, tasks=tasks, space=space, datadir=aomic_datadir
111
+ )
90
112
  with dg:
91
- # Get all elements
92
113
  all_elements = dg.get_elements()
93
- # Get test element
94
114
  test_element = all_elements[0]
95
- # Get test element data
96
115
  out = dg[test_element]
116
+ # Elements only have the subject if no data type of the tasks is
117
+ # grabbed
118
+ if not isinstance(test_element, tuple):
119
+ test_element = (test_element,)
97
120
  # Assert data type
98
- assert type_ in out
99
- # Check task name if BOLD
100
- if type_ == "BOLD" and tasks is not None:
101
- assert test_element[1] in out[type_]["path"].name
102
- assert out[type_]["path"].exists()
103
- assert out[type_]["path"].is_file()
104
- # Asserts data type metadata
105
- assert "meta" in out[type_]
106
- meta = out[type_]["meta"]
107
- assert "element" in meta
108
- assert "subject" in meta["element"]
109
- assert test_element[0] == meta["element"]["subject"]
110
- # Assert nested data type if not None
111
- if nested_types is not None:
112
- for nested_type in nested_types:
113
- assert out[type_][nested_type]["path"].exists()
114
- assert out[type_][nested_type]["path"].is_file()
121
+ if isinstance(type_, str):
122
+ type_ = [type_]
123
+ for t in type_:
124
+ assert t in out
125
+ # Check task name if BOLD
126
+ if t == "BOLD":
127
+ assert test_element[1] in out[t]["path"].name
128
+ assert out[t]["path"].exists()
129
+ assert out[t]["path"].is_file()
130
+ # Asserts data type metadata
131
+ assert "meta" in out[t]
132
+ meta = out[t]["meta"]
133
+ assert "element" in meta
134
+ assert "subject" in meta["element"]
135
+ assert test_element[0] == meta["element"]["subject"]
136
+ assert list(meta["element"]) == dg.get_element_keys()
137
+ # Assert nested data type if not None
138
+ if nested_types is not None:
139
+ for nested_type in nested_types:
140
+ assert out[t][nested_type]["path"].exists()
141
+ assert out[t][nested_type]["path"].is_file()
115
142
 
116
143
 
117
144
  @pytest.mark.parametrize(
@@ -130,7 +157,8 @@ def test_DataladAOMICPIOP2(
130
157
  ],
131
158
  )
132
159
  def test_DataladAOMICPIOP2_partial_data_access(
133
- types: Union[str, list[str]],
160
+ types: str | list[str],
161
+ aomic_datadir: Path,
134
162
  ) -> None:
135
163
  """Test DataladAOMICPIOP2 DataGrabber partial data access.
136
164
 
@@ -138,42 +166,17 @@ def test_DataladAOMICPIOP2_partial_data_access(
138
166
  ----------
139
167
  types : str or list of str
140
168
  The parametrized types.
169
+ aomic_datadir : pathlib.Path
170
+ The path to the AOMIC data directory.
141
171
 
142
172
  """
143
- dg = DataladAOMICPIOP2(types=types)
144
- # Set URI to Gin
145
- dg.uri = URI
146
-
173
+ dg = DataladAOMICPIOP2(uri=URI, types=types, datadir=aomic_datadir)
147
174
  with dg:
148
- # Get all elements
149
175
  all_elements = dg.get_elements()
150
- # Get test element
151
176
  test_element = all_elements[0]
152
- # Get test element data
153
177
  out = dg[test_element]
154
178
  # Assert data type
155
- if isinstance(types, list):
156
- for type_ in types:
157
- assert type_ in out
158
- else:
159
- assert types in out
160
-
161
-
162
- def test_DataladAOMICPIOP2_incorrect_data_type() -> None:
163
- """Test DataladAOMICPIOP2 DataGrabber incorrect data type."""
164
- with pytest.raises(
165
- ValueError, match="`patterns` must contain all `types`"
166
- ):
167
- _ = DataladAOMICPIOP2(types="Vesta")
168
-
169
-
170
- def test_DataladAOMICPIOP2_invalid_tasks():
171
- """Test DataladAOMICIDPIOP2 DataGrabber invalid tasks."""
172
- with pytest.raises(
173
- ValueError,
174
- match=(
175
- "thisisnotarealtask is not a valid task in "
176
- "the AOMIC PIOP2 dataset!"
177
- ),
178
- ):
179
- DataladAOMICPIOP2(tasks="thisisnotarealtask")
179
+ if isinstance(types, str):
180
+ types = [types]
181
+ for t_ in types:
182
+ assert t_ in out
@@ -7,57 +7,140 @@
7
7
 
8
8
  from abc import ABC, abstractmethod
9
9
  from collections.abc import Iterator
10
+ from enum import Enum
10
11
  from pathlib import Path
11
- from typing import Union
12
+ from typing import Annotated, Any
13
+
14
+ import structlog
15
+ from aenum import Enum as AEnum
16
+ from pydantic import BaseModel, BeforeValidator, ConfigDict
12
17
 
13
18
  from ..pipeline import UpdateMetaMixin
14
19
  from ..typing import Element, Elements
15
- from ..utils import logger, raise_error
20
+ from ..utils import ensure_list, raise_error
16
21
 
17
22
 
18
- __all__ = ["BaseDataGrabber"]
23
+ __all__ = ["BaseDataGrabber", "DataType"]
19
24
 
25
+ _log = structlog.get_logger("junifer")
26
+ logger = _log.bind(pkg="datagrabber", step="datagrabber")
20
27
 
21
- class BaseDataGrabber(ABC, UpdateMetaMixin):
22
- """Abstract base class for DataGrabber.
23
28
 
24
- For every interface that is required, one needs to provide a concrete
25
- implementation of this abstract class.
29
+ class DataType(str, AEnum):
30
+ """Accepted data type."""
31
+
32
+ T1w = "T1w"
33
+ T2w = "T2w"
34
+ BOLD = "BOLD"
35
+ Warp = "Warp"
36
+ VBM_GM = "VBM_GM"
37
+ VBM_WM = "VBM_WM"
38
+ VBM_CSF = "VBM_CSF"
39
+ FALFF = "fALFF"
40
+ GCOR = "GCOR"
41
+ LCOR = "LCOR"
42
+ DWI = "DWI"
43
+ FreeSurfer = "FreeSurfer"
44
+
45
+
46
+ def _matches(element: Element, selector: Element) -> bool:
47
+ """Check whether an element matches a selector.
48
+
49
+ Parameters
50
+ ----------
51
+ element : ``Element``
52
+ The element.
53
+ selector : ``Element``
54
+ The partial or complete element selector: a value (the element must
55
+ have it) or a tuple of values (the element must have all of them).
56
+
57
+ Returns
58
+ -------
59
+ bool
60
+ Whether the element matches the selector.
61
+
62
+ """
63
+ if not isinstance(element, tuple):
64
+ element = (element,)
65
+ if isinstance(selector, tuple):
66
+ return set(selector).issubset(element)
67
+ return selector in element
68
+
69
+
70
+ def _selectors_as_tuples(selection: Elements) -> list[tuple]:
71
+ """Get the element selectors as tuples, checking they have the same size.
26
72
 
27
73
  Parameters
28
74
  ----------
29
- types : list of str
30
- The types of data to be grabbed.
31
- datadir : str or pathlib.Path
32
- The directory where the data is / will be stored.
75
+ selection : ``Elements``
76
+ The list of partial or complete element selectors.
77
+
78
+ Returns
79
+ -------
80
+ list of tuple
81
+ The selectors, as tuples (a single value is a tuple of one value).
33
82
 
34
83
  Raises
35
84
  ------
36
- TypeError
37
- If ``types`` is not a list or if the values are not string.
85
+ ValueError
86
+ If the selectors have different numbers of values.
38
87
 
39
88
  """
89
+ selectors = [s if isinstance(s, tuple) else (s,) for s in selection]
90
+ if len({len(selector) for selector in selectors}) > 1:
91
+ raise_error(
92
+ msg=(
93
+ "The element selectors must have the same number of values: "
94
+ f"{selection}"
95
+ ),
96
+ klass=ValueError,
97
+ )
98
+ return selectors
40
99
 
41
- def __init__(self, types: list[str], datadir: Union[str, Path]) -> None:
42
- # Validate types
43
- if not isinstance(types, list):
44
- raise_error(msg="`types` must be a list", klass=TypeError)
45
- if any(not isinstance(x, str) for x in types):
46
- raise_error(
47
- msg="`types` must be a list of strings", klass=TypeError
48
- )
49
- self.types = types
50
100
 
51
- # Convert str to Path
52
- if not isinstance(datadir, Path):
53
- datadir = Path(datadir)
54
- self._datadir = datadir
101
+ class BaseDataGrabber(BaseModel, ABC, UpdateMetaMixin):
102
+ """Abstract base class for data fetcher.
103
+
104
+ For every datagrabber, one needs to provide a concrete
105
+ implementation of this abstract class.
106
+
107
+ Parameters
108
+ ----------
109
+ types : :enum:`.DataType` or list of variants
110
+ The data type(s) to grab.
111
+ datadir : pathlib.Path
112
+ The path where the data is or will be stored.
113
+
114
+ """
55
115
 
56
- logger.debug("Initializing BaseDataGrabber")
57
- logger.debug(f"\t_datadir = {datadir}")
58
- logger.debug(f"\ttypes = {types}")
116
+ model_config = ConfigDict(extra="forbid", use_enum_values=True)
117
+
118
+ types: Annotated[
119
+ DataType | list[DataType],
120
+ BeforeValidator(ensure_list),
121
+ ]
122
+ datadir: Path
123
+
124
+ def model_post_init(self, context: Any): # noqa: D102
125
+ # Run extra validation for datagrabbers and fail early if needed
126
+ self.validate_datagrabber_params()
127
+ # Convert to correct data type
128
+ self.types = [
129
+ DataType(t) if isinstance(t, str) else t for t in self.types
130
+ ]
131
+ logger.info(
132
+ f"Parameters: {self.model_dump(mode='json')}",
133
+ component=self.__class__.__name__,
134
+ )
135
+
136
+ def validate_datagrabber_params(self) -> None:
137
+ """Run extra logical validation for datagrabber.
138
+
139
+ Subclasses can override to provide validation.
140
+ """
141
+ pass
59
142
 
60
- def __iter__(self) -> Iterator:
143
+ def __iter__(self) -> Iterator[Elements]:
61
144
  """Enable iterable support.
62
145
 
63
146
  Yields
@@ -73,7 +156,7 @@ class BaseDataGrabber(ABC, UpdateMetaMixin):
73
156
 
74
157
  Parameters
75
158
  ----------
76
- element : str or tuple of str
159
+ element : `Element`
77
160
  The element to be indexed.
78
161
 
79
162
  Returns
@@ -83,28 +166,66 @@ class BaseDataGrabber(ABC, UpdateMetaMixin):
83
166
  specified element.
84
167
 
85
168
  """
169
+ # Convert element to tuple if not already and extract enum values if
170
+ # present
171
+ element = (
172
+ (element,)
173
+ if not isinstance(element, tuple)
174
+ else tuple(i.value if isinstance(i, Enum) else i for i in element)
175
+ )
86
176
  logger.info(f"Getting element {element}")
87
- # Convert element to tuple if not already
88
- if not isinstance(element, tuple):
89
- element = (element,)
90
177
  # Zip through element keys and actual values to construct element
91
178
  # access dictionary
92
- named_element: dict = dict(zip(self.get_element_keys(), element))
179
+ named_element: dict = dict(
180
+ zip(self.get_element_keys(), element, strict=False)
181
+ )
93
182
  logger.debug(f"Named element: {named_element}")
183
+ self._check_element(named_element)
94
184
  # Fetch element
95
185
  out = self.get_item(**named_element)
96
- # Update metadata
97
- for _, t_val in out.items():
186
+ # Update metadata: the element being processed and the keys of the
187
+ # element that the data of each data type depends on
188
+ for t_type, t_val in out.items():
98
189
  self.update_meta(t_val, "datagrabber")
190
+ element_keys = self.get_type_element_keys(t_type)
99
191
  # Conditional for list dtype vals like Warp
100
- if isinstance(t_val, list):
101
- for entry in t_val:
102
- entry["meta"]["element"] = named_element
103
- else:
104
- t_val["meta"]["element"] = named_element
192
+ for entry in t_val if isinstance(t_val, list) else [t_val]:
193
+ entry["meta"]["element"] = named_element
194
+ entry["meta"]["_element_keys"] = element_keys
105
195
 
106
196
  return out
107
197
 
198
+ def get_type_element_keys(self, data_type: str) -> list[str]:
199
+ """Get the element keys that the data of a data type depends on.
200
+
201
+ The element keys (see :meth:`.get_element_keys`) are the keys that
202
+ the data types to grab depend on.
203
+
204
+ Parameters
205
+ ----------
206
+ data_type : str
207
+ The data type.
208
+
209
+ Returns
210
+ -------
211
+ list of str
212
+ The element keys, all of them unless a subclass knows which ones
213
+ the data type depends on (e.g., only the subject for data that is
214
+ the same for all the tasks of a subject).
215
+
216
+ """
217
+ return self.get_element_keys()
218
+
219
+ def _check_element(self, element: dict) -> None:
220
+ """Check the element can be grabbed.
221
+
222
+ Parameters
223
+ ----------
224
+ element : dict
225
+ The element, as given to :meth:`.get_item`.
226
+
227
+ """
228
+
108
229
  def __enter__(self) -> "BaseDataGrabber":
109
230
  """Context entry."""
110
231
  return self
@@ -119,67 +240,120 @@ class BaseDataGrabber(ABC, UpdateMetaMixin):
119
240
  Returns
120
241
  -------
121
242
  list of str
122
- The types of data to be grabbed.
243
+ The data type(s) to grab.
123
244
 
124
245
  """
125
- return self.types.copy()
246
+ return [x.value if isinstance(x, Enum) else x for x in self.types]
126
247
 
127
248
  @property
128
- def datadir(self) -> Path:
129
- """Get data directory path.
249
+ def fulldir(self) -> Path:
250
+ """Get complete data directory path.
130
251
 
131
252
  Returns
132
253
  -------
133
254
  pathlib.Path
134
- Path to the data directory. Can be overridden by subclasses.
255
+ Complete path to the data directory.
256
+ Can be overridden by subclasses.
135
257
 
136
258
  """
137
- return self._datadir
259
+ return self.datadir
138
260
 
139
261
  def filter(self, selection: Elements) -> Iterator:
140
262
  """Filter elements to be grabbed.
141
263
 
142
264
  Parameters
143
265
  ----------
144
- selection : list
145
- The list of partial or complete element selectors to filter using.
266
+ selection : ``Elements``
267
+ The list of partial or complete element selectors to filter using,
268
+ with the same number of values.
146
269
 
147
270
  Yields
148
271
  ------
149
272
  object
150
273
  An element that can be indexed by the DataGrabber.
151
274
 
275
+ Raises
276
+ ------
277
+ ValueError
278
+ If the selectors have different numbers of values.
279
+
152
280
  """
281
+ selectors = _selectors_as_tuples(selection)
282
+ for element in self.get_elements():
283
+ if any(_matches(element, selector) for selector in selectors):
284
+ yield element
285
+
286
+ def select_elements(self, selection: Elements) -> list[Element]:
287
+ """Select the elements to grab.
288
+
289
+ All the selectors must have the same number of values (a single value
290
+ counts as a tuple of one value), so they are either all complete
291
+ elements or all partial selectors:
292
+
293
+ * Complete elements have a value for each element key, in the order of
294
+ :meth:`.get_element_keys`, e.g., ``("sub-01", "rest")`` for the keys
295
+ ``["subject", "task"]``. They are the elements to grab, so the
296
+ elements of the dataset are not needed (they are not searched for).
297
+ Repeated elements are only returned once.
298
+ * Partial selectors have fewer values, e.g., ``"sub-01"`` or
299
+ ``("sub-01",)``. They select the elements of the dataset that match
300
+ them, i.e., that have all their values for any key (see
301
+ :meth:`.filter`). The elements of the dataset are searched for once,
302
+ and each element is returned once, even if several selectors match
303
+ it: with ``["sub-01", "rest"]``, the element ``("sub-01", "rest")``
304
+ matches both selectors and is returned once, together with the other
305
+ tasks of ``sub-01`` and the ``rest`` task of the other subjects.
306
+
307
+ Parameters
308
+ ----------
309
+ selection : ``Elements``
310
+ The list of partial or complete element selectors.
311
+
312
+ Returns
313
+ -------
314
+ list of tuple
315
+ The selected elements, without duplicates.
316
+
317
+ Raises
318
+ ------
319
+ ValueError
320
+ If the selectors have different numbers of values.
321
+ RuntimeError
322
+ If a partial selector does not match any element.
153
323
 
154
- def filter_func(element: Element) -> bool:
155
- """Filter element based on selection.
156
-
157
- Parameters
158
- ----------
159
- element : str or tuple of str
160
- The element to be filtered.
161
-
162
- Returns
163
- -------
164
- bool
165
- If the element passes the filter or not.
166
-
167
- """
168
- # Convert element to tuple
169
- if not isinstance(element, tuple):
170
- element = (element,)
171
- # Filter based on selection kind
172
- if isinstance(selection[0], str):
173
- for opt in selection:
174
- if opt in element:
175
- return True
176
- elif isinstance(selection[0], tuple):
177
- for opt in selection:
178
- if set(opt).issubset(element):
179
- return True
180
- return False
181
-
182
- yield from filter(filter_func, self.get_elements())
324
+ """
325
+ selectors = _selectors_as_tuples(selection)
326
+ if not selectors:
327
+ return []
328
+ # Complete elements: the selectors are the elements, without the
329
+ # repeated ones
330
+ if len(selectors[0]) == len(self.get_element_keys()):
331
+ # Remove the repeated selectors, keeping the order
332
+ return list(dict.fromkeys(selectors))
333
+ # Partial selectors: the elements of the dataset that match them, in a
334
+ # single pass over the elements. Each element is checked once, so it
335
+ # is returned once even if several selectors match it
336
+ selected = []
337
+ matched = set()
338
+ for element in self.get_elements():
339
+ t_matched = [
340
+ i
341
+ for i, selector in enumerate(selectors)
342
+ if _matches(element, selector)
343
+ ]
344
+ if t_matched:
345
+ selected.append(element)
346
+ matched.update(t_matched)
347
+ # The selectors that no element matches
348
+ invalid = [
349
+ given for i, given in enumerate(selection) if i not in matched
350
+ ]
351
+ if invalid:
352
+ raise_error(
353
+ msg=f"The following element selectors are invalid:\n{invalid}",
354
+ klass=RuntimeError,
355
+ )
356
+ return selected
183
357
 
184
358
  @abstractmethod
185
359
  def get_element_keys(self) -> list[str]: