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
@@ -5,16 +5,19 @@
5
5
  # Synchon Mandal <s.mandal@fz-juelich.de>
6
6
  # License: AGPL
7
7
 
8
+ import importlib
8
9
  import logging
10
+ import pkgutil
9
11
  from abc import ABC
10
12
 
11
13
  import pytest
12
14
 
15
+ import junifer
13
16
  from junifer.datagrabber.pattern import PatternDataGrabber
14
17
  from junifer.pipeline.pipeline_component_registry import (
15
18
  PipelineComponentRegistry,
16
19
  )
17
- from junifer.storage import SQLiteFeatureStorage
20
+ from junifer.storage import HDF5FeatureStorage
18
21
 
19
22
 
20
23
  def test_pipeline_component_registry_singleton() -> None:
@@ -56,7 +59,7 @@ def test_pipeline_component_registry_components():
56
59
  "step, klass",
57
60
  [
58
61
  ("datagrabber", PatternDataGrabber),
59
- ("storage", SQLiteFeatureStorage),
62
+ ("storage", HDF5FeatureStorage),
60
63
  ],
61
64
  )
62
65
  def test_pipeline_component_registry_register(
@@ -85,7 +88,7 @@ def test_pipeline_component_registry_register(
85
88
  "step, klass",
86
89
  [
87
90
  ("datagrabber", PatternDataGrabber),
88
- ("storage", SQLiteFeatureStorage),
91
+ ("storage", HDF5FeatureStorage),
89
92
  ],
90
93
  )
91
94
  def test_pipeline_component_registry_deregister(
@@ -103,11 +106,16 @@ def test_pipeline_component_registry_deregister(
103
106
  The parametrized name of the class.
104
107
 
105
108
  """
106
- with caplog.at_level(logging.INFO):
107
- # Register
108
- PipelineComponentRegistry().deregister(step=step, klass=klass)
109
- # Check logging message
110
- assert "De-registering" in caplog.text
109
+ registry = PipelineComponentRegistry()
110
+ try:
111
+ with caplog.at_level(logging.INFO):
112
+ registry.deregister(step=step, klass=klass)
113
+ # Check logging message
114
+ assert "De-registering" in caplog.text
115
+ assert klass.__name__ not in registry.step_components(step)
116
+ finally:
117
+ # Register again, as the registry is shared by all the tests
118
+ registry.register(step=step, klass=klass)
111
119
 
112
120
 
113
121
  def test_pipeline_component_registry_step_components() -> None:
@@ -149,6 +157,42 @@ def test_pipeline_component_registry_get_class():
149
157
  assert isinstance(obj, type(bar))
150
158
 
151
159
 
160
+ def test_pipeline_component_registry_get_class_builtin() -> None:
161
+ """Test get class for the built-in components of the registry."""
162
+ # A new registry, with the built-in components not imported yet (the
163
+ # singleton might have imported some of them)
164
+ registry = object.__new__(PipelineComponentRegistry)
165
+ registry.__init__()
166
+ for step in registry.steps:
167
+ for name in registry.step_components(step=step):
168
+ assert isinstance(registry.components[step][name], str)
169
+ klass = registry.get_class(step=step, name=name)
170
+ assert klass.__name__ == name
171
+ # Import all the components of the sub-packages of the steps, which
172
+ # registers them, and check that they are all built-in components, except
173
+ # the ones with optional dependencies, which are registered by importing
174
+ # their sub-package (e.g., with ``with:`` in the YAML)
175
+ optional = ("junifer.markers.complexity",)
176
+ for module in pkgutil.walk_packages(junifer.__path__, "junifer."):
177
+ if ".tests" in module.name:
178
+ continue
179
+ try:
180
+ importlib.import_module(module.name)
181
+ except ImportError:
182
+ # Only if the optional dependencies are not installed
183
+ assert module.name.startswith(optional)
184
+ for step, subpkg in registry._step_to_subpkg_mappings.items():
185
+ for name, klass in (
186
+ PipelineComponentRegistry().components[step].items()
187
+ ):
188
+ if isinstance(klass, str):
189
+ continue
190
+ if klass.__module__.startswith(optional):
191
+ assert name not in registry.components[step]
192
+ elif klass.__module__.startswith(f"junifer.{subpkg}."):
193
+ assert name in registry.components[step]
194
+
195
+
152
196
  def test_pipeline_component_registry_build():
153
197
  """Test component instance building for PipelineComponentRegistry."""
154
198
  import numpy as np
@@ -9,7 +9,7 @@ from typing import ClassVar
9
9
 
10
10
  import pytest
11
11
 
12
- from junifer.pipeline.pipeline_step_mixin import PipelineStepMixin
12
+ from junifer.pipeline import ExtDep, PipelineStepMixin
13
13
  from junifer.pipeline.utils import _check_afni
14
14
  from junifer.typing import (
15
15
  ConditionalDependencies,
@@ -18,6 +18,9 @@ from junifer.typing import (
18
18
  )
19
19
 
20
20
 
21
+ pytestmark = pytest.mark.external
22
+
23
+
21
24
  def test_PipelineStepMixin_correct_dependencies() -> None:
22
25
  """Test fit-transform with correct dependencies."""
23
26
 
@@ -64,7 +67,9 @@ def test_PipelineStepMixin_correct_ext_dependencies() -> None:
64
67
  class CorrectMixer(PipelineStepMixin):
65
68
  """Test class for validation."""
66
69
 
67
- _EXT_DEPENDENCIES: ClassVar[ExternalDependencies] = [{"name": "afni"}]
70
+ _EXT_DEPENDENCIES: ClassVar[ExternalDependencies] = [
71
+ {"name": ExtDep.AFNI}
72
+ ]
68
73
 
69
74
  def validate_input(self, input: list[str]) -> list[str]:
70
75
  return input
@@ -86,7 +91,7 @@ def test_PipelineStepMixin_ext_deps_correct_commands() -> None:
86
91
  """Test class for validation."""
87
92
 
88
93
  _EXT_DEPENDENCIES: ClassVar[ExternalDependencies] = [
89
- {"name": "afni", "commands": ["3dReHo"]}
94
+ {"name": ExtDep.AFNI, "commands": ["3dReHo"]}
90
95
  ]
91
96
 
92
97
  def validate_input(self, input: list[str]) -> list[str]:
@@ -111,7 +116,7 @@ def test_PipelineStepMixin_ext_deps_incorrect_commands() -> None:
111
116
  """Test class for validation."""
112
117
 
113
118
  _EXT_DEPENDENCIES: ClassVar[ExternalDependencies] = [
114
- {"name": "afni", "commands": ["3d"]}
119
+ {"name": ExtDep.AFNI, "commands": ["3d"]}
115
120
  ]
116
121
 
117
122
  def validate_input(self, input: list[str]) -> list[str]:
@@ -125,27 +130,6 @@ def test_PipelineStepMixin_ext_deps_incorrect_commands() -> None:
125
130
  mixer.fit_transform({})
126
131
 
127
132
 
128
- def test_PipelineStepMixin_incorrect_ext_dependencies() -> None:
129
- """Test fit-transform with incorrect external dependencies."""
130
-
131
- class IncorrectMixer(PipelineStepMixin):
132
- """Test class for validation."""
133
-
134
- _EXT_DEPENDENCIES: ClassVar[ExternalDependencies] = [
135
- {"name": "foobar", "optional": True}
136
- ]
137
-
138
- def validate_input(self, input: list[str]) -> list[str]:
139
- return input
140
-
141
- def _fit_transform(self, input: dict[str, dict]) -> dict[str, dict]:
142
- return {"input": input}
143
-
144
- mixer = IncorrectMixer()
145
- with pytest.raises(ValueError, match="Invalid value"):
146
- mixer.fit_transform({})
147
-
148
-
149
133
  def test_PipelineStepMixin_correct_conditional_dependencies() -> None:
150
134
  """Test fit-transform with correct conditional dependencies."""
151
135
 
@@ -208,7 +192,9 @@ def test_PipelineStepMixin_correct_conditional_ext_dependencies() -> None:
208
192
  """Test fit-transform with correct conditional external dependencies."""
209
193
 
210
194
  class ExternalDependency:
211
- _EXT_DEPENDENCIES: ClassVar[ExternalDependencies] = [{"name": "afni"}]
195
+ _EXT_DEPENDENCIES: ClassVar[ExternalDependencies] = [
196
+ {"name": ExtDep.AFNI}
197
+ ]
212
198
 
213
199
  class CorrectMixer(PipelineStepMixin):
214
200
  """Test class for validation."""
@@ -4,11 +4,10 @@
4
4
  # Synchon Mandal <s.mandal@fz-juelich.de>
5
5
  # License: AGPL
6
6
 
7
- from typing import Union
8
-
9
7
  import pytest
8
+ from pydantic import BaseModel
10
9
 
11
- from junifer.pipeline.update_meta_mixin import UpdateMetaMixin
10
+ from junifer.pipeline import UpdateMetaMixin
12
11
 
13
12
 
14
13
  @pytest.mark.parametrize(
@@ -23,7 +22,7 @@ from junifer.pipeline.update_meta_mixin import UpdateMetaMixin
23
22
  def test_UpdateMetaMixin(
24
23
  input: dict,
25
24
  step_name: str,
26
- dependencies: Union[set, list, str, None],
25
+ dependencies: set | list | str | None,
27
26
  expected: set,
28
27
  ) -> None:
29
28
  """Test UpdateMetaMixin.
@@ -41,7 +40,7 @@ def test_UpdateMetaMixin(
41
40
 
42
41
  """
43
42
 
44
- class TestUpdateMetaMixin(UpdateMetaMixin):
43
+ class TestUpdateMetaMixin(BaseModel, UpdateMetaMixin):
45
44
  """Test UpdateMetaMixin."""
46
45
 
47
46
  _DEPENDENCIES = dependencies
@@ -29,6 +29,21 @@ def test_workdir_manager_workdir(tmp_path: Path) -> None:
29
29
  assert workdir_mgr.workdir == tmp_path
30
30
 
31
31
 
32
+ def test_workdir_manager_create_workdir(tmp_path: Path) -> None:
33
+ """Test WorkDirManager creates the workdir if it does not exist.
34
+
35
+ Parameters
36
+ ----------
37
+ tmp_path : pathlib.Path
38
+ The path to the test directory.
39
+
40
+ """
41
+ workdir = tmp_path / "new" / "workdir"
42
+ workdir_mgr = WorkDirManager(workdir=workdir)
43
+ assert workdir_mgr.workdir == workdir
44
+ assert workdir.is_dir()
45
+
46
+
32
47
  def test_workdir_manager_get_and_delete_element_tempdir(
33
48
  tmp_path: Path,
34
49
  ) -> None:
@@ -80,6 +95,30 @@ def test_workdir_manager_cleanup_elementdir(
80
95
  assert workdir_mgr.elementdir is None
81
96
 
82
97
 
98
+ def test_workdir_manager_get_elementdir(tmp_path: Path) -> None:
99
+ """Test WorkDirManager gets the element directory correctly.
100
+
101
+ Parameters
102
+ ----------
103
+ tmp_path : pathlib.Path
104
+ The path to the test directory.
105
+
106
+ """
107
+ workdir_mgr = WorkDirManager()
108
+ workdir_mgr.workdir = tmp_path
109
+ assert workdir_mgr.elementdir is None
110
+ # Created on the first call, and the same afterwards
111
+ elementdir = workdir_mgr.get_elementdir()
112
+ assert elementdir.is_dir()
113
+ assert elementdir.parent == tmp_path
114
+ assert workdir_mgr.get_elementdir() == elementdir
115
+ # The element temporary directories are in it
116
+ assert workdir_mgr.get_element_tempdir().parent == elementdir
117
+ workdir_mgr.cleanup_elementdir()
118
+ assert not elementdir.exists()
119
+ assert workdir_mgr.elementdir is None
120
+
121
+
83
122
  def test_workdir_manager_get_and_delete_tempdir(tmp_path: Path) -> None:
84
123
  """Test WorkDirManager gets and deletes temporary directories correctly.
85
124
 
@@ -4,9 +4,6 @@
4
4
  # Synchon Mandal <s.mandal@fz-juelich.de>
5
5
  # License: AGPL
6
6
 
7
- from typing import Union
8
-
9
-
10
7
  __all__ = ["UpdateMetaMixin"]
11
8
 
12
9
 
@@ -15,8 +12,9 @@ class UpdateMetaMixin:
15
12
 
16
13
  def update_meta(
17
14
  self,
18
- input: Union[dict, list[dict]],
15
+ input: dict | list[dict],
19
16
  step_name: str,
17
+ append: bool = False,
20
18
  ) -> None:
21
19
  """Update metadata.
22
20
 
@@ -26,16 +24,18 @@ class UpdateMetaMixin:
26
24
  The data object to update.
27
25
  step_name : str
28
26
  The name of the pipeline step.
27
+ append : bool, optional
28
+ Whether to append the metadata of the step to a list, for the
29
+ steps that can be several (e.g., the preprocessors), instead of
30
+ replacing the metadata of the previous step (default False).
29
31
 
30
32
  """
31
33
  # Initialize empty dictionary for the step's metadata
32
34
  t_meta = {}
33
35
  # Set class name for the step
34
36
  t_meta["class"] = self.__class__.__name__
35
- # Add object variables to metadata if name doesn't start with "_"
36
- for k, v in vars(self).items():
37
- if not k.startswith("_"):
38
- t_meta[k] = v
37
+ # Dump model
38
+ t_meta.update(self.model_dump(mode="json"))
39
39
  # Conditional for list dtype vals like Warp
40
40
  if not isinstance(input, list):
41
41
  input = [input]
@@ -44,13 +44,16 @@ class UpdateMetaMixin:
44
44
  if "meta" not in entry:
45
45
  entry["meta"] = {}
46
46
  # Add step name
47
- entry["meta"][step_name] = t_meta
47
+ if append:
48
+ entry["meta"].setdefault(step_name, []).append(t_meta)
49
+ else:
50
+ entry["meta"][step_name] = t_meta
48
51
  # Add step dependencies
49
52
  if "dependencies" not in entry["meta"]:
50
53
  entry["meta"]["dependencies"] = set()
51
54
  # Update step dependencies
52
55
  dependencies = getattr(self, "_DEPENDENCIES", set())
53
56
  if dependencies is not None:
54
- if not isinstance(dependencies, (set, list)):
57
+ if not isinstance(dependencies, set | list):
55
58
  dependencies = {dependencies}
56
59
  entry["meta"]["dependencies"].update(dependencies)
junifer/pipeline/utils.py CHANGED
@@ -5,22 +5,35 @@
5
5
  # License: AGPL
6
6
 
7
7
  import subprocess
8
- from typing import Any, Optional
8
+ from enum import StrEnum
9
+ from typing import Any
9
10
 
10
- from junifer.utils.logging import raise_error, warn_with_log
11
+ from pydantic import validate_call
11
12
 
13
+ from ..utils import raise_error, warn_with_log
12
14
 
13
- __all__ = ["check_ext_dependencies"]
14
15
 
16
+ __all__ = ["ExtDep", "check_ext_dependencies"]
15
17
 
18
+
19
+ class ExtDep(StrEnum):
20
+ """Accepted external dependencies."""
21
+
22
+ AFNI = "afni"
23
+ FSL = "fsl"
24
+ ANTs = "ants"
25
+ FreeSurfer = "freesurfer"
26
+
27
+
28
+ @validate_call
16
29
  def check_ext_dependencies(
17
- name: str, optional: bool = False, **kwargs: Any
30
+ name: ExtDep, optional: bool = False, **kwargs: Any
18
31
  ) -> bool:
19
32
  """Check if external dependency `name` is found if mandatory.
20
33
 
21
34
  Parameters
22
35
  ----------
23
- name : str
36
+ name : :enum:`.ExtDep`
24
37
  The name of the dependency.
25
38
  optional : bool, optional
26
39
  Whether the dependency is optional (default False).
@@ -34,18 +47,10 @@ def check_ext_dependencies(
34
47
 
35
48
  Raises
36
49
  ------
37
- ValueError
38
- If ``name`` is invalid.
39
50
  RuntimeError
40
51
  If ``name`` is mandatory and is not found.
41
52
 
42
53
  """
43
- valid_ext_dependencies = ("afni", "fsl", "ants", "freesurfer")
44
- if name not in valid_ext_dependencies:
45
- raise_error(
46
- "Invalid value for `name`, should be one of: "
47
- f"{valid_ext_dependencies}"
48
- )
49
54
  # Check for afni
50
55
  if name == "afni":
51
56
  found = _check_afni(**kwargs)
@@ -71,7 +76,7 @@ def check_ext_dependencies(
71
76
  return found
72
77
 
73
78
 
74
- def _check_afni(commands: Optional[list[str]] = None) -> bool:
79
+ def _check_afni(commands: list[str] | None = None) -> bool:
75
80
  """Check if AFNI is present in the system.
76
81
 
77
82
  Parameters
@@ -131,7 +136,7 @@ def _check_afni(commands: Optional[list[str]] = None) -> bool:
131
136
  return afni_found
132
137
 
133
138
 
134
- def _check_fsl(commands: Optional[list[str]] = None) -> bool:
139
+ def _check_fsl(commands: list[str] | None = None) -> bool:
135
140
  """Check if FSL is present in the system.
136
141
 
137
142
  Parameters
@@ -195,7 +200,7 @@ def _check_fsl(commands: Optional[list[str]] = None) -> bool:
195
200
  return fsl_found
196
201
 
197
202
 
198
- def _check_ants(commands: Optional[list[str]] = None) -> bool:
203
+ def _check_ants(commands: list[str] | None = None) -> bool:
199
204
  """Check if ANTs is present in the system.
200
205
 
201
206
  Parameters
@@ -255,7 +260,7 @@ def _check_ants(commands: Optional[list[str]] = None) -> bool:
255
260
  return ants_found
256
261
 
257
262
 
258
- def _check_freesurfer(commands: Optional[list[str]] = None) -> bool:
263
+ def _check_freesurfer(commands: list[str] | None = None) -> bool:
259
264
  """Check if FreeSurfer is present in the system.
260
265
 
261
266
  Parameters
@@ -7,14 +7,17 @@
7
7
  import shutil
8
8
  import tempfile
9
9
  from pathlib import Path
10
- from typing import Optional, Union
11
10
 
12
- from ..utils import logger
11
+ import structlog
12
+
13
13
  from ..utils.singleton import Singleton
14
14
 
15
15
 
16
16
  __all__ = ["WorkDirManager"]
17
17
 
18
+ _log = structlog.get_logger("junifer")
19
+ logger = _log.bind(pkg="pipeline")
20
+
18
21
 
19
22
  class WorkDirManager(metaclass=Singleton):
20
23
  """Class for working directory manager.
@@ -46,7 +49,7 @@ class WorkDirManager(metaclass=Singleton):
46
49
  """
47
50
 
48
51
  def __init__(
49
- self, workdir: Optional[Union[str, Path]] = None, cleanup: bool = True
52
+ self, workdir: str | Path | None = None, cleanup: bool = True
50
53
  ) -> None:
51
54
  """Initialize the class."""
52
55
  self._workdir = Path(workdir) if isinstance(workdir, str) else workdir
@@ -60,20 +63,16 @@ class WorkDirManager(metaclass=Singleton):
60
63
  # Check and set topmost level directory if not provided
61
64
  if self._workdir is None:
62
65
  self._workdir = Path(tempfile.gettempdir()) / "junifer"
63
- # Create directory if not found
64
- if not self._workdir.is_dir():
65
- logger.debug(
66
- "Creating working directory at "
67
- f"{self._workdir.resolve()!s}"
68
- )
69
- self._workdir.mkdir(parents=True)
66
+
67
+ # Create directory if not found
68
+ if not self._workdir.is_dir():
70
69
  logger.debug(
71
- f"Setting working directory to {self._workdir.resolve()!s}"
70
+ f"Creating working directory at {self._workdir.resolve()!s}"
72
71
  )
73
-
74
- def __del__(self) -> None:
75
- """Destructor."""
76
- self._cleanup()
72
+ self._workdir.mkdir(parents=True)
73
+ logger.debug(
74
+ f"Setting working directory to {self._workdir.resolve()!s}"
75
+ )
77
76
 
78
77
  def _cleanup(self) -> None:
79
78
  """Clean up the element and temporary directories."""
@@ -97,7 +96,7 @@ class WorkDirManager(metaclass=Singleton):
97
96
  return self._workdir # type: ignore
98
97
 
99
98
  @workdir.setter
100
- def workdir(self, path: Union[str, Path]) -> None:
99
+ def workdir(self, path: str | Path) -> None:
101
100
  """Set working directory.
102
101
 
103
102
  The directory path is created if it doesn't exist yet.
@@ -129,8 +128,28 @@ class WorkDirManager(metaclass=Singleton):
129
128
  """Get element directory."""
130
129
  return self._elementdir # type: ignore
131
130
 
131
+ def get_elementdir(self) -> Path:
132
+ """Get the element directory, creating it if needed.
133
+
134
+ This directory should be available only for the lifetime of an
135
+ element.
136
+
137
+ Returns
138
+ -------
139
+ pathlib.Path
140
+ The path to the element directory.
141
+
142
+ """
143
+ if self._elementdir is None:
144
+ logger.debug(
145
+ "Setting up element directory under "
146
+ f"{self._workdir.resolve()!s}" # type: ignore
147
+ )
148
+ self._elementdir = Path(tempfile.mkdtemp(dir=self._workdir))
149
+ return self._elementdir
150
+
132
151
  def get_element_tempdir(
133
- self, prefix: Optional[str] = None, suffix: Optional[str] = None
152
+ self, prefix: str | None = None, suffix: str | None = None
134
153
  ) -> Path:
135
154
  """Get an element-scoped temporary directory.
136
155
 
@@ -152,22 +171,12 @@ class WorkDirManager(metaclass=Singleton):
152
171
  The path to the temporary directory.
153
172
 
154
173
  """
155
- # Create element directory if not created already
156
- if self._elementdir is None:
157
- logger.debug(
158
- "Setting up element directory under "
159
- f"{self._workdir.resolve()!s}" # type: ignore
160
- )
161
- self._elementdir = Path(tempfile.mkdtemp(dir=self._workdir))
162
-
174
+ elementdir = self.get_elementdir()
163
175
  logger.debug(
164
- "Creating element temporary directory at "
165
- f"{self._elementdir.resolve()!s}"
176
+ f"Creating element temporary directory at {elementdir.resolve()!s}"
166
177
  )
167
178
  return Path(
168
- tempfile.mkdtemp(
169
- dir=self._elementdir, prefix=prefix, suffix=suffix
170
- )
179
+ tempfile.mkdtemp(dir=elementdir, prefix=prefix, suffix=suffix)
171
180
  )
172
181
 
173
182
  def delete_element_tempdir(self, tempdir: Path) -> None:
@@ -203,12 +212,12 @@ class WorkDirManager(metaclass=Singleton):
203
212
  self._elementdir = None
204
213
 
205
214
  @property
206
- def root_tempdir(self) -> Optional[Path]:
215
+ def root_tempdir(self) -> Path | None:
207
216
  """Get root temporary directory."""
208
217
  return self._root_tempdir
209
218
 
210
219
  def get_tempdir(
211
- self, prefix: Optional[str] = None, suffix: Optional[str] = None
220
+ self, prefix: str | None = None, suffix: str | None = None
212
221
  ) -> Path:
213
222
  """Get a component-scoped temporary directory.
214
223
 
@@ -1,15 +1,20 @@
1
1
  __all__ = [
2
2
  "BasePreprocessor",
3
3
  "fMRIPrepConfoundRemover",
4
+ "Confounds",
5
+ "Strategy",
4
6
  "SpaceWarper",
7
+ "SpaceWarpingImpl",
5
8
  "Smoothing",
9
+ "SmoothingImpl",
6
10
  "TemporalSlicer",
7
11
  "TemporalFilter",
12
+ "logger",
8
13
  ]
9
14
 
10
- from .base import BasePreprocessor
11
- from .confounds import fMRIPrepConfoundRemover
12
- from .warping import SpaceWarper
13
- from .smoothing import Smoothing
15
+ from .base import BasePreprocessor, logger
16
+ from .confounds import fMRIPrepConfoundRemover, Confounds, Strategy
17
+ from .warping import SpaceWarper, SpaceWarpingImpl
18
+ from .smoothing import Smoothing, SmoothingImpl
14
19
  from ._temporal_slicer import TemporalSlicer
15
20
  from ._temporal_filter import TemporalFilter