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,23 +5,73 @@
5
5
  # Synchon Mandal <s.mandal@fz-juelich.de>
6
6
  # License: AGPL
7
7
 
8
- from typing import Union
8
+ from collections import defaultdict
9
+ from pathlib import Path
10
+ from typing import Annotated
11
+
12
+ from pydantic import BeforeValidator, ConfigDict
9
13
 
10
14
  from ..api.decorators import register_datagrabber
11
- from ..typing import DataGrabberLike
12
- from ..utils import deep_update, raise_error
13
- from .base import BaseDataGrabber
15
+ from ..typing import DataGrabberLike, Element
16
+ from ..utils import deep_update, ensure_list, raise_error
17
+ from .base import BaseDataGrabber, DataType
18
+ from .pattern import PatternDataGrabber, _join_elements
19
+ from .pattern_datalad import PatternDataladDataGrabber
14
20
 
15
21
 
16
22
  __all__ = ["MultipleDataGrabber"]
17
23
 
18
24
 
25
+ def _nested_types_only(
26
+ datagrabber: DataGrabberLike, data_type: str
27
+ ) -> list[str] | None:
28
+ """Get the nested data types grabbed without the main file of a data type.
29
+
30
+ Parameters
31
+ ----------
32
+ datagrabber : DataGrabber-like object
33
+ The DataGrabber.
34
+ data_type : str
35
+ The data type.
36
+
37
+ Returns
38
+ -------
39
+ list of str or None
40
+ The nested data types (e.g., ``["confounds"]``) if the DataGrabber
41
+ only grabs them, without the main file of the data type, or None if
42
+ it grabs the main file.
43
+
44
+ """
45
+ patterns = getattr(datagrabber, "patterns", None)
46
+ if patterns is None:
47
+ return None
48
+ dtype_val = patterns.get(data_type)
49
+ # Data types with a list of patterns (e.g., Warp) have their main files
50
+ if dtype_val is None or isinstance(dtype_val, list):
51
+ return None
52
+ if "pattern" in dtype_val:
53
+ return None
54
+ return [k for k, v in dtype_val.items() if isinstance(v, dict)]
55
+
56
+
19
57
  @register_datagrabber
20
58
  class MultipleDataGrabber(BaseDataGrabber):
21
59
  """Concrete implementation for multi sourced data fetching.
22
60
 
23
61
  Implements a DataGrabber which can be used to fetch data from multiple
24
- DataGrabbers.
62
+ DataGrabbers, e.g., from different datasets:
63
+
64
+ * Each data type is grabbed by one DataGrabber (set with their ``types``),
65
+ except for the DataGrabbers that only grab the nested data types of a
66
+ data type (without its main file, e.g., only the ``confounds`` of
67
+ ``BOLD``), which replace the nested data types of the other one.
68
+ * The element keys are the ones of the first DataGrabber, followed by the
69
+ other keys of the other DataGrabbers, which can have different keys
70
+ (e.g., ``subject`` and ``task`` for ``BOLD``, and only ``subject`` for
71
+ ``VBM_GM``). The keys with the same name must have the same values in
72
+ all the DataGrabbers.
73
+ * The elements are the ones available in all the DataGrabbers, joined on
74
+ their common keys, and each DataGrabber grabs an element with its keys.
25
75
 
26
76
  Parameters
27
77
  ----------
@@ -33,62 +83,66 @@ class MultipleDataGrabber(BaseDataGrabber):
33
83
  Raises
34
84
  ------
35
85
  RuntimeError
36
- If ``datagrabbers`` have different element keys or
37
- overlapping data types or nested data types.
86
+ If more than one DataGrabber grabs the main file of a data type or
87
+ the same nested data type without the main file.
38
88
 
39
89
  """
40
90
 
