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
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from .study import resolve as study
4
+ from .scan import resolve as scan
5
+
6
+ __all__ = [
7
+ 'study',
8
+ 'scan'
9
+ ]
10
+
11
+ __dir__ = __all__
@@ -0,0 +1,85 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, cast, TYPE_CHECKING, Dict, Optional, Union
4
+ from pathlib import Path
5
+ import logging
6
+ from ....specs.remapper import load_spec, map_parameters
7
+ from ....specs.remapper.validator import validate_spec
8
+
9
+
10
+ if TYPE_CHECKING:
11
+ from ..types import ScanLoader, RecoLoader
12
+
13
+ logger = logging.getLogger("brkraw")
14
+
15
+
16
+ def resolve(
17
+ scan: "ScanLoader",
18
+ *,
19
+ spec: Optional[Dict[str, Any]] = None,
20
+ transforms: Optional[Dict[str, Any]] = None,
21
+ spec_source: Optional[Union[str, Path]] = None,
22
+ spec_filename: Optional[str] = None,
23
+ validate: bool = True,
24
+ ) -> Dict[str, Any]:
25
+ """Resolve scan-level metadata using a remapper spec.
26
+
27
+ The spec/transform sources are resolved in the following priority order:
28
+ 1) ``spec``/``transforms`` mappings passed directly.
29
+ 2) ``spec_source``.
30
+ 3) Package default ``scan.yaml``, optionally overridden by ``spec_filename``.
31
+
32
+ Args:
33
+ scan: ScanLoader instance to resolve metadata from.
34
+ spec: Spec mapping to use directly (highest priority).
35
+ transforms: Transform mapping to use with ``spec``.
36
+ spec_source: Path for a YAML spec.
37
+ spec_filename: Package-relative spec filename (default ``scan.yaml``).
38
+ validate: When True, validate the spec before mapping.
39
+
40
+ Returns:
41
+ A dictionary of resolved scan metadata.
42
+ """
43
+ scan = cast("ScanLoader", scan)
44
+
45
+ if spec is not None:
46
+ spec_data = spec
47
+ transforms_data = transforms or {}
48
+ if validate:
49
+ validate_spec(spec_data)
50
+ elif spec_source is not None:
51
+ spec_data, transforms_data = load_spec(
52
+ spec_source,
53
+ validate=validate,
54
+ )
55
+ else:
56
+ spec_file = Path(__file__).with_name(spec_filename or "scan.yaml")
57
+ spec_data, transforms_data = load_spec(
58
+ spec_file,
59
+ validate=validate,
60
+ )
61
+ results = map_parameters(scan, spec_data, transforms_data)
62
+ if len(scan.avail):
63
+ results['Reco(s)'] = {}
64
+ for reco_id in scan.avail.keys():
65
+ reco_spec = {
66
+ "Type": {
67
+ "sources": [
68
+ {
69
+ "file": "visu_pars",
70
+ "key": "VisuCoreFrameType",
71
+ "reco_id": reco_id,
72
+ }
73
+ ]
74
+ }
75
+ }
76
+ try:
77
+ results["Reco(s)"][reco_id] = map_parameters(scan, reco_spec)
78
+ except (FileNotFoundError, AttributeError) as exc:
79
+ logger.warning(
80
+ "visu_pars missing for scan %s reco %s; skipping reco entry: %s",
81
+ getattr(scan, "scan_id", "unknown"),
82
+ reco_id,
83
+ exc,
84
+ )
85
+ return results
@@ -0,0 +1,90 @@
1
+ __meta__:
2
+ name: "scan_info"
3
+ version: "1.0.0"
4
+ description: "Scan-level info mapping for Bruker datasets."
5
+ category: "info_spec"
6
+ transforms_source: "transform.py"
7
+
8
+ Method:
9
+ sources:
10
+ - file: acqp
11
+ key: ACQ_method
12
+ transform: strip_jcamp_string
13
+ Protocol:
14
+ sources:
15
+ - file: acqp
16
+ key: ACQ_protocol_name
17
+ transform: strip_jcamp_string
18
+ Dim:
19
+ sources:
20
+ - file: acqp
21
+ key: ACQ_dim
22
+ DimDesc:
23
+ sources:
24
+ - file: acqp
25
+ key: ACQ_dim_desc
26
+ transform: convert_to_list
27
+ NumEchoes:
28
+ sources:
29
+ - file: acqp
30
+ key: NECHOES
31
+ TE (ms):
32
+ sources:
33
+ - file: method
34
+ key: PVM_EchoTime
35
+ - file: acqp
36
+ key: ACQ_echo_time
37
+ transform: convert_to_list
38
+ TR (ms):
39
+ sources:
40
+ - file: method
41
+ key: PVM_RepetitionTime
42
+ - file: acqp
43
+ key: ACQ_repetition_time
44
+ transform: convert_to_list
45
+ FlipAngle (degree):
46
+ sources:
47
+ - file: acqp
48
+ key: ACQ_flip_angle
49
+ Shape:
50
+ sources:
51
+ - file: method
52
+ key: PVM_Matrix
53
+ transform: convert_to_list
54
+ FOV (mm):
55
+ sources:
56
+ - file: method
57
+ key: PVM_Fov
58
+ transform: convert_to_list
59
+ NumSlicePack:
60
+ sources:
61
+ - file: method
62
+ key: PVM_NSPacks
63
+ SliceOrient:
64
+ sources:
65
+ - file: method
66
+ key: PVM_SPackArrSliceOrient
67
+ transform: convert_to_list
68
+ ReadOrient:
69
+ sources:
70
+ - file: method
71
+ key: PVM_SPackArrReadOrient
72
+ transform: convert_to_list
73
+ SliceDistance (mm):
74
+ sources:
75
+ - file: method
76
+ key: PVM_SPackArrSliceDistance
77
+ transform: convert_to_list
78
+ SliceGap (mm):
79
+ sources:
80
+ - file: method
81
+ key: PVM_SPackArrSliceGap
82
+ transform: convert_to_list
83
+ NumAverage:
84
+ sources:
85
+ - file: method
86
+ key: PVM_NAverages
87
+ NumRepeat:
88
+ sources:
89
+ - file: method
90
+ key: PVM_NRepetitions
@@ -0,0 +1,69 @@
1
+ from __future__ import annotations
2
+
3
+ """Resolve study-level metadata using remapping rules.
4
+
5
+ This module maps Paravision JCAMP parameters into a normalized dictionary.
6
+ It prefers subject-level parameters when available, and falls back to scan-level
7
+ visu_pars data when subject metadata is missing. Scan fallback selects the
8
+ first scan that contains a reco with a readable visu_pars object.
9
+ """
10
+
11
+ from typing import Any, cast, TYPE_CHECKING, Dict, Optional
12
+ from pathlib import Path
13
+ from ....core.parameters import Parameters
14
+ from ....specs.remapper import load_spec, map_parameters
15
+ from ....specs.remapper.validator import validate_spec
16
+
17
+
18
+ if TYPE_CHECKING:
19
+ from .. import BrukerLoader
20
+ from ..types import StudyLoader
21
+
22
+
23
+ def resolve(loader: "BrukerLoader") -> Dict[str, Any]:
24
+ """Resolve study/subject metadata into a normalized mapping.
25
+
26
+ Args:
27
+ loader: BrukerLoader instance providing access to Study/Scan/Reco nodes.
28
+
29
+ Returns:
30
+ A dictionary containing study and subject metadata fields.
31
+
32
+ Raises:
33
+ ValueError: If no scans are available or no scan has a readable
34
+ visu_pars when subject metadata is missing.
35
+ """
36
+ def _resolve_section_transforms_path(section: Dict[str, Any]) -> Optional[Path]:
37
+ meta = section.get("__meta__")
38
+ if not isinstance(meta, dict) or not meta.get("transforms_source"):
39
+ return None
40
+ path = Path(meta["transforms_source"])
41
+ if not path.is_absolute():
42
+ path = (spec_path.parent / path).resolve()
43
+ return path
44
+
45
+ spec_path = Path(__file__).with_name("study.yaml")
46
+ spec, transforms = load_spec(
47
+ spec_path,
48
+ validate=False,
49
+ )
50
+ validate_spec(spec["study"], transforms_source=_resolve_section_transforms_path(spec["study"]))
51
+ validate_spec(spec["scan"], transforms_source=_resolve_section_transforms_path(spec["scan"]))
52
+ study = cast("StudyLoader", loader._study)
53
+ if study.has_subject:
54
+ return map_parameters(study, spec["study"], transforms=transforms, validate=True)
55
+ else:
56
+ if not study.avail:
57
+ raise ValueError("No scans available to resolve study info from visu_pars.")
58
+ scan_with_visu = None
59
+ for scan in study.avail.values():
60
+ for reco in scan.avail.values():
61
+ visu = getattr(reco, "visu_pars", None)
62
+ if isinstance(visu, Parameters):
63
+ scan_with_visu = scan
64
+ break
65
+ if scan_with_visu is not None:
66
+ break
67
+ if scan_with_visu is None:
68
+ raise ValueError("No scan contains a reco with readable visu_pars for study info.")
69
+ return map_parameters(scan_with_visu, spec["scan"], transforms=transforms)
@@ -0,0 +1,156 @@
1
+ study:
2
+ __meta__:
3
+ name: "study_info"
4
+ version: "1.0.0"
5
+ description: "Study-level info mapping for Bruker datasets."
6
+ category: "info_spec"
7
+ transforms_source: "transform.py"
8
+ Study.Opperator:
9
+ sources:
10
+ - file: subject
11
+ key: SUBJECT_referral
12
+ - file: subject
13
+ key: SUBJECT_study_operator
14
+ transform: strip_jcamp_string
15
+ Study.Date:
16
+ sources:
17
+ - file: subject
18
+ key: SUBJECT_abs_date
19
+ - file: subject
20
+ key: SUBJECT_study_date
21
+ transform:
22
+ - unixtime_to_datetime
23
+ Study.ID:
24
+ sources:
25
+ - file: subject
26
+ key: SUBJECT_study_name
27
+ transform: strip_jcamp_string
28
+ Study.Number:
29
+ sources:
30
+ - file: subject
31
+ key: SUBJECT_study_nr
32
+ transform: strip_jcamp_string
33
+ Study.InstanceUID:
34
+ sources:
35
+ - file: subject
36
+ key: SUBJECT_study_instance_uid
37
+ transform: strip_jcamp_string
38
+ Subject.ID:
39
+ sources:
40
+ - file: subject
41
+ key: SUBJECT_id
42
+ transform: strip_jcamp_string
43
+ Subject.Name:
44
+ sources:
45
+ - file: subject
46
+ key: SUBJECT_name_string
47
+ transform: strip_jcamp_string
48
+ Subject.Type:
49
+ sources:
50
+ - file: subject
51
+ key: SUBJECT_type
52
+ Subject.Sex:
53
+ sources:
54
+ - file: subject
55
+ key: SUBJECT_sex
56
+ - file: subject
57
+ key: SUBJECT_gender
58
+ transform: strip_jcamp_string
59
+ Subject.DateOfBirth:
60
+ sources:
61
+ - file: subject
62
+ key: SUBJECT_dbirth
63
+ transform:
64
+ - strip_jcamp_string
65
+ - stringtime_to_datetime
66
+ Subject.Weight:
67
+ sources:
68
+ - file: subject
69
+ key: SUBJECT_weight
70
+ transform: strip_jcamp_string
71
+ Subject.Position:
72
+ sources:
73
+ - file: subject
74
+ key: SUBJECT_study_instrument_position
75
+ - inputs:
76
+ entry:
77
+ sources:
78
+ - file: subject
79
+ key: SUBJECT_entry
80
+ position:
81
+ sources:
82
+ - file: subject
83
+ key: SUBJECT_position
84
+ transform: merge_entry_and_position
85
+ transform: strip_jcamp_string
86
+
87
+ scan:
88
+ __meta__:
89
+ name: "scan_info_fallback"
90
+ version: "1.0.0"
91
+ description: "Scan-level fallback info mapping for Bruker datasets."
92
+ category: "info_spec"
93
+ transforms_source: "transform.py"
94
+ Study.Opperator:
95
+ sources:
96
+ - file: visu_pars
97
+ key: VisuStudyReferringPhysician
98
+ transform: strip_jcamp_string
99
+ Study.Date:
100
+ sources:
101
+ - file: visu_pars
102
+ key: VisuStudyDate
103
+ transform:
104
+ - strip_jcamp_string
105
+ - stringtime_to_datetime
106
+ Study.ID:
107
+ sources:
108
+ - file: visu_pars
109
+ key: VisuStudyId
110
+ transform: strip_jcamp_string
111
+ Study.Number:
112
+ sources:
113
+ - file: visu_pars
114
+ key: VisuStudyNumber
115
+ transform: strip_jcamp_string
116
+ Study.InstanceUID:
117
+ sources:
118
+ - file: visu_pars
119
+ key: VisuStudyUid
120
+ transform: strip_jcamp_string
121
+ Subject.ID:
122
+ sources:
123
+ - file: visu_pars
124
+ key: VisuSubjectId
125
+ transform: strip_jcamp_string
126
+ Subject.Name:
127
+ sources:
128
+ - file: visu_pars
129
+ key: VisuSubjectName
130
+ transform: strip_jcamp_string
131
+ Subject.Type:
132
+ sources:
133
+ - file: visu_pars
134
+ key: VisuSubjectType
135
+ transform: strip_jcamp_string
136
+ Subject.Sex:
137
+ sources:
138
+ - file: visu_pars
139
+ key: VisuSubjectSex
140
+ transform: strip_jcamp_string
141
+ Subject.DateOfBirth:
142
+ sources:
143
+ - file: visu_pars
144
+ key: VisuSubjectBirthDate
145
+ transform:
146
+ - strip_jcamp_string
147
+ - stringtime_to_datetime
148
+ Subject.Weight:
149
+ sources:
150
+ - file: visu_pars
151
+ key: VisuSubjectWeight
152
+ transform: strip_jcamp_string
153
+ Subject.Position:
154
+ sources:
155
+ - file: visu_pars
156
+ key: VisuSubjectPosition
@@ -0,0 +1,92 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Union, Optional, Tuple, List, Any, cast
4
+ from datetime import datetime, timezone, timedelta
5
+ import numpy as np
6
+ import re
7
+
8
+ def strip_jcamp_string(value: Optional[str]) -> str:
9
+ if value is None:
10
+ return "Unknown"
11
+ text = str(value).strip()
12
+ if text.startswith("<") and text.endswith(">"):
13
+ text = text[1:-1]
14
+ text = re.sub(r"\^+", " ", text)
15
+ return " ".join(text.split())
16
+
17
+
18
+ def unixtime_to_datetime(value: Union[int, float, Tuple[Union[int, float], ...]]) -> datetime:
19
+ """Convert unix time value to timezone-aware datetime.
20
+
21
+ Accepts:
22
+ - int/float: epoch seconds (local timezone)
23
+ - tuple: (sec,), (sec, ms), or (sec, ms, offset_min)
24
+
25
+ If offset_min is missing, local timezone is used.
26
+ """
27
+ local_tz = datetime.now().astimezone().tzinfo
28
+
29
+ if isinstance(value, (int, float)):
30
+ return datetime.fromtimestamp(value, tz=local_tz)
31
+
32
+ if not isinstance(value, tuple) or not value:
33
+ raise TypeError(f"Unsupported value: {value!r}")
34
+
35
+ sec = value[0]
36
+ ms = value[1] if len(value) > 1 else 0
37
+ offset_min = value[2] if len(value) > 2 else None
38
+
39
+ tz = timezone(timedelta(minutes=offset_min)) if offset_min is not None else local_tz
40
+ return datetime.fromtimestamp(sec, tz=tz).replace(microsecond=int(ms) * 1000)
41
+
42
+
43
+ def stringtime_to_datetime(value: str) -> Union[datetime, str]:
44
+ """Parse PV time strings into datetime.
45
+
46
+ Supported formats:
47
+ - 2026-01-01T12:00:00,873-0500
48
+ - 12:00:00 1 Jan 2026
49
+ - 1 Jan 2026
50
+ - 20260101
51
+ """
52
+ _FORMATS = (
53
+ "%Y-%m-%dT%H:%M:%S,%f%z",
54
+ "%H:%M:%S %d %b %Y",
55
+ "%d %b %Y",
56
+ "%Y%m%d",
57
+ )
58
+
59
+ value = value.strip()
60
+ if len(value) == 0:
61
+ return "Unknown"
62
+ for fmt in _FORMATS:
63
+ try:
64
+ return datetime.strptime(value, fmt)
65
+ except ValueError:
66
+ continue
67
+ raise ValueError(f"Unsupported time format: {value!r}")
68
+
69
+
70
+ def merge_entry_and_position(entry: str, position: str):
71
+ entry = entry.split('_')[-1].replace("First", "")
72
+ position = position.split('_')[-1]
73
+ return f'{entry}_{position}'
74
+
75
+
76
+ def convert_to_list(value: Any) -> List[Any]:
77
+ if value is None:
78
+ return []
79
+ if hasattr(value, "tolist"):
80
+ return cast(Any, value).tolist()
81
+ if isinstance(value, (list, tuple)):
82
+ return list(value)
83
+ return [value]
84
+
85
+
86
+ __all__ = [
87
+ 'strip_jcamp_string',
88
+ 'unixtime_to_datetime',
89
+ 'stringtime_to_datetime',
90
+ 'merge_entry_and_position',
91
+ 'convert_to_list'
92
+ ]