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,367 @@
1
+ """Render mapping/sequence data into structured, indented text using templates.
2
+
3
+ This module provides a small template renderer that supports:
4
+ - Mapping/sequence rendering via Python format strings.
5
+ - Optional wrapping and indentation.
6
+ - Per-value formatting specs for alignment, padding, repetition, and ANSI colors.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import textwrap
12
+ from collections.abc import Mapping, Sequence
13
+ from string import Formatter
14
+ import re
15
+ from typing import Any, Callable, Literal, List, Dict, Optional, Union
16
+
17
+ MissingPolicy = Literal["error", "skip", "placeholder"]
18
+ FilterFunc = Callable[[Any], str]
19
+
20
+ _SPECIAL_VALUE_KEYS = {
21
+ "value",
22
+ "pattern",
23
+ "repeat",
24
+ "align",
25
+ "size",
26
+ "fill",
27
+ "gap",
28
+ "color",
29
+ "underline",
30
+ "bold",
31
+ "italic",
32
+ }
33
+ _ANSI_COLORS = {
34
+ "black": "30",
35
+ "red": "31",
36
+ "green": "32",
37
+ "yellow": "33",
38
+ "blue": "34",
39
+ "magenta": "35",
40
+ "cyan": "36",
41
+ "white": "37",
42
+ "gray": "90",
43
+ "reset": "0",
44
+ }
45
+ _ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
46
+
47
+
48
+ class _SafeFormatter(Formatter):
49
+ """Formatter that blocks dunder access and supports mapping-based lookups."""
50
+
51
+ def get_field(self, field_name: str, args: Sequence[Any], kwargs: Mapping[str, Any]):
52
+ if "__" in field_name:
53
+ raise KeyError(field_name)
54
+ return super().get_field(field_name, args, kwargs)
55
+
56
+ def get_value(self, key, args, kwargs):
57
+ return super().get_value(key, args, kwargs)
58
+
59
+
60
+ def _apply_color(text: str, color: Optional[str]) -> str:
61
+ if not color:
62
+ return text
63
+ code = _ANSI_COLORS.get(color, color)
64
+ return f"\033[{code}m{text}\033[0m"
65
+
66
+
67
+ def _apply_style(
68
+ text: str,
69
+ *,
70
+ underline: bool = False,
71
+ bold: bool = False,
72
+ italic: bool = False,
73
+ ) -> str:
74
+ if not any((underline, bold, italic)):
75
+ return text
76
+ codes = []
77
+ if underline:
78
+ codes.append("4")
79
+ if bold:
80
+ codes.append("1")
81
+ if italic:
82
+ codes.append("3")
83
+ return f"\033[{';'.join(codes)}m{text}\033[0m"
84
+
85
+
86
+ def _visible_len(text: str) -> int:
87
+ return len(_ANSI_ESCAPE_RE.sub("", text))
88
+
89
+
90
+ def _pad_text(text: str, width: int, align: str, fill_char: str) -> str:
91
+ visible_len = _visible_len(text)
92
+ if width <= visible_len:
93
+ return text
94
+ pad = width - visible_len
95
+ if align == "right":
96
+ return f"{fill_char * pad}{text}"
97
+ if align == "center":
98
+ left = pad // 2
99
+ right = pad - left
100
+ return f"{fill_char * left}{text}{fill_char * right}"
101
+ return f"{text}{fill_char * pad}"
102
+
103
+
104
+ def _format_special_value(spec: Mapping[str, Any]) -> str:
105
+ base = spec.get("pattern", spec.get("value", ""))
106
+ text = str(base)
107
+ if "repeat" in spec:
108
+ text = text * int(spec["repeat"])
109
+
110
+ size = spec.get("size")
111
+ if size is None:
112
+ styled = _apply_color(text, spec.get("color"))
113
+ return _apply_style(
114
+ styled,
115
+ underline=bool(spec.get("underline", False)),
116
+ bold=bool(spec.get("bold", False)),
117
+ italic=bool(spec.get("italic", False)),
118
+ )
119
+
120
+ align = spec.get("align", "left")
121
+ fill = spec.get("fill", spec.get("gap", " "))
122
+ if not fill:
123
+ fill = " "
124
+ fill_char = str(fill)[0]
125
+ width = int(size)
126
+
127
+ aligned = _pad_text(text, width, align, fill_char)
128
+ styled = _apply_color(aligned, spec.get("color"))
129
+ return _apply_style(
130
+ styled,
131
+ underline=bool(spec.get("underline", False)),
132
+ bold=bool(spec.get("bold", False)),
133
+ italic=bool(spec.get("italic", False)),
134
+ )
135
+
136
+
137
+ def _apply_filters(value: Any, filters: Optional[Mapping[str, FilterFunc]]) -> str:
138
+ if isinstance(value, Mapping) and _SPECIAL_VALUE_KEYS.intersection(value):
139
+ return _format_special_value(value)
140
+ if filters is None:
141
+ return str(value)
142
+ if isinstance(value, str):
143
+ return value
144
+ type_name = type(value).__name__
145
+ if type_name in filters:
146
+ return filters[type_name](value)
147
+ return str(value)
148
+
149
+
150
+ def _render_item(
151
+ item: Mapping[str, Any],
152
+ template: str,
153
+ formatter: _SafeFormatter,
154
+ on_missing: MissingPolicy,
155
+ placeholder: str,
156
+ filters: Optional[Mapping[str, FilterFunc]],
157
+ ) -> str:
158
+ class Proxy(dict):
159
+ def __missing__(self, key):
160
+ if on_missing == "error":
161
+ raise KeyError(key)
162
+ if on_missing == "skip":
163
+ return None
164
+ return placeholder
165
+
166
+ def __getitem__(self, key):
167
+ missing = False
168
+ try:
169
+ val = super().__getitem__(key)
170
+ except KeyError:
171
+ val = self.__missing__(key)
172
+ missing = True
173
+ if missing and val is None and on_missing == "skip":
174
+ raise KeyError(key)
175
+ return _apply_filters(val, filters)
176
+
177
+ proxy = Proxy(item)
178
+ try:
179
+ return formatter.vformat(template, (), proxy)
180
+ except KeyError:
181
+ if on_missing == "skip":
182
+ return ""
183
+ raise
184
+
185
+
186
+ def format_data(
187
+ data: Union[Mapping[str, Any], Sequence[Mapping[str, Any]]],
188
+ template: str,
189
+ *,
190
+ indent: int = 0,
191
+ width: Optional[int] = None,
192
+ on_missing: MissingPolicy = "error",
193
+ placeholder: str = "?",
194
+ max_output_length: Optional[int] = None,
195
+ filters: Optional[Mapping[str, FilterFunc]] = None,
196
+ ) -> str:
197
+ formatter = _SafeFormatter()
198
+ if isinstance(data, Mapping):
199
+ rendered_items = [_render_item(data, template, formatter, on_missing, placeholder, filters)]
200
+ elif isinstance(data, Sequence) and not isinstance(data, (str, bytes)):
201
+ rendered_items = []
202
+ for item in data:
203
+ if not isinstance(item, Mapping):
204
+ raise TypeError("Sequence items must be mappings for templating.")
205
+ rendered = _render_item(item, template, formatter, on_missing, placeholder, filters)
206
+ if rendered:
207
+ rendered_items.append(rendered)
208
+ else:
209
+ raise TypeError("data must be a mapping or a sequence of mappings.")
210
+
211
+ joined = "\n".join(rendered_items)
212
+ if width:
213
+ joined = "\n".join(textwrap.fill(line, width=width) for line in joined.splitlines())
214
+
215
+ result = textwrap.indent(joined, " " * indent) if indent else joined
216
+ if max_output_length is not None and len(result) > max_output_length:
217
+ result = result[: max_output_length - 3] + "..."
218
+ return result
219
+
220
+
221
+ def _cell_value(cell: Any) -> str:
222
+ if isinstance(cell, Mapping) and "value" in cell:
223
+ return str(cell.get("value", ""))
224
+ return str(cell)
225
+
226
+
227
+ def _cell_align(cell: Any) -> str:
228
+ if isinstance(cell, Mapping):
229
+ align = cell.get("align")
230
+ if align in {"left", "right", "center"}:
231
+ return align
232
+ return "left"
233
+
234
+
235
+ def _cell_color(cell: Any, default_color: Optional[str]) -> Optional[str]:
236
+ if isinstance(cell, Mapping) and "color" in cell:
237
+ return cell.get("color") # type: ignore[return-value]
238
+ return default_color
239
+
240
+
241
+ def compute_column_widths(
242
+ columns: Sequence[str],
243
+ rows: Sequence[Mapping[str, Any]],
244
+ *,
245
+ include_header: bool = True,
246
+ wrap_last: bool = True,
247
+ ) -> Dict[str, int]:
248
+ widths: Dict[str, int] = {}
249
+ target_cols = columns[:-1] if wrap_last and columns else columns
250
+ if include_header:
251
+ for col in target_cols:
252
+ widths[col] = len(col.upper())
253
+ for row in rows:
254
+ for col in target_cols:
255
+ widths[col] = max(widths.get(col, 0), len(_cell_value(row.get(col, ""))))
256
+ return widths
257
+
258
+
259
+ def format_table(
260
+ title: str,
261
+ columns: Sequence[str],
262
+ rows: Sequence[Mapping[str, Any]],
263
+ *,
264
+ width: int = 80,
265
+ colors: Optional[Mapping[str, str]] = None,
266
+ title_color: Optional[str] = None,
267
+ col_widths: Optional[Mapping[str, int]] = None,
268
+ gap: int = 2,
269
+ wrap_last: bool = True,
270
+ min_last_col_width: int = 30,
271
+ ) -> str:
272
+ if not columns:
273
+ return ""
274
+ if col_widths is None:
275
+ col_widths = compute_column_widths(columns, rows, include_header=True, wrap_last=wrap_last)
276
+
277
+ header_cols = columns[:-1] if wrap_last else columns
278
+ header_row = (" " * gap).join(
279
+ _apply_style(
280
+ _pad_text(col, col_widths.get(col, len(col)), "center", " "),
281
+ underline=True,
282
+ )
283
+ for col in header_cols
284
+ )
285
+ if wrap_last:
286
+ header_row += (" " * gap) + _apply_style(
287
+ _pad_text(columns[-1], col_widths.get(columns[-1], len(columns[-1])), "center", " "),
288
+ underline=True,
289
+ )
290
+
291
+ lines: List[str] = []
292
+ if title:
293
+ title_text = _apply_color(f"[ {title} ]", title_color)
294
+ lines.append(title_text)
295
+ lines.append(header_row)
296
+
297
+ if not rows:
298
+ lines.append("(none)")
299
+ return "\n".join(lines)
300
+
301
+ for row in rows:
302
+ prefix_parts = []
303
+ for col in header_cols:
304
+ value = _cell_value(row.get(col, ""))
305
+ align = _cell_align(row.get(col, ""))
306
+ padded = _pad_text(value, col_widths.get(col, len(value)), align, " ")
307
+ color = _cell_color(row.get(col, ""), colors.get(col) if colors else None)
308
+ prefix_parts.append(_apply_color(padded, color))
309
+
310
+ prefix = (" " * gap).join(prefix_parts)
311
+ prefix_plain_parts = []
312
+ for col in header_cols:
313
+ value = _cell_value(row.get(col, ""))
314
+ align = _cell_align(row.get(col, ""))
315
+ prefix_plain_parts.append(_pad_text(value, col_widths.get(col, len(value)), align, " "))
316
+ prefix_plain = (" " * gap).join(prefix_plain_parts)
317
+
318
+ if wrap_last:
319
+ desc = _cell_value(row.get(columns[-1], ""))
320
+ desc_color = _cell_color(row.get(columns[-1], ""), colors.get(columns[-1]) if colors else None)
321
+ indent = " " * (len(prefix_plain) + gap)
322
+ wrap_width = max(1, width - len(prefix_plain) - gap)
323
+ if desc:
324
+ if wrap_width < min_last_col_width:
325
+ lines.append(prefix)
326
+ wrapped = textwrap.fill(
327
+ desc,
328
+ width=width,
329
+ initial_indent=" " * gap,
330
+ subsequent_indent=" " * gap,
331
+ break_long_words=False,
332
+ break_on_hyphens=False,
333
+ )
334
+ for line in wrapped.splitlines():
335
+ lines.append(_apply_color(line, desc_color))
336
+ else:
337
+ wrapped = textwrap.fill(
338
+ desc,
339
+ width=wrap_width,
340
+ initial_indent="",
341
+ subsequent_indent="",
342
+ break_long_words=False,
343
+ break_on_hyphens=False,
344
+ )
345
+ wrapped_lines = wrapped.splitlines()
346
+ lines.append(f"{prefix}{' ' * gap}{_apply_color(wrapped_lines[0], desc_color)}")
347
+ for extra in wrapped_lines[1:]:
348
+ lines.append(f"{indent}{_apply_color(extra, desc_color)}")
349
+ else:
350
+ lines.append(prefix)
351
+ else:
352
+ lines.append(prefix)
353
+
354
+ return "\n".join(lines)
355
+
356
+
357
+ __all__ = ["format_data", "format_table", "compute_column_widths"]
358
+
359
+
360
+ def __dir__() -> List[str]:
361
+ return sorted(__all__)
362
+
363
+
364
+ if __name__ == "__main__":
365
+ import doctest
366
+
367
+ doctest.testmod()