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,288 @@
1
+ """Formatting helpers for loader info output.
2
+
3
+ Last updated: 2025-12-30
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from collections.abc import Mapping, Sequence
9
+ import textwrap
10
+ from typing import Any, Optional, List, Dict
11
+
12
+ import numpy as np
13
+
14
+ from ...core import formatter as formatter_core
15
+
16
+
17
+ def _format_value(value: Any, *, float_decimals: Optional[int] = None) -> str:
18
+ """Format values into a human-readable string.
19
+
20
+ Args:
21
+ value: Value to format.
22
+ float_decimals: Decimal precision for floats.
23
+
24
+ Returns:
25
+ String representation of the value.
26
+ """
27
+ if isinstance(value, Mapping):
28
+ return ", ".join(
29
+ f"{k}={_format_value(v, float_decimals=float_decimals)}"
30
+ for k, v in value.items()
31
+ )
32
+ if isinstance(value, np.ndarray):
33
+ if float_decimals is not None and np.issubdtype(value.dtype, np.floating):
34
+ return str(np.round(value, float_decimals).tolist())
35
+ return str(value.tolist())
36
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes)):
37
+ return "[" + ", ".join(_format_value(v, float_decimals=float_decimals) for v in value) + "]"
38
+ if float_decimals is not None and isinstance(value, (float, np.floating)):
39
+ return f"{value:.{float_decimals}f}"
40
+ return str(value)
41
+
42
+
43
+ def _kv_rows(data: Mapping[str, Any], *, float_decimals: Optional[int]) -> List[Dict[str, str]]:
44
+ """Convert a mapping into table row dicts.
45
+
46
+ Args:
47
+ data: Source mapping.
48
+ float_decimals: Decimal precision for floats.
49
+
50
+ Returns:
51
+ List of row dictionaries with field/value keys.
52
+ """
53
+ return [
54
+ {"field": str(key), "value": _format_value(val, float_decimals=float_decimals)}
55
+ for key, val in data.items()
56
+ ]
57
+
58
+
59
+ def format_info_tables(
60
+ info: Mapping[str, Any],
61
+ *,
62
+ width: int = 80,
63
+ indent: int = 0,
64
+ scan_indent: int = 2,
65
+ reco_indent: int = 4,
66
+ scan_transpose: bool = False,
67
+ float_decimals: Optional[int] = None,
68
+ ) -> str:
69
+ """Format study/subject/scan info into nested tables.
70
+
71
+ Args:
72
+ info: Mapping containing Study/Subject/Scan(s) blocks.
73
+ width: Total table width for layout.
74
+ indent: Base indent for the first-level tables.
75
+ scan_indent: Extra indent for scan blocks.
76
+ reco_indent: Extra indent for reco blocks.
77
+ scan_transpose: If True, print scan fields in a transposed layout.
78
+ float_decimals: Decimal precision for float values.
79
+
80
+ Returns:
81
+ Rendered table string.
82
+ """
83
+ blocks: List[str] = []
84
+ study = info.get("Study")
85
+ if isinstance(study, Mapping) and study:
86
+ blocks.append(
87
+ formatter_core.format_table(
88
+ "Study",
89
+ ("field", "value"),
90
+ _kv_rows(study, float_decimals=float_decimals),
91
+ width=width,
92
+ )
93
+ )
94
+ subject = info.get("Subject")
95
+ if isinstance(subject, Mapping) and subject:
96
+ blocks.append(
97
+ formatter_core.format_table(
98
+ "Subject",
99
+ ("field", "value"),
100
+ _kv_rows(subject, float_decimals=float_decimals),
101
+ width=width,
102
+ )
103
+ )
104
+ scans = info.get("Scan(s)", {})
105
+ if isinstance(scans, Mapping):
106
+ scan_blocks: List[str] = []
107
+ if scans:
108
+ scan_blocks.append("[ Scan(s) ]")
109
+ scan_items = list(scans.items())
110
+ for idx, (scan_id, scan_data) in enumerate(scan_items):
111
+ if not isinstance(scan_data, Mapping):
112
+ continue
113
+ scan_fields = {k: v for k, v in scan_data.items() if k != "Reco(s)"}
114
+ if scan_transpose:
115
+ scan_blocks.extend(
116
+ _format_scan_transposed(
117
+ scan_id,
118
+ scan_fields,
119
+ width=width,
120
+ indent=indent + scan_indent,
121
+ float_decimals=float_decimals,
122
+ )
123
+ )
124
+ else:
125
+ scan_fields = {"ScanID": scan_id, **scan_fields}
126
+ scan_table = formatter_core.format_table(
127
+ "",
128
+ ("field", "value"),
129
+ _kv_rows(scan_fields, float_decimals=float_decimals),
130
+ width=width,
131
+ )
132
+ scan_blocks.append(textwrap.indent(scan_table, " " * (indent + scan_indent)))
133
+
134
+ recos = scan_data.get("Reco(s)", {})
135
+ if isinstance(recos, Mapping) and recos:
136
+ reco_rows = []
137
+ for reco_id, reco_data in recos.items():
138
+ if isinstance(reco_data, Mapping):
139
+ value = _format_value(
140
+ reco_data.get("Type", reco_data),
141
+ float_decimals=float_decimals,
142
+ )
143
+ else:
144
+ value = _format_value(reco_data, float_decimals=float_decimals)
145
+ reco_rows.append(
146
+ {"RecoID": {"value": str(reco_id), "align": "center"}, "value": value}
147
+ )
148
+ reco_table = formatter_core.format_table(
149
+ "Reco(s)",
150
+ ("RecoID", "value"),
151
+ reco_rows,
152
+ width=width,
153
+ )
154
+ scan_blocks.append(textwrap.indent(reco_table, " " * (indent + reco_indent)))
155
+ if idx < len(scan_items) - 1:
156
+ scan_blocks.append("")
157
+ if scan_blocks:
158
+ blocks.append("\n".join(scan_blocks))
159
+ return "\n\n".join(blocks)
160
+
161
+
162
+ def _format_scan_transposed(
163
+ scan_id: Any,
164
+ scan_fields: Mapping[str, Any],
165
+ *,
166
+ width: int,
167
+ indent: int,
168
+ float_decimals: Optional[int],
169
+ ) -> List[str]:
170
+ """Render scan fields as a transposed table layout.
171
+
172
+ Args:
173
+ scan_id: Scan identifier.
174
+ scan_fields: Mapping of scan field names to values.
175
+ width: Total table width.
176
+ indent: Base indent for the table.
177
+ float_decimals: Decimal precision for floats.
178
+
179
+ Returns:
180
+ List of formatted table blocks.
181
+ """
182
+ columns = [str(key) for key in scan_fields.keys()]
183
+ values = {
184
+ str(key): _format_value(val, float_decimals=float_decimals)
185
+ for key, val in scan_fields.items()
186
+ }
187
+ grouped = _chunk_columns(columns, values, width=width, gap=2, base_cols=["ScanID"])
188
+ blocks: List[str] = []
189
+ scan_id_width = max(len("ScanID"), len(str(scan_id)))
190
+ for idx, group in enumerate(grouped):
191
+ if idx == 0:
192
+ title = ""
193
+ cols = ["ScanID"] + group
194
+ row: Dict[str, Any] = {"ScanID": {"value": str(scan_id), "align": "center"}}
195
+ else:
196
+ title = ""
197
+ cols = group
198
+ row = {}
199
+ row.update({name: values.get(name, "") for name in group})
200
+ table = formatter_core.format_table(
201
+ title,
202
+ cols,
203
+ [row],
204
+ width=width,
205
+ wrap_last=False,
206
+ )
207
+ lines = table.splitlines()
208
+ if idx != 0:
209
+ if lines and lines[0].startswith("["):
210
+ lines = lines[1:]
211
+ table = "\n".join(lines)
212
+ extra_indent = scan_id_width + 2
213
+ blocks.append(textwrap.indent(table, " " * (indent + extra_indent)))
214
+ else:
215
+ blocks.append(textwrap.indent(table, " " * indent))
216
+ return blocks
217
+
218
+
219
+ def _chunk_columns(
220
+ columns: List[str],
221
+ values: Mapping[str, str],
222
+ *,
223
+ width: int,
224
+ gap: int,
225
+ base_cols: List[str],
226
+ ) -> List[List[str]]:
227
+ """Group columns so each table fits within the width.
228
+
229
+ Args:
230
+ columns: Column names to group.
231
+ values: Mapping of column values used for width estimation.
232
+ width: Maximum table width.
233
+ gap: Spacing between columns.
234
+ base_cols: Columns always included in width calculations.
235
+
236
+ Returns:
237
+ List of grouped column name lists.
238
+ """
239
+ groups: List[List[str]] = []
240
+ current: List[str] = []
241
+ for col in columns:
242
+ candidate = current + [col]
243
+ if _fits_width(candidate, values, width=width, gap=gap, base_cols=base_cols):
244
+ current = candidate
245
+ else:
246
+ if current:
247
+ groups.append(current)
248
+ current = [col]
249
+ if current:
250
+ groups.append(current)
251
+ return groups
252
+
253
+
254
+ def _fits_width(
255
+ columns: List[str],
256
+ values: Mapping[str, str],
257
+ *,
258
+ width: int,
259
+ gap: int,
260
+ base_cols: List[str],
261
+ ) -> bool:
262
+ """Check if columns fit within a target width.
263
+
264
+ Args:
265
+ columns: Column names to measure.
266
+ values: Mapping of column values used for width estimation.
267
+ width: Maximum table width.
268
+ gap: Spacing between columns.
269
+ base_cols: Columns always included in width calculations.
270
+
271
+ Returns:
272
+ True if the columns fit within the width.
273
+ """
274
+ cols = base_cols + columns
275
+ total = 0
276
+ for idx, col in enumerate(cols):
277
+ value_len = len(values.get(col, "")) if col in values else 0
278
+ col_width = max(len(col), value_len)
279
+ total += col_width
280
+ if idx < len(cols) - 1:
281
+ total += gap
282
+ return total <= width
283
+
284
+
285
+ __all__ = ["format_info_tables"]
286
+
287
+ def __dir__() -> List[str]:
288
+ return sorted(__all__)