brkraw 0.3.11__py3-none-any.whl → 0.5.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. brkraw/__init__.py +9 -3
  2. brkraw/apps/__init__.py +12 -0
  3. brkraw/apps/addon/__init__.py +30 -0
  4. brkraw/apps/addon/core.py +35 -0
  5. brkraw/apps/addon/dependencies.py +402 -0
  6. brkraw/apps/addon/installation.py +500 -0
  7. brkraw/apps/addon/io.py +21 -0
  8. brkraw/apps/hook/__init__.py +25 -0
  9. brkraw/apps/hook/core.py +636 -0
  10. brkraw/apps/loader/__init__.py +10 -0
  11. brkraw/apps/loader/core.py +622 -0
  12. brkraw/apps/loader/formatter.py +288 -0
  13. brkraw/apps/loader/helper.py +797 -0
  14. brkraw/apps/loader/info/__init__.py +11 -0
  15. brkraw/apps/loader/info/scan.py +85 -0
  16. brkraw/apps/loader/info/scan.yaml +90 -0
  17. brkraw/apps/loader/info/study.py +69 -0
  18. brkraw/apps/loader/info/study.yaml +156 -0
  19. brkraw/apps/loader/info/transform.py +92 -0
  20. brkraw/apps/loader/types.py +220 -0
  21. brkraw/cli/__init__.py +5 -0
  22. brkraw/cli/commands/__init__.py +2 -0
  23. brkraw/cli/commands/addon.py +327 -0
  24. brkraw/cli/commands/config.py +205 -0
  25. brkraw/cli/commands/convert.py +903 -0
  26. brkraw/cli/commands/hook.py +348 -0
  27. brkraw/cli/commands/info.py +74 -0
  28. brkraw/cli/commands/init.py +214 -0
  29. brkraw/cli/commands/params.py +106 -0
  30. brkraw/cli/commands/prune.py +288 -0
  31. brkraw/cli/commands/session.py +371 -0
  32. brkraw/cli/hook_args.py +80 -0
  33. brkraw/cli/main.py +83 -0
  34. brkraw/cli/utils.py +60 -0
  35. brkraw/core/__init__.py +13 -0
  36. brkraw/core/config.py +380 -0
  37. brkraw/core/entrypoints.py +25 -0
  38. brkraw/core/formatter.py +367 -0
  39. brkraw/core/fs.py +495 -0
  40. brkraw/core/jcamp.py +600 -0
  41. brkraw/core/layout.py +451 -0
  42. brkraw/core/parameters.py +781 -0
  43. brkraw/core/zip.py +1121 -0
  44. brkraw/dataclasses/__init__.py +14 -0
  45. brkraw/dataclasses/node.py +139 -0
  46. brkraw/dataclasses/reco.py +33 -0
  47. brkraw/dataclasses/scan.py +61 -0
  48. brkraw/dataclasses/study.py +131 -0
  49. brkraw/default/__init__.py +3 -0
  50. brkraw/default/pruner_specs/deid4share.yaml +42 -0
  51. brkraw/default/rules/00_default.yaml +4 -0
  52. brkraw/default/specs/metadata_dicom.yaml +236 -0
  53. brkraw/default/specs/metadata_transforms.py +92 -0
  54. brkraw/resolver/__init__.py +7 -0
  55. brkraw/resolver/affine.py +539 -0
  56. brkraw/resolver/datatype.py +69 -0
  57. brkraw/resolver/fid.py +90 -0
  58. brkraw/resolver/helpers.py +36 -0
  59. brkraw/resolver/image.py +188 -0
  60. brkraw/resolver/nifti.py +370 -0
  61. brkraw/resolver/shape.py +235 -0
  62. brkraw/schema/__init__.py +3 -0
  63. brkraw/schema/context_map.yaml +62 -0
  64. brkraw/schema/meta.yaml +57 -0
  65. brkraw/schema/niftiheader.yaml +95 -0
  66. brkraw/schema/pruner.yaml +55 -0
  67. brkraw/schema/remapper.yaml +128 -0
  68. brkraw/schema/rules.yaml +154 -0
  69. brkraw/specs/__init__.py +10 -0
  70. brkraw/specs/hook/__init__.py +12 -0
  71. brkraw/specs/hook/logic.py +31 -0
  72. brkraw/specs/hook/validator.py +22 -0
  73. brkraw/specs/meta/__init__.py +5 -0
  74. brkraw/specs/meta/validator.py +156 -0
  75. brkraw/specs/pruner/__init__.py +15 -0
  76. brkraw/specs/pruner/logic.py +361 -0
  77. brkraw/specs/pruner/validator.py +119 -0
  78. brkraw/specs/remapper/__init__.py +27 -0
  79. brkraw/specs/remapper/logic.py +924 -0
  80. brkraw/specs/remapper/validator.py +314 -0
  81. brkraw/specs/rules/__init__.py +6 -0
  82. brkraw/specs/rules/logic.py +263 -0
  83. brkraw/specs/rules/validator.py +103 -0
  84. brkraw-0.5.0.dist-info/METADATA +81 -0
  85. brkraw-0.5.0.dist-info/RECORD +88 -0
  86. {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info}/WHEEL +1 -2
  87. brkraw-0.5.0.dist-info/entry_points.txt +13 -0
  88. brkraw/lib/__init__.py +0 -4
  89. brkraw/lib/backup.py +0 -641
  90. brkraw/lib/bids.py +0 -0
  91. brkraw/lib/errors.py +0 -125
  92. brkraw/lib/loader.py +0 -1220
  93. brkraw/lib/orient.py +0 -194
  94. brkraw/lib/parser.py +0 -48
  95. brkraw/lib/pvobj.py +0 -301
  96. brkraw/lib/reference.py +0 -245
  97. brkraw/lib/utils.py +0 -471
  98. brkraw/scripts/__init__.py +0 -0
  99. brkraw/scripts/brk_backup.py +0 -106
  100. brkraw/scripts/brkraw.py +0 -744
  101. brkraw/ui/__init__.py +0 -0
  102. brkraw/ui/config.py +0 -17
  103. brkraw/ui/main_win.py +0 -214
  104. brkraw/ui/previewer.py +0 -225
  105. brkraw/ui/scan_info.py +0 -72
  106. brkraw/ui/scan_list.py +0 -73
  107. brkraw/ui/subj_info.py +0 -128
  108. brkraw-0.3.11.dist-info/METADATA +0 -25
  109. brkraw-0.3.11.dist-info/RECORD +0 -28
  110. brkraw-0.3.11.dist-info/entry_points.txt +0 -3
  111. brkraw-0.3.11.dist-info/top_level.txt +0 -2
  112. tests/__init__.py +0 -0
  113. {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info/licenses}/LICENSE +0 -0
