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.
- brkraw/__init__.py +9 -3
- brkraw/apps/__init__.py +12 -0
- brkraw/apps/addon/__init__.py +30 -0
- brkraw/apps/addon/core.py +35 -0
- brkraw/apps/addon/dependencies.py +402 -0
- brkraw/apps/addon/installation.py +500 -0
- brkraw/apps/addon/io.py +21 -0
- brkraw/apps/hook/__init__.py +25 -0
- brkraw/apps/hook/core.py +636 -0
- brkraw/apps/loader/__init__.py +10 -0
- brkraw/apps/loader/core.py +622 -0
- brkraw/apps/loader/formatter.py +288 -0
- brkraw/apps/loader/helper.py +797 -0
- brkraw/apps/loader/info/__init__.py +11 -0
- brkraw/apps/loader/info/scan.py +85 -0
- brkraw/apps/loader/info/scan.yaml +90 -0
- brkraw/apps/loader/info/study.py +69 -0
- brkraw/apps/loader/info/study.yaml +156 -0
- brkraw/apps/loader/info/transform.py +92 -0
- brkraw/apps/loader/types.py +220 -0
- brkraw/cli/__init__.py +5 -0
- brkraw/cli/commands/__init__.py +2 -0
- brkraw/cli/commands/addon.py +327 -0
- brkraw/cli/commands/config.py +205 -0
- brkraw/cli/commands/convert.py +903 -0
- brkraw/cli/commands/hook.py +348 -0
- brkraw/cli/commands/info.py +74 -0
- brkraw/cli/commands/init.py +214 -0
- brkraw/cli/commands/params.py +106 -0
- brkraw/cli/commands/prune.py +288 -0
- brkraw/cli/commands/session.py +371 -0
- brkraw/cli/hook_args.py +80 -0
- brkraw/cli/main.py +83 -0
- brkraw/cli/utils.py +60 -0
- brkraw/core/__init__.py +13 -0
- brkraw/core/config.py +380 -0
- brkraw/core/entrypoints.py +25 -0
- brkraw/core/formatter.py +367 -0
- brkraw/core/fs.py +495 -0
- brkraw/core/jcamp.py +600 -0
- brkraw/core/layout.py +451 -0
- brkraw/core/parameters.py +781 -0
- brkraw/core/zip.py +1121 -0
- brkraw/dataclasses/__init__.py +14 -0
- brkraw/dataclasses/node.py +139 -0
- brkraw/dataclasses/reco.py +33 -0
- brkraw/dataclasses/scan.py +61 -0
- brkraw/dataclasses/study.py +131 -0
- brkraw/default/__init__.py +3 -0
- brkraw/default/pruner_specs/deid4share.yaml +42 -0
- brkraw/default/rules/00_default.yaml +4 -0
- brkraw/default/specs/metadata_dicom.yaml +236 -0
- brkraw/default/specs/metadata_transforms.py +92 -0
- brkraw/resolver/__init__.py +7 -0
- brkraw/resolver/affine.py +539 -0
- brkraw/resolver/datatype.py +69 -0
- brkraw/resolver/fid.py +90 -0
- brkraw/resolver/helpers.py +36 -0
- brkraw/resolver/image.py +188 -0
- brkraw/resolver/nifti.py +370 -0
- brkraw/resolver/shape.py +235 -0
- brkraw/schema/__init__.py +3 -0
- brkraw/schema/context_map.yaml +62 -0
- brkraw/schema/meta.yaml +57 -0
- brkraw/schema/niftiheader.yaml +95 -0
- brkraw/schema/pruner.yaml +55 -0
- brkraw/schema/remapper.yaml +128 -0
- brkraw/schema/rules.yaml +154 -0
- brkraw/specs/__init__.py +10 -0
- brkraw/specs/hook/__init__.py +12 -0
- brkraw/specs/hook/logic.py +31 -0
- brkraw/specs/hook/validator.py +22 -0
- brkraw/specs/meta/__init__.py +5 -0
- brkraw/specs/meta/validator.py +156 -0
- brkraw/specs/pruner/__init__.py +15 -0
- brkraw/specs/pruner/logic.py +361 -0
- brkraw/specs/pruner/validator.py +119 -0
- brkraw/specs/remapper/__init__.py +27 -0
- brkraw/specs/remapper/logic.py +924 -0
- brkraw/specs/remapper/validator.py +314 -0
- brkraw/specs/rules/__init__.py +6 -0
- brkraw/specs/rules/logic.py +263 -0
- brkraw/specs/rules/validator.py +103 -0
- brkraw-0.5.0.dist-info/METADATA +81 -0
- brkraw-0.5.0.dist-info/RECORD +88 -0
- {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info}/WHEEL +1 -2
- brkraw-0.5.0.dist-info/entry_points.txt +13 -0
- brkraw/lib/__init__.py +0 -4
- brkraw/lib/backup.py +0 -641
- brkraw/lib/bids.py +0 -0
- brkraw/lib/errors.py +0 -125
- brkraw/lib/loader.py +0 -1220
- brkraw/lib/orient.py +0 -194
- brkraw/lib/parser.py +0 -48
- brkraw/lib/pvobj.py +0 -301
- brkraw/lib/reference.py +0 -245
- brkraw/lib/utils.py +0 -471
- brkraw/scripts/__init__.py +0 -0
- brkraw/scripts/brk_backup.py +0 -106
- brkraw/scripts/brkraw.py +0 -744
- brkraw/ui/__init__.py +0 -0
- brkraw/ui/config.py +0 -17
- brkraw/ui/main_win.py +0 -214
- brkraw/ui/previewer.py +0 -225
- brkraw/ui/scan_info.py +0 -72
- brkraw/ui/scan_list.py +0 -73
- brkraw/ui/subj_info.py +0 -128
- brkraw-0.3.11.dist-info/METADATA +0 -25
- brkraw-0.3.11.dist-info/RECORD +0 -28
- brkraw-0.3.11.dist-info/entry_points.txt +0 -3
- brkraw-0.3.11.dist-info/top_level.txt +0 -2
- tests/__init__.py +0 -0
- {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info/licenses}/LICENSE +0 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
"""Installation and listing helpers for addon specs and rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import resources
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
resources.files # type: ignore[attr-defined]
|
|
9
|
+
except AttributeError: # pragma: no cover - fallback for Python 3.8
|
|
10
|
+
import importlib_resources as resources # type: ignore[assignment]
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
15
|
+
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
from ...core import config as config_core
|
|
19
|
+
from ...specs import remapper
|
|
20
|
+
from ...specs.pruner import validator as pruner_validator
|
|
21
|
+
from ...specs.rules import validator as rules_validator
|
|
22
|
+
from .dependencies import (
|
|
23
|
+
RULE_KEYS,
|
|
24
|
+
collect_transforms_sources as _collect_transforms_sources,
|
|
25
|
+
extract_transforms_source as _extract_transforms_source,
|
|
26
|
+
load_pruner_spec_records as _load_pruner_spec_records,
|
|
27
|
+
load_spec_records as _load_spec_records,
|
|
28
|
+
normalize_transform_ref as _normalize_transform_ref,
|
|
29
|
+
warn_dependencies as _warn_dependencies,
|
|
30
|
+
resolve_spec_reference,
|
|
31
|
+
)
|
|
32
|
+
from .io import write_file as _write_file
|
|
33
|
+
|
|
34
|
+
logger = logging.getLogger("brkraw")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def add(path: Union[str, Path], root: Optional[Union[str, Path]] = None) -> List[Path]:
|
|
38
|
+
"""Install a spec or rule YAML file."""
|
|
39
|
+
src = Path(path)
|
|
40
|
+
if not src.exists():
|
|
41
|
+
raise FileNotFoundError(src)
|
|
42
|
+
if src.suffix.lower() in {".yaml", ".yml"}:
|
|
43
|
+
return add_from_yaml(src, root=root)
|
|
44
|
+
raise ValueError(f"Unsupported file type: {src}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def add_spec_data(
|
|
48
|
+
spec_data: Dict[str, Any],
|
|
49
|
+
*,
|
|
50
|
+
filename: Optional[str] = None,
|
|
51
|
+
source_path: Optional[Path] = None,
|
|
52
|
+
root: Optional[Union[str, Path]] = None,
|
|
53
|
+
transforms_dir: Optional[Path] = None,
|
|
54
|
+
) -> List[Path]:
|
|
55
|
+
"""Install a spec from parsed data."""
|
|
56
|
+
if not isinstance(spec_data, dict):
|
|
57
|
+
raise ValueError("Spec data must be a mapping.")
|
|
58
|
+
if filename is None:
|
|
59
|
+
if source_path is None:
|
|
60
|
+
raise ValueError("filename is required when source_path is not provided.")
|
|
61
|
+
filename = source_path.name
|
|
62
|
+
if not filename.endswith((".yaml", ".yml")):
|
|
63
|
+
raise ValueError(f"Spec filename must be .yaml/.yml: {filename}")
|
|
64
|
+
paths = config_core.paths(root=root)
|
|
65
|
+
target = paths.specs_dir / filename
|
|
66
|
+
installed = [target]
|
|
67
|
+
installed_transforms, updated = install_transforms_from_spec(
|
|
68
|
+
spec_data,
|
|
69
|
+
base_dir=source_path.parent if source_path else None,
|
|
70
|
+
target_spec=target,
|
|
71
|
+
root=root,
|
|
72
|
+
target_transforms_dir=transforms_dir,
|
|
73
|
+
)
|
|
74
|
+
installed += installed_transforms
|
|
75
|
+
content = yaml.safe_dump(spec_data, sort_keys=False)
|
|
76
|
+
_write_file(target, content)
|
|
77
|
+
logger.info("Installed spec: %s", target)
|
|
78
|
+
return installed
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def add_pruner_spec_data(
|
|
82
|
+
spec_data: Dict[str, Any],
|
|
83
|
+
*,
|
|
84
|
+
filename: Optional[str] = None,
|
|
85
|
+
source_path: Optional[Path] = None,
|
|
86
|
+
root: Optional[Union[str, Path]] = None,
|
|
87
|
+
) -> List[Path]:
|
|
88
|
+
"""Install a pruner spec from parsed data."""
|
|
89
|
+
if not isinstance(spec_data, dict):
|
|
90
|
+
raise ValueError("Pruner spec data must be a mapping.")
|
|
91
|
+
if filename is None:
|
|
92
|
+
if source_path is None:
|
|
93
|
+
raise ValueError("filename is required when source_path is not provided.")
|
|
94
|
+
filename = source_path.name
|
|
95
|
+
if not filename.endswith((".yaml", ".yml")):
|
|
96
|
+
raise ValueError(f"Pruner spec filename must be .yaml/.yml: {filename}")
|
|
97
|
+
pruner_validator.validate_prune_spec(spec_data)
|
|
98
|
+
paths = config_core.paths(root=root)
|
|
99
|
+
target = paths.pruner_specs_dir / filename
|
|
100
|
+
content = yaml.safe_dump(spec_data, sort_keys=False)
|
|
101
|
+
_write_file(target, content)
|
|
102
|
+
logger.info("Installed pruner spec: %s", target)
|
|
103
|
+
return [target]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def add_rule_data(
|
|
107
|
+
rule_data: Dict[str, Any],
|
|
108
|
+
*,
|
|
109
|
+
filename: Optional[str] = None,
|
|
110
|
+
source_path: Optional[Path] = None,
|
|
111
|
+
root: Optional[Union[str, Path]] = None,
|
|
112
|
+
) -> List[Path]:
|
|
113
|
+
"""Install a rule file from parsed data."""
|
|
114
|
+
if not isinstance(rule_data, dict):
|
|
115
|
+
raise ValueError("Rule data must be a mapping.")
|
|
116
|
+
if filename is None:
|
|
117
|
+
if source_path is None:
|
|
118
|
+
raise ValueError("filename is required when source_path is not provided.")
|
|
119
|
+
filename = source_path.name
|
|
120
|
+
if not filename.endswith((".yaml", ".yml")):
|
|
121
|
+
raise ValueError(f"Rule filename must be .yaml/.yml: {filename}")
|
|
122
|
+
rules_validator.validate_rules(rule_data)
|
|
123
|
+
ensure_rule_specs_present(rule_data, root=root)
|
|
124
|
+
paths = config_core.paths(root=root)
|
|
125
|
+
target = paths.rules_dir / filename
|
|
126
|
+
content = yaml.safe_dump(rule_data, sort_keys=False)
|
|
127
|
+
_write_file(target, content)
|
|
128
|
+
logger.info("Installed rule: %s", target)
|
|
129
|
+
return [target]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def install_defaults(root: Optional[Union[str, Path]] = None) -> List[Path]:
|
|
133
|
+
"""Install bundled default specs and rules."""
|
|
134
|
+
installed: List[Path] = []
|
|
135
|
+
|
|
136
|
+
base = resources.files("brkraw.default")
|
|
137
|
+
for rel_dir in ("specs", "rules", "pruner_specs"):
|
|
138
|
+
src_dir = base / rel_dir
|
|
139
|
+
if not src_dir.is_dir():
|
|
140
|
+
continue
|
|
141
|
+
for entry in src_dir.iterdir():
|
|
142
|
+
name = entry.name
|
|
143
|
+
if not (name.endswith(".yaml") or name.endswith(".yml")):
|
|
144
|
+
continue
|
|
145
|
+
content = entry.read_text(encoding="utf-8")
|
|
146
|
+
data = yaml.safe_load(content)
|
|
147
|
+
if not isinstance(data, dict):
|
|
148
|
+
continue
|
|
149
|
+
entry_path = Path(str(entry))
|
|
150
|
+
if rel_dir == "specs":
|
|
151
|
+
installed += add_spec_data(
|
|
152
|
+
data,
|
|
153
|
+
filename=name,
|
|
154
|
+
source_path=entry_path,
|
|
155
|
+
root=root,
|
|
156
|
+
)
|
|
157
|
+
elif rel_dir == "pruner_specs":
|
|
158
|
+
installed += add_pruner_spec_data(
|
|
159
|
+
data,
|
|
160
|
+
filename=name,
|
|
161
|
+
source_path=entry_path,
|
|
162
|
+
root=root,
|
|
163
|
+
)
|
|
164
|
+
else:
|
|
165
|
+
installed += add_rule_data(
|
|
166
|
+
data,
|
|
167
|
+
filename=name,
|
|
168
|
+
source_path=entry_path,
|
|
169
|
+
root=root,
|
|
170
|
+
)
|
|
171
|
+
return installed
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def list_installed(root: Optional[Union[str, Path]] = None) -> Dict[str, List[Dict[str, str]]]:
|
|
175
|
+
"""List installed specs and rules with metadata."""
|
|
176
|
+
paths = config_core.paths(root=root)
|
|
177
|
+
result: Dict[str, List[Dict[str, str]]] = {
|
|
178
|
+
"specs": [],
|
|
179
|
+
"pruner_specs": [],
|
|
180
|
+
"rules": [],
|
|
181
|
+
"transforms": [],
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
rule_entries = load_rule_entries(paths.rules_dir)
|
|
185
|
+
spec_categories = spec_categories_from_rules(rule_entries)
|
|
186
|
+
|
|
187
|
+
transforms_map: Dict[str, Set[str]] = {}
|
|
188
|
+
for record in _load_spec_records(paths.specs_dir):
|
|
189
|
+
name = record.get("name")
|
|
190
|
+
desc = record.get("description")
|
|
191
|
+
version = record.get("version")
|
|
192
|
+
category = (
|
|
193
|
+
record.get("category")
|
|
194
|
+
or spec_categories.get(record["file"])
|
|
195
|
+
or spec_categories.get(name or "")
|
|
196
|
+
)
|
|
197
|
+
result["specs"].append(
|
|
198
|
+
{
|
|
199
|
+
"file": record["file"],
|
|
200
|
+
"name": name if name else "<Unknown>",
|
|
201
|
+
"version": version if version else "<Unknown>",
|
|
202
|
+
"description": desc if desc else "<Unknown>",
|
|
203
|
+
"category": category if category else "<Unknown>",
|
|
204
|
+
"name_unknown": "1" if not name else "0",
|
|
205
|
+
"version_unknown": "1" if not version else "0",
|
|
206
|
+
"description_unknown": "1" if not desc else "0",
|
|
207
|
+
"category_unknown": "1" if not category else "0",
|
|
208
|
+
}
|
|
209
|
+
)
|
|
210
|
+
spec_path = record["path"]
|
|
211
|
+
spec_label = record["file"]
|
|
212
|
+
for src in _collect_transforms_sources(spec_path):
|
|
213
|
+
normalized = _normalize_transform_ref(
|
|
214
|
+
src,
|
|
215
|
+
spec_path=spec_path,
|
|
216
|
+
transforms_dir=paths.transforms_dir,
|
|
217
|
+
)
|
|
218
|
+
transforms_map.setdefault(normalized, set()).add(spec_label)
|
|
219
|
+
|
|
220
|
+
for entry in rule_entries:
|
|
221
|
+
result["rules"].append(entry)
|
|
222
|
+
|
|
223
|
+
for record in _load_pruner_spec_records(paths.pruner_specs_dir):
|
|
224
|
+
result["pruner_specs"].append(
|
|
225
|
+
{
|
|
226
|
+
"file": record["file"],
|
|
227
|
+
"name": record.get("name") or "<Unknown>",
|
|
228
|
+
"version": record.get("version") or "<Unknown>",
|
|
229
|
+
"description": record.get("description") or "<Unknown>",
|
|
230
|
+
"name_unknown": "1" if not record.get("name") else "0",
|
|
231
|
+
"version_unknown": "1" if not record.get("version") else "0",
|
|
232
|
+
"description_unknown": "1" if not record.get("description") else "0",
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
for path in sorted(paths.transforms_dir.rglob("*.py")):
|
|
237
|
+
relpath = str(path.relative_to(paths.transforms_dir))
|
|
238
|
+
mapped = transforms_map.get(relpath)
|
|
239
|
+
result["transforms"].append(
|
|
240
|
+
{
|
|
241
|
+
"file": relpath,
|
|
242
|
+
"spec": ", ".join(sorted(mapped)) if mapped else "<Unknown>",
|
|
243
|
+
"spec_unknown": "1" if not mapped else "0",
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
return result
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def remove(
|
|
251
|
+
filename: Union[str, Path],
|
|
252
|
+
*,
|
|
253
|
+
root: Optional[Union[str, Path]] = None,
|
|
254
|
+
kind: Optional[str] = None,
|
|
255
|
+
force: bool = False,
|
|
256
|
+
) -> List[Path]:
|
|
257
|
+
"""Remove an installed spec/rule/transform file by filename.
|
|
258
|
+
|
|
259
|
+
Notes:
|
|
260
|
+
The `filename` argument matches the installed file name (not the
|
|
261
|
+
spec/rule `__meta__.name`).
|
|
262
|
+
"""
|
|
263
|
+
name = Path(filename).name
|
|
264
|
+
paths = config_core.paths(root=root)
|
|
265
|
+
removed: List[Path] = []
|
|
266
|
+
kinds = [kind] if kind else ["spec", "pruner", "rule", "transform"]
|
|
267
|
+
targets = resolve_targets(name, kinds, paths)
|
|
268
|
+
if not targets:
|
|
269
|
+
raise FileNotFoundError(name)
|
|
270
|
+
has_deps = False
|
|
271
|
+
for target, item in targets:
|
|
272
|
+
has_deps = _warn_dependencies(target, kind=item, root=root) or has_deps
|
|
273
|
+
if has_deps and not force:
|
|
274
|
+
raise RuntimeError("Dependencies found; use --force to remove.")
|
|
275
|
+
for target, item in targets:
|
|
276
|
+
target.unlink()
|
|
277
|
+
removed.append(target)
|
|
278
|
+
if not removed:
|
|
279
|
+
raise FileNotFoundError(name)
|
|
280
|
+
return removed
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def resolve_targets(
|
|
284
|
+
name: str,
|
|
285
|
+
kinds: List[str],
|
|
286
|
+
paths: config_core.ConfigPaths,
|
|
287
|
+
) -> List[Tuple[Path, str]]:
|
|
288
|
+
targets: List[Tuple[Path, str]] = []
|
|
289
|
+
for item in kinds:
|
|
290
|
+
if item == "spec":
|
|
291
|
+
base = paths.specs_dir
|
|
292
|
+
elif item == "pruner":
|
|
293
|
+
base = paths.pruner_specs_dir
|
|
294
|
+
elif item == "rule":
|
|
295
|
+
base = paths.rules_dir
|
|
296
|
+
elif item == "transform":
|
|
297
|
+
base = paths.transforms_dir
|
|
298
|
+
else:
|
|
299
|
+
raise ValueError("kind must be 'spec' or 'pruner' or 'rule' or 'transform'.")
|
|
300
|
+
if not base.exists():
|
|
301
|
+
continue
|
|
302
|
+
candidate = (base / name).resolve()
|
|
303
|
+
if candidate.exists():
|
|
304
|
+
matches = [candidate]
|
|
305
|
+
else:
|
|
306
|
+
matches = [path for path in base.rglob(name) if path.is_file()]
|
|
307
|
+
for path in matches:
|
|
308
|
+
targets.append((path, item))
|
|
309
|
+
return targets
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def add_from_yaml(path: Path, root: Optional[Union[str, Path]]) -> List[Path]:
|
|
313
|
+
"""Install a spec or rule YAML after classifying the content."""
|
|
314
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
315
|
+
if data is None:
|
|
316
|
+
raise ValueError(f"Empty YAML file: {path}")
|
|
317
|
+
if not isinstance(data, dict):
|
|
318
|
+
raise ValueError(f"Rule/spec YAML must be a mapping: {path}")
|
|
319
|
+
|
|
320
|
+
kind = classify_yaml(data)
|
|
321
|
+
if kind == "rule":
|
|
322
|
+
return add_rule_data(data, filename=path.name, source_path=path, root=root)
|
|
323
|
+
if kind == "spec":
|
|
324
|
+
return add_spec_data(data, filename=path.name, source_path=path, root=root)
|
|
325
|
+
if kind == "pruner_spec":
|
|
326
|
+
return add_pruner_spec_data(data, filename=path.name, source_path=path, root=root)
|
|
327
|
+
raise ValueError(f"Unrecognized YAML file: {path}")
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def classify_yaml(data: Dict[str, Any]) -> str:
|
|
331
|
+
"""Classify YAML content as a spec or rule mapping."""
|
|
332
|
+
if RULE_KEYS.intersection(data.keys()):
|
|
333
|
+
rules_validator.validate_rules(data)
|
|
334
|
+
return "rule"
|
|
335
|
+
errors = remapper.validate_spec(data, raise_on_error=False)
|
|
336
|
+
if not errors:
|
|
337
|
+
return "spec"
|
|
338
|
+
try:
|
|
339
|
+
pruner_validator.validate_prune_spec(data)
|
|
340
|
+
except Exception:
|
|
341
|
+
pass
|
|
342
|
+
else:
|
|
343
|
+
return "pruner_spec"
|
|
344
|
+
rules_validator.validate_rules(data)
|
|
345
|
+
return "rule"
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def update_transforms_source(spec_data: Dict[str, Any], value: List[str]) -> None:
|
|
349
|
+
"""Rewrite transforms_source fields inside a spec mapping."""
|
|
350
|
+
meta = spec_data.get("__meta__")
|
|
351
|
+
if isinstance(meta, dict) and meta.get("transforms_source"):
|
|
352
|
+
meta["transforms_source"] = value[0] if len(value) == 1 else value
|
|
353
|
+
for section in spec_data.values():
|
|
354
|
+
if not isinstance(section, dict):
|
|
355
|
+
continue
|
|
356
|
+
section_meta = section.get("__meta__")
|
|
357
|
+
if isinstance(section_meta, dict) and section_meta.get("transforms_source"):
|
|
358
|
+
section_meta["transforms_source"] = value[0] if len(value) == 1 else value
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def install_transforms_from_spec(
|
|
362
|
+
spec_data: Dict[str, Any],
|
|
363
|
+
*,
|
|
364
|
+
base_dir: Optional[Path],
|
|
365
|
+
target_spec: Path,
|
|
366
|
+
root: Optional[Union[str, Path]],
|
|
367
|
+
target_transforms_dir: Optional[Path] = None,
|
|
368
|
+
) -> Tuple[List[Path], bool]:
|
|
369
|
+
"""Install transforms referenced by a spec and rewrite paths."""
|
|
370
|
+
sources = _extract_transforms_source(spec_data)
|
|
371
|
+
if not sources:
|
|
372
|
+
return [], False
|
|
373
|
+
paths = config_core.paths(root=root)
|
|
374
|
+
transforms_dir = target_transforms_dir or paths.transforms_dir
|
|
375
|
+
installed: List[Path] = []
|
|
376
|
+
rel_paths: List[str] = []
|
|
377
|
+
for src in sources:
|
|
378
|
+
src_path = Path(src)
|
|
379
|
+
target = transforms_dir / src_path.name
|
|
380
|
+
if base_dir is not None:
|
|
381
|
+
candidate = (base_dir / src_path).resolve()
|
|
382
|
+
if not candidate.exists():
|
|
383
|
+
raise FileNotFoundError(candidate)
|
|
384
|
+
_write_file(target, candidate.read_text(encoding="utf-8"))
|
|
385
|
+
elif src_path.is_absolute():
|
|
386
|
+
if not src_path.exists():
|
|
387
|
+
raise FileNotFoundError(src_path)
|
|
388
|
+
_write_file(target, src_path.read_text(encoding="utf-8"))
|
|
389
|
+
else:
|
|
390
|
+
raise FileNotFoundError(src_path)
|
|
391
|
+
rel_paths.append(os.path.relpath(target, start=target_spec.parent))
|
|
392
|
+
installed.append(target)
|
|
393
|
+
logger.info("Installed transforms: %s", target)
|
|
394
|
+
update_transforms_source(spec_data, rel_paths)
|
|
395
|
+
return installed, True
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def ensure_rule_specs_present(rule_data: Dict[str, Any], *, root: Optional[Union[str, Path]]) -> None:
|
|
399
|
+
"""Ensure rule references point to installed specs."""
|
|
400
|
+
base = config_core.resolve_root(root)
|
|
401
|
+
for key in RULE_KEYS:
|
|
402
|
+
if key == "converter_hook":
|
|
403
|
+
continue
|
|
404
|
+
for item in rule_data.get(key, []) or []:
|
|
405
|
+
if not isinstance(item, dict):
|
|
406
|
+
continue
|
|
407
|
+
use = item.get("use")
|
|
408
|
+
if not isinstance(use, str):
|
|
409
|
+
continue
|
|
410
|
+
version = item.get("version") if isinstance(item.get("version"), str) else None
|
|
411
|
+
spec_path = resolve_spec_reference(
|
|
412
|
+
use,
|
|
413
|
+
category=key,
|
|
414
|
+
version=version,
|
|
415
|
+
root=base,
|
|
416
|
+
)
|
|
417
|
+
ensure_spec_category(spec_path, key)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def ensure_spec_category(spec_path: Path, category: str) -> None:
|
|
421
|
+
if category not in {"info_spec", "metadata_spec"}:
|
|
422
|
+
return
|
|
423
|
+
data = yaml.safe_load(spec_path.read_text(encoding="utf-8"))
|
|
424
|
+
if not isinstance(data, dict):
|
|
425
|
+
raise ValueError(f"Spec file must be a mapping: {spec_path}")
|
|
426
|
+
meta = data.get("__meta__")
|
|
427
|
+
if not isinstance(meta, dict):
|
|
428
|
+
raise ValueError(f"{spec_path}: __meta__ must be an object.")
|
|
429
|
+
current = meta.get("category")
|
|
430
|
+
if current is None:
|
|
431
|
+
raise ValueError(f"{spec_path}: __meta__.category is required.")
|
|
432
|
+
if current != category:
|
|
433
|
+
raise ValueError(
|
|
434
|
+
f"{spec_path}: __meta__.category={current!r} conflicts with rule category {category!r}."
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def load_rule_entries(rules_dir: Path) -> List[Dict[str, str]]:
|
|
439
|
+
"""Load rule metadata for listing."""
|
|
440
|
+
entries: List[Dict[str, str]] = []
|
|
441
|
+
if not rules_dir.exists():
|
|
442
|
+
return entries
|
|
443
|
+
files = list(rules_dir.rglob("*.yaml")) + list(rules_dir.rglob("*.yml"))
|
|
444
|
+
for path in sorted(files):
|
|
445
|
+
relpath = str(path.relative_to(rules_dir))
|
|
446
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
447
|
+
if not isinstance(data, dict):
|
|
448
|
+
continue
|
|
449
|
+
for key in RULE_KEYS:
|
|
450
|
+
for item in data.get(key, []) or []:
|
|
451
|
+
if not isinstance(item, dict):
|
|
452
|
+
continue
|
|
453
|
+
entries.append(
|
|
454
|
+
{
|
|
455
|
+
"file": relpath,
|
|
456
|
+
"category": key,
|
|
457
|
+
"name": str(item.get("name", "")),
|
|
458
|
+
"description": str(item.get("description", "")),
|
|
459
|
+
"version": str(item.get("version", "")),
|
|
460
|
+
"use": str(item.get("use", "")),
|
|
461
|
+
}
|
|
462
|
+
)
|
|
463
|
+
return entries
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def spec_categories_from_rules(rule_entries: List[Dict[str, str]]) -> Dict[str, str]:
|
|
467
|
+
"""Derive spec category labels from rule entries."""
|
|
468
|
+
mapping: Dict[str, Set[str]] = {}
|
|
469
|
+
for entry in rule_entries:
|
|
470
|
+
category = entry.get("category", "")
|
|
471
|
+
if category not in {"info_spec", "metadata_spec"}:
|
|
472
|
+
continue
|
|
473
|
+
use = entry.get("use", "")
|
|
474
|
+
if not use:
|
|
475
|
+
continue
|
|
476
|
+
spec_name = Path(use).name if use else ""
|
|
477
|
+
if not spec_name:
|
|
478
|
+
continue
|
|
479
|
+
mapping.setdefault(spec_name, set()).add(category)
|
|
480
|
+
return {name: ", ".join(sorted(cats)) for name, cats in mapping.items()}
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
__all__ = [
|
|
484
|
+
"add",
|
|
485
|
+
"add_spec_data",
|
|
486
|
+
"add_pruner_spec_data",
|
|
487
|
+
"add_rule_data",
|
|
488
|
+
"install_defaults",
|
|
489
|
+
"list_installed",
|
|
490
|
+
"remove",
|
|
491
|
+
"resolve_targets",
|
|
492
|
+
"add_from_yaml",
|
|
493
|
+
"classify_yaml",
|
|
494
|
+
"update_transforms_source",
|
|
495
|
+
"install_transforms_from_spec",
|
|
496
|
+
"ensure_rule_specs_present",
|
|
497
|
+
"ensure_spec_category",
|
|
498
|
+
"load_rule_entries",
|
|
499
|
+
"spec_categories_from_rules",
|
|
500
|
+
]
|
brkraw/apps/addon/io.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Addon IO helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def write_file(target: Path, content: str) -> None:
|
|
9
|
+
"""Write text content to a target path.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
target: Output file path.
|
|
13
|
+
content: Text content to write.
|
|
14
|
+
"""
|
|
15
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
16
|
+
target.write_text(content, encoding="utf-8")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"write_file",
|
|
21
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Hook package utilities for converter hooks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import List
|
|
6
|
+
|
|
7
|
+
from .core import (
|
|
8
|
+
install_hook,
|
|
9
|
+
install_all,
|
|
10
|
+
list_hooks,
|
|
11
|
+
read_hook_docs,
|
|
12
|
+
uninstall_hook,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"install_all",
|
|
17
|
+
"install_hook",
|
|
18
|
+
"list_hooks",
|
|
19
|
+
"read_hook_docs",
|
|
20
|
+
"uninstall_hook",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def __dir__() -> List[str]:
|
|
25
|
+
return sorted(__all__)
|