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/config.py ADDED
@@ -0,0 +1,380 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import Any, Dict, Optional, Union
8
+
9
+ import yaml
10
+
11
+ ENV_CONFIG_HOME = "BRKRAW_CONFIG_HOME"
12
+ DEFAULT_PROFILE_DIRNAME = ".brkraw"
13
+ CONFIG_VERSION = 0
14
+
15
+ DEFAULT_CONFIG_YAML = """# brkraw user configuration
16
+ # This file is optional. Delete it to fall back to package defaults.
17
+ # You can override the config root by setting BRKRAW_CONFIG_HOME.
18
+ config_version: 0
19
+
20
+ # Editor command used by brkraw config/addon edit.
21
+ editor: null
22
+
23
+ logging:
24
+ level: INFO
25
+ print_width: 120
26
+
27
+ output:
28
+ # output.layout_entries defines how NIfTI filenames are built.
29
+ layout_entries:
30
+ - key: Subject.ID
31
+ entry: sub
32
+ hide: false
33
+ - key: Study.ID
34
+ entry: study
35
+ hide: false
36
+ - key: ScanID
37
+ entry: scan
38
+ hide: false
39
+ - key: Protocol
40
+ hide: true
41
+ layout_template: null
42
+ slicepack_suffix: "_slpack{index}"
43
+ # float_decimals: 6
44
+
45
+ # Viewer settings for brkraw-viewer (optional GUI extension).
46
+ viewer:
47
+ cache:
48
+ # Cache loaded scan data in memory to speed up space/pose changes.
49
+ enabled: true
50
+ # Maximum number of scan/reco entries to keep (LRU). 0 disables caching.
51
+ max_items: 10
52
+
53
+ # rules_dir: rules
54
+ # specs_dir: specs
55
+ # pruner_specs_dir: pruner_specs
56
+ # transforms_dir: transforms
57
+ """
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class ConfigPaths:
62
+ root: Path
63
+ config_file: Path
64
+ specs_dir: Path
65
+ pruner_specs_dir: Path
66
+ rules_dir: Path
67
+ transforms_dir: Path
68
+
69
+
70
+ def resolve_root(root: Optional[Union[str, Path]] = None) -> Path:
71
+ if root is not None:
72
+ return Path(root).expanduser()
73
+ env_root = os.environ.get(ENV_CONFIG_HOME)
74
+ if env_root:
75
+ return Path(env_root).expanduser()
76
+ return Path.home() / DEFAULT_PROFILE_DIRNAME
77
+
78
+
79
+ def get_paths(root: Optional[Union[str, Path]] = None) -> ConfigPaths:
80
+ base = resolve_root(root)
81
+ return ConfigPaths(
82
+ root=base,
83
+ config_file=base / "config.yaml",
84
+ specs_dir=base / "specs",
85
+ pruner_specs_dir=base / "pruner_specs",
86
+ rules_dir=base / "rules",
87
+ transforms_dir=base / "transforms",
88
+ )
89
+
90
+
91
+ def paths(root: Optional[Union[str, Path]] = None) -> ConfigPaths:
92
+ return get_paths(root=root)
93
+
94
+
95
+ def get_path(name: str, root: Optional[Union[str, Path]] = None) -> Path:
96
+ paths_obj = get_paths(root=root)
97
+ mapping = {
98
+ "root": paths_obj.root,
99
+ "config": paths_obj.config_file,
100
+ "specs": paths_obj.specs_dir,
101
+ "pruner_specs": paths_obj.pruner_specs_dir,
102
+ "rules": paths_obj.rules_dir,
103
+ "transforms": paths_obj.transforms_dir,
104
+ }
105
+ if name not in mapping:
106
+ raise KeyError(f"Unknown config path: {name}")
107
+ return mapping[name]
108
+
109
+
110
+ def is_initialized(root: Optional[Union[str, Path]] = None) -> bool:
111
+ paths = get_paths(root)
112
+ return paths.config_file.exists()
113
+
114
+
115
+ def ensure_initialized(
116
+ root: Optional[Union[str, Path]] = None,
117
+ *,
118
+ create_config: bool = True,
119
+ exist_ok: bool = True,
120
+ ) -> ConfigPaths:
121
+ paths = get_paths(root)
122
+ if paths.root.exists() and not exist_ok:
123
+ raise FileExistsError(paths.root)
124
+ paths.root.mkdir(parents=True, exist_ok=True)
125
+ paths.specs_dir.mkdir(parents=True, exist_ok=True)
126
+ paths.pruner_specs_dir.mkdir(parents=True, exist_ok=True)
127
+ paths.rules_dir.mkdir(parents=True, exist_ok=True)
128
+ paths.transforms_dir.mkdir(parents=True, exist_ok=True)
129
+ if create_config and not paths.config_file.exists():
130
+ paths.config_file.write_text(DEFAULT_CONFIG_YAML, encoding="utf-8")
131
+ return paths
132
+
133
+
134
+ def init(
135
+ root: Optional[Union[str, Path]] = None,
136
+ *,
137
+ create_config: bool = True,
138
+ exist_ok: bool = True,
139
+ ) -> ConfigPaths:
140
+ return ensure_initialized(root=root, create_config=create_config, exist_ok=exist_ok)
141
+
142
+
143
+ def load_config(root: Optional[Union[str, Path]] = None) -> Optional[Dict[str, Any]]:
144
+ paths = get_paths(root)
145
+ if not paths.config_file.exists():
146
+ return None
147
+ with paths.config_file.open("r", encoding="utf-8") as handle:
148
+ data = yaml.safe_load(handle)
149
+ if data is None:
150
+ return {}
151
+ if not isinstance(data, dict):
152
+ raise ValueError("config.yaml must contain a YAML mapping at the top level.")
153
+ return data
154
+
155
+
156
+ def load(root: Optional[Union[str, Path]] = None) -> Optional[Dict[str, Any]]:
157
+ return load_config(root=root)
158
+
159
+
160
+ def write_config(data: Dict[str, Any], root: Optional[Union[str, Path]] = None) -> None:
161
+ data = _normalize_config(dict(data))
162
+ data["config_version"] = CONFIG_VERSION
163
+ paths = ensure_initialized(root=root, create_config=False, exist_ok=True)
164
+ paths.config_file.write_text(
165
+ yaml.safe_dump(data, sort_keys=False),
166
+ encoding="utf-8",
167
+ )
168
+
169
+
170
+ def reset_config(root: Optional[Union[str, Path]] = None) -> None:
171
+ paths = ensure_initialized(root=root, create_config=False, exist_ok=True)
172
+ paths.config_file.write_text(DEFAULT_CONFIG_YAML, encoding="utf-8")
173
+
174
+
175
+ def default_config() -> Dict[str, Any]:
176
+ data = yaml.safe_load(DEFAULT_CONFIG_YAML)
177
+ if not isinstance(data, dict):
178
+ return {}
179
+ return _normalize_config(data)
180
+
181
+
182
+ def resolve_config(root: Optional[Union[str, Path]] = None) -> Dict[str, Any]:
183
+ defaults = default_config()
184
+ overrides = _normalize_config(load(root=root) or {})
185
+ overrides.pop("nifti_filename_template", None)
186
+ overrides.pop("output_format", None)
187
+ overrides["config_version"] = CONFIG_VERSION
188
+ defaults.pop("nifti_filename_template", None)
189
+ defaults.pop("output_format", None)
190
+ return _deep_merge(defaults, overrides)
191
+
192
+
193
+ def resolve_editor_binary(root: Optional[Union[str, Path]] = None) -> Optional[str]:
194
+ config = resolve_config(root=root)
195
+ editor = config.get("editor")
196
+ if not isinstance(editor, str) or not editor.strip():
197
+ editor = config.get("editor_binary")
198
+ if isinstance(editor, str) and editor.strip():
199
+ return editor.strip()
200
+ env_editor = os.environ.get("VISUAL") or os.environ.get("EDITOR")
201
+ if isinstance(env_editor, str) and env_editor.strip():
202
+ return env_editor.strip()
203
+ return None
204
+
205
+
206
+ def clear_config(
207
+ root: Optional[Union[str, Path]] = None,
208
+ *,
209
+ keep_config: bool = False,
210
+ keep_rules: bool = False,
211
+ keep_specs: bool = False,
212
+ keep_pruner_specs: bool = False,
213
+ keep_transforms: bool = False,
214
+ ) -> None:
215
+ paths = get_paths(root=root)
216
+ if not paths.root.exists():
217
+ return
218
+ if paths.config_file.exists() and not keep_config:
219
+ paths.config_file.unlink()
220
+ if paths.rules_dir.exists() and not keep_rules:
221
+ _remove_tree(paths.rules_dir)
222
+ if paths.specs_dir.exists() and not keep_specs:
223
+ _remove_tree(paths.specs_dir)
224
+ if paths.pruner_specs_dir.exists() and not keep_pruner_specs:
225
+ _remove_tree(paths.pruner_specs_dir)
226
+ if paths.transforms_dir.exists() and not keep_transforms:
227
+ _remove_tree(paths.transforms_dir)
228
+ try:
229
+ paths.root.rmdir()
230
+ except OSError:
231
+ pass
232
+
233
+
234
+ def clear(
235
+ root: Optional[Union[str, Path]] = None,
236
+ *,
237
+ keep_config: bool = False,
238
+ keep_rules: bool = False,
239
+ keep_specs: bool = False,
240
+ keep_pruner_specs: bool = False,
241
+ keep_transforms: bool = False,
242
+ ) -> None:
243
+ clear_config(
244
+ root=root,
245
+ keep_config=keep_config,
246
+ keep_rules=keep_rules,
247
+ keep_specs=keep_specs,
248
+ keep_pruner_specs=keep_pruner_specs,
249
+ keep_transforms=keep_transforms,
250
+ )
251
+
252
+
253
+ def configure_logging(
254
+ *,
255
+ root: Optional[Union[str, Path]] = None,
256
+ level: Optional[Union[str, int]] = None,
257
+ stream=None,
258
+ ) -> logging.Logger:
259
+ config = resolve_config(root=root)
260
+ if level is None:
261
+ level = config.get("logging", {}).get("level", "INFO")
262
+ if isinstance(level, str):
263
+ level = getattr(logging, level.upper(), logging.INFO)
264
+ if not logging.getLogger().handlers:
265
+ if level == logging.INFO:
266
+ fmt = "%(message)s"
267
+ else:
268
+ fmt = "%(levelname)s %(asctime)s %(message)s"
269
+ logging.basicConfig(level=level, format=fmt, stream=stream)
270
+ return logging.getLogger("brkraw")
271
+
272
+
273
+ def output_width(root: Optional[Union[str, Path]] = None, default: int = 120) -> int:
274
+ config = resolve_config(root=root)
275
+ width = config.get("logging", {}).get("print_width", default)
276
+ try:
277
+ return int(width)
278
+ except (TypeError, ValueError):
279
+ return default
280
+
281
+
282
+ def float_decimals(root: Optional[Union[str, Path]] = None, default: int = 6) -> int:
283
+ config = resolve_config(root=root)
284
+ output_cfg = config.get("output", {})
285
+ decimals = output_cfg.get("float_decimals", config.get("float_decimals", default))
286
+ try:
287
+ return int(decimals)
288
+ except (TypeError, ValueError):
289
+ return default
290
+
291
+
292
+ def affine_decimals(root: Optional[Union[str, Path]] = None, default: int = 6) -> int:
293
+ return float_decimals(root=root, default=default)
294
+
295
+
296
+ def layout_template(
297
+ root: Optional[Union[str, Path]] = None,
298
+ ) -> Optional[str]:
299
+ config = resolve_config(root=root)
300
+ output_cfg = config.get("output", {})
301
+ value = output_cfg.get("layout_template")
302
+ if isinstance(value, str) and value.strip():
303
+ return value
304
+ return None
305
+
306
+
307
+ def layout_entries(
308
+ root: Optional[Union[str, Path]] = None,
309
+ default: Optional[list] = None,
310
+ ) -> list:
311
+ config = resolve_config(root=root)
312
+ output_cfg = config.get("output", {})
313
+ fields = output_cfg.get("layout_entries")
314
+ if fields is None:
315
+ fields = output_cfg.get("layout_fields")
316
+ if fields is None:
317
+ fields = output_cfg.get("format_fields")
318
+ if isinstance(fields, list):
319
+ return fields
320
+ if default is None:
321
+ default = default_config().get("output", {}).get("layout_entries", [])
322
+ return list(default) if isinstance(default, list) else []
323
+
324
+
325
+ def output_slicepack_suffix(
326
+ root: Optional[Union[str, Path]] = None,
327
+ default: str = "_slpack{index}",
328
+ ) -> str:
329
+ config = resolve_config(root=root)
330
+ value = config.get("output", {}).get("slicepack_suffix", default)
331
+ return str(value) if isinstance(value, str) and value else default
332
+
333
+
334
+ def _normalize_config(data: Dict[str, Any]) -> Dict[str, Any]:
335
+ config = dict(data)
336
+ logging_cfg = dict(config.get("logging") or {})
337
+ output_cfg = dict(config.get("output") or {})
338
+
339
+ config.pop("output_format", None)
340
+ if "log_level" in config and "level" not in logging_cfg:
341
+ logging_cfg["level"] = config.pop("log_level")
342
+ if "output_width" in config and "print_width" not in logging_cfg:
343
+ logging_cfg["print_width"] = config.pop("output_width")
344
+ config.pop("output_format_fields", None)
345
+ config.pop("output_format_spec", None)
346
+ if "layout_fields" in output_cfg and "layout_entries" not in output_cfg:
347
+ output_cfg["layout_entries"] = output_cfg["layout_fields"]
348
+ if "float_decimals" in config and "float_decimals" not in output_cfg:
349
+ output_cfg["float_decimals"] = config.pop("float_decimals")
350
+ if "editor_binary" in config and "editor" not in config:
351
+ config["editor"] = config.pop("editor_binary")
352
+
353
+ if logging_cfg:
354
+ config["logging"] = logging_cfg
355
+ if output_cfg:
356
+ config["output"] = output_cfg
357
+ return config
358
+
359
+
360
+ def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
361
+ merged = dict(base)
362
+ for key, value in override.items():
363
+ if (
364
+ key in merged
365
+ and isinstance(merged[key], dict)
366
+ and isinstance(value, dict)
367
+ ):
368
+ merged[key] = _deep_merge(merged[key], value)
369
+ else:
370
+ merged[key] = value
371
+ return merged
372
+
373
+
374
+ def _remove_tree(path: Path) -> None:
375
+ for child in path.iterdir():
376
+ if child.is_dir():
377
+ _remove_tree(child)
378
+ else:
379
+ child.unlink()
380
+ path.rmdir()
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib.metadata
4
+ from typing import Optional, List, Any, cast
5
+
6
+
7
+ def list_entry_points(group: str, name: Optional[str] = None) -> List[importlib.metadata.EntryPoint]:
8
+ """List installed entry points for a group/name.
9
+
10
+ Args:
11
+ group: Entry point group name.
12
+ name: Optional entry point name filter.
13
+
14
+ Returns:
15
+ List of matching entry points.
16
+ """
17
+ eps = importlib.metadata.entry_points()
18
+ if hasattr(eps, "select"):
19
+ eps_any = cast(Any, eps)
20
+ if name is not None:
21
+ return list(eps_any.select(group=group, name=name))
22
+ return list(eps_any.select(group=group))
23
+ if name is not None:
24
+ return [ep for ep in eps.get(group, []) if ep.name == name] # type: ignore[call-arg,attr-defined]
25
+ return list(eps.get(group, [])) # type: ignore[call-arg,attr-defined]