brkraw/core/layout.py ADDED
@@ -0,0 +1,451 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, Dict, Iterable, Mapping, Optional, Union, List, Sequence, Tuple
5
+ import re
6
+
7
+ import numpy as np
8
+
9
+ from ..apps.addon.core import resolve_spec_reference
10
+ from ..apps.loader import info as info_resolver
11
+ from ..specs.remapper import (
12
+ load_spec,
13
+ map_parameters,
14
+ load_context_map,
15
+ load_context_map_meta,
16
+ apply_context_map,
17
+ )
18
+
19
+ _ENTRY_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_-]*$")
20
+ _DEFAULT_VALUE_PATTERN = r"[A-Za-z0-9._-]"
21
+ _SLICEPACK_TAG = re.compile(r"\{([^}]+)\}")
22
+ _LAYOUT_TAG = re.compile(r"\{([^}]+)\}")
23
+
24
+
25
+ def render_layout(
26
+ loader: Any,
27
+ scan_id: int,
28
+ *,
29
+ layout_entries: Optional[Iterable[Mapping[str, Any]]] = None,
30
+ layout_template: Optional[str] = None,
31
+ context_map: Optional[Union[str, Path]] = None,
32
+ root: Optional[Union[str, Path]] = None,
33
+ reco_id: Optional[int] = None,
34
+ counter: Optional[int] = None,
35
+ override_info_spec: Optional[Union[str, Path]] = None,
36
+ override_metadata_spec: Optional[Union[str, Path]] = None,
37
+ ) -> str:
38
+ if layout_entries is None and layout_template is None and context_map:
39
+ meta = load_layout_meta(context_map)
40
+ layout_entries = meta.get("layout_entries")
41
+ if layout_entries is None:
42
+ layout_entries = meta.get("layout_fields")
43
+ layout_template = meta.get("layout_template")
44
+ info = load_layout_info(
45
+ loader,
46
+ scan_id,
47
+ context_map=context_map,
48
+ root=root,
49
+ reco_id=reco_id,
50
+ override_info_spec=override_info_spec,
51
+ override_metadata_spec=override_metadata_spec,
52
+ )
53
+ if isinstance(layout_template, str) and layout_template:
54
+ return _render_layout_template(layout_template, info, scan_id, reco_id=reco_id, counter=counter)
55
+ return _render_fields(layout_entries, info, scan_id, reco_id=reco_id, counter=counter)
56
+
57
+
58
+ def load_layout_info(
59
+ loader: Any,
60
+ scan_id: int,
61
+ *,
62
+ context_map: Optional[Union[str, Path]],
63
+ root: Optional[Union[str, Path]] = None,
64
+ reco_id: Optional[int],
65
+ override_info_spec: Optional[Union[str, Path]] = None,
66
+ override_metadata_spec: Optional[Union[str, Path]] = None,
67
+ ) -> Dict[str, Any]:
68
+ info, metadata = load_layout_info_parts(
69
+ loader,
70
+ scan_id,
71
+ context_map=context_map,
72
+ root=root,
73
+ reco_id=reco_id,
74
+ override_info_spec=override_info_spec,
75
+ override_metadata_spec=override_metadata_spec,
76
+ )
77
+ merged = dict(info)
78
+ merged.update(metadata)
79
+ return merged
80
+
81
+
82
+ def load_layout_info_parts(
83
+ loader: Any,
84
+ scan_id: int,
85
+ *,
86
+ context_map: Optional[Union[str, Path]],
87
+ root: Optional[Union[str, Path]] = None,
88
+ reco_id: Optional[int],
89
+ override_info_spec: Optional[Union[str, Path]] = None,
90
+ override_metadata_spec: Optional[Union[str, Path]] = None,
91
+ ) -> Tuple[Dict[str, Any], Dict[str, Any]]:
92
+ map_data = None
93
+ if context_map:
94
+ map_data = load_context_map(context_map)
95
+ scan = loader.get_scan(scan_id)
96
+ if override_info_spec:
97
+ spec_path = resolve_spec_reference(
98
+ str(override_info_spec),
99
+ category="info_spec",
100
+ root=root,
101
+ )
102
+ spec_data, transforms = load_spec(spec_path, validate=False)
103
+ params_map = _build_params_map(loader, scan, reco_id=reco_id)
104
+ mapped = map_parameters(
105
+ params_map,
106
+ spec_data,
107
+ transforms,
108
+ validate=False,
109
+ context_map=None,
110
+ context={"scan_id": scan_id, "reco_id": reco_id},
111
+ )
112
+ if not isinstance(mapped, dict):
113
+ raise ValueError("override_info_spec must resolve to a mapping.")
114
+ info = mapped
115
+ else:
116
+ study_info = info_resolver.study(loader) or {}
117
+ scan_info = info_resolver.scan(scan) or {}
118
+ if isinstance(study_info, dict):
119
+ scan_info = dict(scan_info)
120
+ if "Study" in study_info:
121
+ scan_info["Study"] = study_info["Study"]
122
+ if "Subject" in study_info:
123
+ scan_info["Subject"] = study_info["Subject"]
124
+ info = scan_info if isinstance(scan_info, dict) else {}
125
+ if map_data:
126
+ info = apply_context_map(
127
+ info,
128
+ map_data,
129
+ target="info_spec",
130
+ context={"scan_id": scan_id, "reco_id": reco_id},
131
+ )
132
+ metadata: Dict[str, Any] = {}
133
+ if map_data or override_metadata_spec:
134
+ meta = loader.get_metadata(
135
+ scan_id,
136
+ reco_id=reco_id,
137
+ context_map=context_map,
138
+ spec=override_metadata_spec,
139
+ )
140
+ if isinstance(meta, dict):
141
+ metadata = meta
142
+ return info, metadata
143
+
144
+
145
+ def load_layout_meta(context_map: Optional[Union[str, Path]]) -> Dict[str, Any]:
146
+ if not context_map:
147
+ return {}
148
+ return load_context_map_meta(context_map)
149
+
150
+
151
+ def render_slicepack_suffixes(
152
+ info: Mapping[str, Any],
153
+ *,
154
+ count: int,
155
+ template: str = "_slpack{index}",
156
+ counter: Optional[int] = None,
157
+ ) -> List[str]:
158
+ suffixes: List[str] = []
159
+ for idx in range(count):
160
+ suffixes.append(_render_slicepack_suffix(template, info, idx, counter=counter))
161
+ return suffixes
162
+
163
+
164
+ def _render_slicepack_suffix(
165
+ template: str,
166
+ info: Mapping[str, Any],
167
+ idx: int,
168
+ *,
169
+ counter: Optional[int],
170
+ ) -> str:
171
+ def _replace(match: re.Match[str]) -> str:
172
+ tag = match.group(1)
173
+ if tag.lower() == "index":
174
+ return str(idx + 1)
175
+ value = _resolve_tag(tag, info, idx + 1, reco_id=None, counter=counter)
176
+ chosen = _select_indexed_value(value, idx)
177
+ if chosen is None:
178
+ return str(idx + 1)
179
+ rendered = _format_value_with_options(
180
+ chosen,
181
+ value_pattern=None,
182
+ value_replace="",
183
+ max_length=None,
184
+ )
185
+ return rendered or str(idx + 1)
186
+
187
+ return _SLICEPACK_TAG.sub(_replace, template)
188
+
189
+
190
+ def _select_indexed_value(value: Any, idx: int) -> Any:
191
+ if value is None:
192
+ return None
193
+ if isinstance(value, np.ndarray):
194
+ items = value.tolist()
195
+ return items[idx] if idx < len(items) else None
196
+ if isinstance(value, (list, tuple)):
197
+ return value[idx] if idx < len(value) else None
198
+ return value
199
+
200
+
201
+ def _build_params_map(loader: Any, scan: Any, *, reco_id: Optional[int]) -> Dict[str, Any]:
202
+ params_map: Dict[str, Any] = {
203
+ "method": getattr(scan, "method", None),
204
+ "acqp": getattr(scan, "acqp", None),
205
+ }
206
+ study = getattr(loader, "_study", None)
207
+ subject = getattr(study, "subject", None)
208
+ if subject is not None:
209
+ params_map["subject"] = subject
210
+
211
+ if reco_id is None:
212
+ reco_ids = list(getattr(scan, "avail", {}).keys())
213
+ reco_id = reco_ids[0] if reco_ids else None
214
+ if reco_id is not None:
215
+ reco = scan.get_reco(reco_id)
216
+ params_map["visu_pars"] = {reco_id: reco.visu_pars}
217
+ params_map["reco"] = {reco_id: reco.reco}
218
+ return params_map
219
+
220
+
221
+ def _render_fields(
222
+ fields: Optional[Iterable[Mapping[str, Any]]],
223
+ info: Mapping[str, Any],
224
+ scan_id: int,
225
+ *,
226
+ reco_id: Optional[int],
227
+ counter: Optional[int],
228
+ ) -> str:
229
+ parts: List[str] = []
230
+ seps: List[Optional[str]] = []
231
+ entry_values: Dict[str, Any] = {}
232
+
233
+ for field in fields or []:
234
+ if not isinstance(field, Mapping):
235
+ continue
236
+ key = field.get("key")
237
+ use_entry = field.get("use_entry")
238
+ hide = bool(field.get("hide"))
239
+ entry = field.get("entry")
240
+ sep = field.get("sep")
241
+ value_pattern = field.get("value_pattern")
242
+ value_replace = field.get("value_replace", "")
243
+ max_length = field.get("max_length")
244
+
245
+ if key is not None and use_entry is not None:
246
+ continue
247
+ if key is None and use_entry is None:
248
+ continue
249
+
250
+ value_str: Optional[str] = None
251
+ entry_clean: Optional[str] = None
252
+
253
+ if key is not None:
254
+ if not isinstance(key, str) or not key.strip():
255
+ continue
256
+ if isinstance(entry, str) and entry.strip():
257
+ entry_clean = entry.strip()
258
+ if not _ENTRY_PATTERN.match(entry_clean):
259
+ continue
260
+ elif not hide:
261
+ entry_clean = key.replace(".", "").lower()
262
+ if not _ENTRY_PATTERN.match(entry_clean):
263
+ continue
264
+ value = _resolve_tag(key, info, scan_id, reco_id=reco_id, counter=counter)
265
+ value_str = _format_value_with_options(
266
+ value,
267
+ value_pattern=value_pattern,
268
+ value_replace=value_replace,
269
+ max_length=max_length,
270
+ )
271
+ if value_str is None:
272
+ continue
273
+ if entry_clean:
274
+ entry_values[entry_clean] = value
275
+ else:
276
+ if not isinstance(use_entry, str) or not use_entry.strip():
277
+ continue
278
+ entry_clean = use_entry.strip()
279
+ if not _ENTRY_PATTERN.match(entry_clean):
280
+ continue
281
+ raw_value = entry_values.get(entry_clean)
282
+ if raw_value is None:
283
+ continue
284
+ value_str = _format_value_with_options(
285
+ raw_value,
286
+ value_pattern=value_pattern,
287
+ value_replace=value_replace,
288
+ max_length=max_length,
289
+ )
290
+ if value_str is None:
291
+ continue
292
+
293
+ if hide:
294
+ parts.append(value_str)
295
+ else:
296
+ parts.append(f"{entry_clean}-{value_str}")
297
+ if isinstance(sep, str) and sep:
298
+ seps.append(sep)
299
+ else:
300
+ seps.append(None)
301
+
302
+ if not parts:
303
+ return f"scan-{scan_id}"
304
+ result = parts[0]
305
+ for idx in range(1, len(parts)):
306
+ joiner = seps[idx - 1] if seps[idx - 1] is not None else "_"
307
+ result = f"{result}{joiner}{parts[idx]}"
308
+ return result
309
+
310
+
311
+ def _render_layout_template(
312
+ template: str,
313
+ info: Mapping[str, Any],
314
+ scan_id: int,
315
+ *,
316
+ reco_id: Optional[int],
317
+ counter: Optional[int],
318
+ ) -> str:
319
+ if not _LAYOUT_TAG.search(template):
320
+ return template
321
+ rendered = _LAYOUT_TAG.sub(
322
+ lambda m: _resolve_layout_tag(m, info, scan_id, reco_id=reco_id, counter=counter),
323
+ template,
324
+ )
325
+ return rendered or template
326
+
327
+
328
+ def _resolve_layout_tag(
329
+ match: re.Match[str],
330
+ info: Mapping[str, Any],
331
+ scan_id: int,
332
+ *,
333
+ reco_id: Optional[int],
334
+ counter: Optional[int],
335
+ ) -> str:
336
+ tag = match.group(1) or ""
337
+ if not tag:
338
+ return ""
339
+ value = _resolve_tag(tag.strip(), info, scan_id, reco_id=reco_id, counter=counter)
340
+ rendered = _format_value(value)
341
+ return rendered or ""
342
+
343
+
344
+ def _resolve_tag(
345
+ tag: str,
346
+ info: Mapping[str, Any],
347
+ scan_id: int,
348
+ *,
349
+ reco_id: Optional[int] = None,
350
+ counter: Optional[int] = None,
351
+ ) -> Any:
352
+ if tag in {"ScanID", "scan_id", "scanid"}:
353
+ return scan_id
354
+ if tag in {"RecoID", "reco_id", "recoid"}:
355
+ return reco_id
356
+ if tag in {"Counter", "counter"}:
357
+ return counter
358
+ if "." in tag:
359
+ root_key, rest = tag.split(".", 1)
360
+ root_val = info.get(root_key)
361
+ if isinstance(root_val, Mapping):
362
+ return _resolve_nested(root_val, rest)
363
+ return None
364
+ return info.get(tag)
365
+
366
+
367
+ def _format_value(value: Any) -> Optional[str]:
368
+ return _format_value_with_options(
369
+ value,
370
+ value_pattern=None,
371
+ value_replace="",
372
+ max_length=None,
373
+ )
374
+
375
+
376
+ def _format_value_with_options(
377
+ value: Any,
378
+ *,
379
+ value_pattern: Optional[str],
380
+ value_replace: Optional[str],
381
+ max_length: Optional[Any],
382
+ ) -> Optional[str]:
383
+ if value is None:
384
+ return None
385
+ if isinstance(value, Mapping):
386
+ parts = []
387
+ for k, v in value.items():
388
+ k_str = _sanitize_value(k, value_pattern, value_replace, None)
389
+ v_str = _format_value_with_options(
390
+ v,
391
+ value_pattern=value_pattern,
392
+ value_replace=value_replace,
393
+ max_length=None,
394
+ )
395
+ if k_str and v_str:
396
+ parts.append(f"{k_str}-{v_str}")
397
+ raw = "-".join(parts)
398
+ elif isinstance(value, np.ndarray):
399
+ raw = "-".join(str(v) for v in value.tolist())
400
+ elif isinstance(value, (list, tuple)):
401
+ items = [
402
+ v for v in (
403
+ _format_value_with_options(
404
+ v,
405
+ value_pattern=value_pattern,
406
+ value_replace=value_replace,
407
+ max_length=None,
408
+ )
409
+ for v in value
410
+ )
411
+ if v
412
+ ]
413
+ raw = "-".join(items)
414
+ else:
415
+ raw = str(value).strip()
416
+ if not raw:
417
+ return None
418
+ cleaned = _sanitize_value(raw, value_pattern, value_replace, max_length)
419
+ return cleaned or None
420
+
421
+
422
+ def _sanitize_value(
423
+ raw: Any,
424
+ value_pattern: Optional[str],
425
+ value_replace: Optional[str],
426
+ max_length: Optional[Any],
427
+ ) -> Optional[str]:
428
+ text = str(raw).strip()
429
+ if not text:
430
+ return None
431
+ pattern = value_pattern or _DEFAULT_VALUE_PATTERN
432
+ try:
433
+ regex = re.compile(pattern)
434
+ except re.error as exc:
435
+ raise ValueError(f"Invalid value_pattern: {pattern!r}") from exc
436
+ repl = "" if value_replace is None else str(value_replace)
437
+ cleaned = "".join(
438
+ ch if regex.fullmatch(ch) else repl for ch in text
439
+ )
440
+ if isinstance(max_length, int) and max_length > 0:
441
+ cleaned = cleaned[:max_length]
442
+ return cleaned or None
443
+
444
+
445
+ def _resolve_nested(value: Mapping[str, Any], dotted: str) -> Any:
446
+ current: Any = value
447
+ for part in dotted.split("."):
448
+ if not isinstance(current, Mapping):
449
+ return None
450
+ current = current.get(part)
451
+ return current