41
- def __init__(self, datagrabbers: list[DataGrabberLike], **kwargs) -> None:
42
- # Check datagrabbers consistency
43
- # Check for same element keys
44
- first_keys = datagrabbers[0].get_element_keys()
45
- for dg in datagrabbers[1:]:
46
- if dg.get_element_keys() != first_keys:
91
+ model_config = ConfigDict(extra="allow")
92
+
93
+ datagrabbers: list[
94
+ DataGrabberLike | PatternDataGrabber | PatternDataladDataGrabber
95
+ ]
96
+ types: Annotated[
97
+ DataType | list[DataType], BeforeValidator(ensure_list)
98
+ ] = [] # noqa: RUF012
99
+ datadir: Path = Path(".")
100
+
101
+ def validate_datagrabber_params(self) -> None:
102
+ """Run extra logical validation for datagrabber."""
103
+ # Each data type is grabbed by one DataGrabber, except for the nested
104
+ # data types grabbed without the main file, which can only be
105
+ # grabbed by one of these DataGrabbers
106
+ main = defaultdict(list)
107
+ nested = defaultdict(list)
108
+ for i, dg in enumerate(self.datagrabbers):
109
+ for data_type in dg.get_types():
110
+ nested_types = _nested_types_only(dg, data_type)
111
+ if nested_types is None:
112
+ main[data_type].append(i)
113
+ else:
114
+ for nested_type in nested_types:
115
+ nested[(data_type, nested_type)].append(i)
116
+ for data_type, dgs in main.items():
117
+ if len(dgs) > 1:
47
118
  raise_error(
48
- msg="DataGrabbers have different element keys",
119
+ msg=(
120
+ f"Data type `{data_type}` is grabbed by more than one "
121
+ f"DataGrabber ({dgs}), it must be removed from the "
122
+ "`types` of all but one of them"
123
+ ),
49
124
  klass=RuntimeError,
50
125
  )
51
- # Check for no overlapping types (and nested data types)
52
- types = [x for dg in datagrabbers for x in dg.get_types()]
53
- if len(types) != len(set(types)):
54
- if all(hasattr(dg, "patterns") for dg in datagrabbers):
55
- first_patterns = datagrabbers[0].patterns
56
- for dg in datagrabbers[1:]:
57
- for data_type in set(types):
58
- dtype_pattern = dg.patterns.get(data_type)
59
- if dtype_pattern is None:
60
- continue
61
- # Check if first-level keys of data type are same
62
- if (
63
- dtype_pattern.keys()
64
- == first_patterns[data_type].keys()
65
- ):
66
- raise_error(
67
- msg=(
68
- "DataGrabbers have overlapping mandatory "
69
- "and / or optional key(s) for data type: "
70
- f"`{data_type}`"
71
- ),
72
- klass=RuntimeError,
73
- )
74
- else:
75
- # Can't check further
126
+ for (data_type, nested_type), dgs in nested.items():
127
+ if len(dgs) > 1:
76
128
  raise_error(
77
- msg="DataGrabbers have overlapping types",
129
+ msg=(
130
+ f"Nested data type `{data_type}.{nested_type}` is "
131
+ f"grabbed by more than one DataGrabber ({dgs})"
132
+ ),
78
133
  klass=RuntimeError,
79
134
  )
80
- self._datagrabbers = datagrabbers
81
135
 
82
- def __getitem__(self, element: Union[str, tuple]) -> dict:
136
+ def __getitem__(self, element: Element) -> dict:
83
137
  """Implement indexing.
84
138
 
85
139
  Parameters
86
140
  ----------
87
- element : str or tuple
141
+ element : `Element`
88
142
  The element to be indexed. If one string is provided, it is
89
143
  assumed to be a tuple with only one item. If a tuple is provided,
90
- each item in the tuple is the value for the replacement string
91
- specified in "replacements".
144
+ each item in the tuple is the value for the element key (see
145
+ :meth:`.get_element_keys`).
92
146
 
93
147
  Returns
94
148
  -------
@@ -97,57 +151,104 @@ class MultipleDataGrabber(BaseDataGrabber):
97
151
  specified element.
98
152
 
99
153
  """
100
-
101
- out = {}
154
+ if not isinstance(element, tuple):
155
+ element = (element,)
156
+ named_element = dict(
157
+ zip(self.get_element_keys(), element, strict=True)
158
+ )
159
+ # Grab the element with the keys of each DataGrabber
160
+ outs = []
102
161
  metas = []
103
- for dg in self._datagrabbers:
104
- t_out = dg[element]
105
- deep_update(out, t_out)
162
+ for dg in self.datagrabbers:
163
+ t_element = tuple(named_element[k] for k in dg.get_element_keys())
164
+ outs.append((dg, dg[t_element]))
106
165
  # Now get the meta for this datagrabber
107
166
  t_meta = {}
108
167
  dg.update_meta(t_meta, "datagrabber")
109
168
  # Store all the sub-datagrabbers meta
110
169
  metas.append(t_meta["meta"]["datagrabber"])
111
170
 
171
+ # Add the data types with their main file first, and then the nested
172
+ # data types grabbed without the main file, which replace the ones
173
+ # grabbed with the main file
174
+ out = {}
175
+ for nested_only in (False, True):
176
+ for dg, t_out in outs:
177
+ for data_type, value in t_out.items():
178
+ t_nested_only = (
179
+ _nested_types_only(dg, data_type) is not None
180
+ )
181
+ if t_nested_only != nested_only:
182
+ continue
183
+ if data_type in out:
184
+ deep_update(out[data_type], value)
185
+ else:
186
+ out[data_type] = value
187
+
112
188
  # Update all the metas again
