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,106 @@
1
+ from __future__ import annotations
2
+
3
+ """Parameter search command for BrkRaw.
4
+
5
+ Last updated: 2025-12-30
6
+ """
7
+
8
+ import argparse
9
+ import logging
10
+ import os
11
+ from pathlib import Path
12
+ import yaml
13
+ import numpy as np
14
+
15
+ from brkraw.cli.utils import load
16
+
17
+ logger = logging.getLogger("brkraw")
18
+
19
+
20
+ def cmd_params(args: argparse.Namespace) -> int:
21
+ if args.path is None:
22
+ args.path = os.environ.get("BRKRAW_PATH")
23
+ if args.path is None:
24
+ args.parser.print_help()
25
+ return 2
26
+ if args.key is None:
27
+ args.key = os.environ.get("BRKRAW_PARAM_KEY")
28
+ if args.key is None:
29
+ logger.error("Missing --key (or BRKRAW_PARAM_KEY).")
30
+ return 2
31
+ if args.scan_id is None:
32
+ env_scan = os.environ.get("BRKRAW_SCAN_ID")
33
+ if env_scan:
34
+ try:
35
+ args.scan_id = int(env_scan)
36
+ except ValueError:
37
+ logger.error("Invalid BRKRAW_SCAN_ID: %s", env_scan)
38
+ return 2
39
+ if args.reco_id is None:
40
+ env_reco = os.environ.get("BRKRAW_RECO_ID")
41
+ if env_reco:
42
+ try:
43
+ args.reco_id = int(env_reco)
44
+ except ValueError:
45
+ logger.error("Invalid BRKRAW_RECO_ID: %s", env_reco)
46
+ return 2
47
+ if args.file is None:
48
+ env_file = os.environ.get("BRKRAW_PARAM_FILE")
49
+ if env_file:
50
+ args.file = [p.strip() for p in env_file.split(",") if p.strip()]
51
+ if not Path(args.path).exists():
52
+ logger.error("Path not found: %s", args.path)
53
+ return 2
54
+ loader = load(args.path, prefix="Loading")
55
+ result = loader.search_params(
56
+ args.key,
57
+ file=args.file,
58
+ scan_id=args.scan_id,
59
+ reco_id=args.reco_id,
60
+ )
61
+ if result is None:
62
+ logger.info("(none)")
63
+ return 0
64
+ def _to_yaml_safe(value):
65
+ if isinstance(value, dict):
66
+ return {k: _to_yaml_safe(v) for k, v in value.items()}
67
+ if isinstance(value, (list, tuple)):
68
+ return [_to_yaml_safe(v) for v in value]
69
+ if isinstance(value, np.ndarray):
70
+ return value.tolist()
71
+ return value
72
+
73
+ logger.info("%s", yaml.safe_dump(_to_yaml_safe(result), sort_keys=False))
74
+ return 0
75
+
76
+
77
+ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[name-defined]
78
+ params_parser = subparsers.add_parser(
79
+ "params",
80
+ help="Search parameter files for key matches.",
81
+ )
82
+ params_parser.add_argument("path", nargs="?", help="Path to the Bruker study.")
83
+ params_parser.add_argument(
84
+ "-k",
85
+ "--key",
86
+ help="Parameter key to search for.",
87
+ )
88
+ params_parser.add_argument(
89
+ "-s",
90
+ "--scan-id",
91
+ type=int,
92
+ help="Scan id to search (required for study-level search).",
93
+ )
94
+ params_parser.add_argument(
95
+ "-r",
96
+ "--reco-id",
97
+ type=int,
98
+ help="Reco id to search within a scan.",
99
+ )
100
+ params_parser.add_argument(
101
+ "-f",
102
+ "--file",
103
+ nargs="*",
104
+ help="Parameter file(s) to search (method, acqp, visu_pars, reco).",
105
+ )
106
+ params_parser.set_defaults(func=cmd_params, parser=params_parser)
@@ -0,0 +1,288 @@
1
+ from __future__ import annotations
2
+
3
+ """Create a pruned dataset zip using a prune spec."""
4
+
5
+ import argparse
6
+ import logging
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Optional, Union
10
+
11
+ import yaml
12
+
13
+ from brkraw.cli.utils import spinner
14
+ from brkraw.core import config as config_core
15
+ from brkraw.specs.pruner import prune_dataset_to_zip_from_spec
16
+
17
+ logger = logging.getLogger("brkraw")
18
+
19
+
20
+ def cmd_prune(args: argparse.Namespace) -> int:
21
+ output = args.output
22
+ root_name_override = None
23
+ dirs_override = _build_dir_override(args.scan_ids, args.reco_ids)
24
+ template_vars = _parse_kv_pairs(args.set_vars)
25
+ try:
26
+ spec_path = _resolve_pruner_spec(
27
+ args.spec_name if args.spec_name else args.spec
28
+ )
29
+ except ValueError as exc:
30
+ logger.error("%s", exc)
31
+ return 2
32
+ if output is None:
33
+ root_name = _load_root_name(spec_path)
34
+ if not root_name:
35
+ logger.error("Prune spec has no root_name; please provide --output.")
36
+ return 2
37
+ output = _default_output_path(Path(args.path), spec_path=spec_path)
38
+ else:
39
+ root_name_override = Path(output).stem
40
+ try:
41
+ logger.info("Pruning dataset: %s", args.path)
42
+ logger.info("Prune spec: %s", spec_path)
43
+ logger.info("Output zip: %s", output)
44
+ with spinner("Pruning"):
45
+ out_path = prune_dataset_to_zip_from_spec(
46
+ spec_path,
47
+ source=args.path,
48
+ dest=output,
49
+ validate=not args.no_validate,
50
+ strip_jcamp_comments=args.strip_jcamp_comments,
51
+ root_name=root_name_override,
52
+ dirs=dirs_override,
53
+ mode=args.mode,
54
+ template_vars=template_vars,
55
+ )
56
+ logger.info("Wrote pruned zip: %s", out_path)
57
+ _write_prune_sidecar(
58
+ out_path=Path(out_path),
59
+ input_path=Path(args.path),
60
+ spec_path=spec_path,
61
+ args=args,
62
+ root_name_override=root_name_override,
63
+ dirs_override=dirs_override,
64
+ template_vars=template_vars,
65
+ )
66
+ except Exception as exc:
67
+ logger.error("%s", exc)
68
+ return 2
69
+ return 0
70
+
71
+
72
+ def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[name-defined]
73
+ prune_parser = subparsers.add_parser(
74
+ "prune",
75
+ help="Create a pruned dataset zip from a prune spec.",
76
+ )
77
+ prune_parser.add_argument(
78
+ "path",
79
+ type=str,
80
+ help="Source dataset path.",
81
+ )
82
+ spec_group = prune_parser.add_mutually_exclusive_group(required=True)
83
+ spec_group.add_argument(
84
+ "--spec",
85
+ dest="spec",
86
+ type=str,
87
+ help="Path to prune spec YAML (or basename of installed spec).",
88
+ )
89
+ spec_group.add_argument(
90
+ "--spec-name",
91
+ dest="spec_name",
92
+ type=str,
93
+ help="Use an installed pruner spec by name (basename, no path).",
94
+ )
95
+ prune_parser.add_argument(
96
+ "-o",
97
+ "--output",
98
+ dest="output",
99
+ type=str,
100
+ help="Output zip path (default: <input>_pruned.*).",
101
+ )
102
+ prune_parser.add_argument(
103
+ "--no-validate",
104
+ action="store_true",
105
+ help="Skip prune spec validation.",
106
+ )
107
+ prune_parser.add_argument(
108
+ "--strip-jcamp-comments",
109
+ action="store_true",
110
+ help="Remove $$ comment lines from kept JCAMP files.",
111
+ )
112
+ prune_parser.add_argument(
113
+ "--mode",
114
+ choices=["keep", "drop"],
115
+ help="Override spec mode for file selection.",
116
+ )
117
+ prune_parser.add_argument(
118
+ "--set-var",
119
+ dest="set_vars",
120
+ action="append",
121
+ metavar="KEY=VALUE",
122
+ help="Template variable for use in prune spec (can repeat).",
123
+ )
124
+ prune_parser.add_argument(
125
+ "--scan-ids",
126
+ nargs="+",
127
+ metavar="SCAN_ID",
128
+ help="Override scan IDs to keep (space or comma separated).",
129
+ )
130
+ prune_parser.add_argument(
131
+ "--reco-ids",
132
+ nargs="+",
133
+ metavar="RECO_ID",
134
+ help="Override reco IDs to keep (space or comma separated).",
135
+ )
136
+ prune_parser.set_defaults(func=cmd_prune)
137
+
138
+
139
+ def _default_output_path(path: Path, *, spec_path: Path) -> str:
140
+ root_name = _load_root_name(spec_path)
141
+ suffix = path.suffix
142
+ base_dir = Path.cwd()
143
+ base_name = root_name or _stem_or_name(path)
144
+ if path.is_dir():
145
+ return str(base_dir / f"{base_name}.zip")
146
+ if suffix and path.name.endswith(suffix):
147
+ return str(base_dir / f"{base_name}{suffix}")
148
+ return str(base_dir / f"{base_name}.zip")
149
+
150
+
151
+ def _stem_or_name(path: Path) -> str:
152
+ if path.is_dir():
153
+ return f"{path.name}_pruned"
154
+ suffix = path.suffix
155
+ return path.name[:-len(suffix)] if suffix else path.name
156
+
157
+
158
+ def _load_root_name(spec_path: Path) -> Optional[str]:
159
+ try:
160
+ data = yaml.safe_load(spec_path.read_text(encoding="utf-8"))
161
+ except Exception:
162
+ return None
163
+ if not isinstance(data, dict):
164
+ return None
165
+ root_name = data.get("root_name")
166
+ if isinstance(root_name, str) and root_name.strip():
167
+ return root_name.strip()
168
+ return None
169
+
170
+
171
+ def _write_prune_sidecar(
172
+ *,
173
+ out_path: Path,
174
+ input_path: Path,
175
+ spec_path: Path,
176
+ args: argparse.Namespace,
177
+ root_name_override: Optional[str],
178
+ dirs_override: Optional[list],
179
+ template_vars: dict,
180
+ ) -> None:
181
+ sidecar = out_path.with_suffix(".prune.yaml")
182
+ spec_summary = _load_prune_spec_summary(spec_path)
183
+ payload = {
184
+ "timestamp": datetime.utcnow().isoformat() + "Z",
185
+ "command": "brkraw prune",
186
+ "input_path": str(input_path),
187
+ "output_path": str(out_path),
188
+ "spec_path": str(spec_path),
189
+ "spec": spec_summary,
190
+ "mode": args.mode,
191
+ "strip_jcamp_comments": bool(args.strip_jcamp_comments),
192
+ "scan_ids": args.scan_ids,
193
+ "reco_ids": args.reco_ids,
194
+ "set_vars": args.set_vars,
195
+ "template_vars": template_vars,
196
+ "root_name_override": root_name_override,
197
+ "dirs_override": dirs_override,
198
+ }
199
+ sidecar.write_text(yaml.safe_dump(payload, sort_keys=False), encoding="utf-8")
200
+
201
+
202
+ def _resolve_pruner_spec(value: Optional[str]) -> Path:
203
+ if value is None:
204
+ raise ValueError("A prune spec is required (use --spec or --spec-name).")
205
+ raw = Path(value).expanduser()
206
+ candidates = []
207
+ if raw.suffix:
208
+ candidates.append(raw)
209
+ else:
210
+ candidates.append(raw)
211
+ candidates.append(raw.with_suffix(".yaml"))
212
+ candidates.append(raw.with_suffix(".yml"))
213
+
214
+ if raw.is_absolute():
215
+ for cand in candidates:
216
+ if cand.exists():
217
+ return cand
218
+ raise ValueError(f"Prune spec not found: {value}")
219
+
220
+ search_roots = [Path.cwd(), config_core.paths().pruner_specs_dir]
221
+ for base in search_roots:
222
+ for cand in candidates:
223
+ path = (base / cand).resolve()
224
+ if path.exists():
225
+ return path
226
+ raise ValueError(f"Prune spec not found: {value}")
227
+
228
+
229
+
230
+
231
+ def _load_prune_spec_summary(spec_path: Path) -> dict:
232
+ try:
233
+ data = yaml.safe_load(spec_path.read_text(encoding="utf-8"))
234
+ except Exception:
235
+ return {}
236
+ if not isinstance(data, dict):
237
+ return {}
238
+ return {
239
+ "__meta__": data.get("__meta__", {}),
240
+ "mode": data.get("mode"),
241
+ "files": data.get("files"),
242
+ "dirs": data.get("dirs"),
243
+ "update_params_keys": list((data.get("update_params") or {}).keys()),
244
+ "add_root": data.get("add_root"),
245
+ "root_name": data.get("root_name"),
246
+ "strip_jcamp_comments": data.get("strip_jcamp_comments"),
247
+ }
248
+
249
+
250
+ def _build_dir_override(
251
+ scan_ids: Optional[list[str]],
252
+ reco_ids: Optional[list[str]],
253
+ ) -> Optional[list[dict[str, object]]]:
254
+ scan_list = _parse_id_list(scan_ids)
255
+ reco_list = _parse_id_list(reco_ids)
256
+ rules: list[dict[str, object]] = []
257
+ if scan_list:
258
+ rules.append({"level": 1, "dirs": scan_list})
259
+ if reco_list:
260
+ rules.append({"level": 3, "dirs": reco_list})
261
+ return rules or None
262
+
263
+
264
+ def _parse_id_list(values: Optional[list[str]]) -> list[str]:
265
+ if not values:
266
+ return []
267
+ result: list[str] = []
268
+ for value in values:
269
+ for part in str(value).split(","):
270
+ part = part.strip()
271
+ if part:
272
+ result.append(part)
273
+ return result
274
+
275
+
276
+ def _parse_kv_pairs(items: Optional[list[str]]) -> dict[str, str]:
277
+ if not items:
278
+ return {}
279
+ pairs: dict[str, str] = {}
280
+ for item in items:
281
+ if "=" not in item:
282
+ continue
283
+ key, value = item.split("=", 1)
284
+ key = key.strip()
285
+ if not key:
286
+ continue
287
+ pairs[key] = value
288
+ return pairs