113
189
  for kind in out:
114
190
  to_update = out[kind]
115
191
  if not isinstance(to_update, list):
116
192
  to_update = [to_update]
193
+ element_keys = self.get_type_element_keys(kind)
117
194
  for t_kind in to_update:
118
195
  self.update_meta(t_kind, "datagrabber")
119
196
  t_kind["meta"]["datagrabber"]["datagrabbers"] = metas
197
+ t_kind["meta"]["element"] = named_element
198
+ t_kind["meta"]["_element_keys"] = element_keys
120
199
  return out
121
200
 
201
+ def get_type_element_keys(self, data_type: str) -> list[str]:
202
+ """Get the element keys that the data of a data type depends 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 that the data of the data type depends on in all
213
+ the DataGrabbers grabbing it (e.g., the one grabbing the main file
214
+ and the one grabbing only its nested data types).
215
+
216
+ """
217
+ keys = set()
218
+ for dg in self.datagrabbers:
219
+ if data_type in dg.get_types():
220
+ keys.update(dg.get_type_element_keys(data_type))
221
+ return [k for k in self.get_element_keys() if k in keys]
222
+
122
223
  def __enter__(self) -> "MultipleDataGrabber":
123
224
  """Implement context entry."""
124
- for dg in self._datagrabbers:
225
+ for dg in self.datagrabbers:
125
226
  dg.__enter__()
126
227
  return self
127
228
 
128
229
  def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
129
230
  """Implement context exit."""
130
- for dg in self._datagrabbers:
231
+ for dg in self.datagrabbers:
131
232
  dg.__exit__(exc_type, exc_value, exc_traceback)
132
233
 
133
- # TODO: return type should be List[List[str]], but base type is List[str]
134
234
  def get_types(self) -> list[str]:
135
235
  """Get types.
136
236
 
137
237
  Returns
138
238
  -------
139
- list of list of str
239
+ list of str
140
240
  The types of data to be grabbed.
141
241
 
142
242
  """
143
- types = [x for dg in self._datagrabbers for x in dg.get_types()]
144
- return types
243
+ types = [x for dg in self.datagrabbers for x in dg.get_types()]
244
+ return list(dict.fromkeys(types))
145
245
 
146
246
  def get_element_keys(self) -> list[str]:
147
247
  """Get element keys.
148
248
 
149
249
  For each item in the ``element`` tuple passed to ``__getitem__()``,
150
- this method returns the corresponding key(s).
250
+ this method returns the corresponding key(s): the keys of the first
251
+ DataGrabber, followed by the other keys of the other DataGrabbers.
151
252
 
152
253
  Returns
153
254
  -------
@@ -155,11 +256,16 @@ class MultipleDataGrabber(BaseDataGrabber):
155
256
  The element keys.
156
257
 
157
258
  """
158
- return self._datagrabbers[0].get_element_keys()
259
+ keys = [k for dg in self.datagrabbers for k in dg.get_element_keys()]
260
+ return list(dict.fromkeys(keys))
159
261
 
160
262
  def get_elements(self) -> list:
161
263
  """Get elements.
162
264
 
265
+ The elements of the DataGrabbers are joined on their common keys, as
266
+ the elements of the data types of a :class:`.PatternDataGrabber`, so
267
+ the elements are the ones available in all the DataGrabbers.
268
+
163
269
  Returns
164
270
  -------
165
271
  list
@@ -169,11 +275,25 @@ class MultipleDataGrabber(BaseDataGrabber):
169
275
  related DataGrabbers.
170
276
 
171
277
  """
172
- all_elements = [dg.get_elements() for dg in self._datagrabbers]
173
- elements = set(all_elements[0])
174
- for s in all_elements[1:]:
175
- elements.intersection_update(s)
176
- return list(elements)
278
+ elements = None
279
+ keys = []
280
+ for dg in self.datagrabbers:
281
+ t_keys = dg.get_element_keys()
282
+ t_elements = {
283
+ e if isinstance(e, tuple) else (e,) for e in dg.get_elements()
284
+ }
285
+ if elements is None:
286
+ elements, keys = t_elements, t_keys
287
+ else:
288
+ elements, keys = _join_elements(
289
+ elements, keys, t_elements, t_keys
290
+ )
291
+ if elements is None:
292
+ return []
293
+ out = sorted(elements)
294
+ if len(keys) == 1:
295
+ out = [element[0] for element in out]
296
+ return out
177
297
 
178
298
  def get_item(self, **_: dict) -> dict[str, dict]:
179
299
  """Get the specified item from the dataset.