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
brkraw/__init__.py
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
from
|
|
1
|
+
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
__version__ = '0.
|
|
4
|
-
|
|
3
|
+
__version__ = '0.5.0'
|
|
4
|
+
from .apps.loader import BrukerLoader
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def load(path):
|
|
8
8
|
return BrukerLoader(path)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'load',
|
|
12
|
+
'BrukerLoader',
|
|
13
|
+
'__version__',
|
|
14
|
+
]
|
brkraw/apps/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Addon package entrypoint.
|
|
2
|
+
|
|
3
|
+
Last updated: 2025-12-30
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
from .core import (
|
|
9
|
+
add,
|
|
10
|
+
add_rule_data,
|
|
11
|
+
add_spec_data,
|
|
12
|
+
add_pruner_spec_data,
|
|
13
|
+
install_defaults,
|
|
14
|
+
resolve_spec_reference,
|
|
15
|
+
resolve_pruner_spec_reference,
|
|
16
|
+
list_installed,
|
|
17
|
+
remove,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"add",
|
|
22
|
+
"add_rule_data",
|
|
23
|
+
"add_spec_data",
|
|
24
|
+
"add_pruner_spec_data",
|
|
25
|
+
"install_defaults",
|
|
26
|
+
"resolve_spec_reference",
|
|
27
|
+
"resolve_pruner_spec_reference",
|
|
28
|
+
"list_installed",
|
|
29
|
+
"remove",
|
|
30
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Addon installer utilities for specs and rules.
|
|
2
|
+
|
|
3
|
+
Last updated: 2025-12-30
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
from .dependencies import resolve_pruner_spec_reference, resolve_spec_reference
|
|
11
|
+
from .installation import (
|
|
12
|
+
add,
|
|
13
|
+
add_pruner_spec_data,
|
|
14
|
+
add_rule_data,
|
|
15
|
+
add_spec_data,
|
|
16
|
+
install_defaults,
|
|
17
|
+
list_installed,
|
|
18
|
+
remove,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"add",
|
|
23
|
+
"add_rule_data",
|
|
24
|
+
"add_spec_data",
|
|
25
|
+
"add_pruner_spec_data",
|
|
26
|
+
"install_defaults",
|
|
27
|
+
"resolve_spec_reference",
|
|
28
|
+
"resolve_pruner_spec_reference",
|
|
29
|
+
"list_installed",
|
|
30
|
+
"remove",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def __dir__() -> List[str]:
|
|
35
|
+
return sorted(__all__)
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
"""Dependency and reference helpers for addon specs and rules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from ...core import config as config_core
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("brkraw")
|
|
15
|
+
|
|
16
|
+
RULE_KEYS = {"info_spec", "metadata_spec", "converter_hook"}
|
|
17
|
+
_SPEC_EXTS = (".yaml", ".yml")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def warn_dependencies(target: Path, *, kind: str, root: Optional[Union[str, Path]]) -> bool:
|
|
21
|
+
paths = config_core.paths(root=root)
|
|
22
|
+
warned = False
|
|
23
|
+
if kind == "spec":
|
|
24
|
+
used_by_rules = rules_using_spec(target.name, paths.rules_dir)
|
|
25
|
+
if used_by_rules:
|
|
26
|
+
logger.warning(
|
|
27
|
+
"Spec %s is referenced by rules: %s",
|
|
28
|
+
target.name,
|
|
29
|
+
", ".join(sorted(used_by_rules)),
|
|
30
|
+
)
|
|
31
|
+
warned = True
|
|
32
|
+
included_by = specs_including_spec(target.name, paths.specs_dir)
|
|
33
|
+
if included_by:
|
|
34
|
+
logger.warning(
|
|
35
|
+
"Spec %s is included by: %s",
|
|
36
|
+
target.name,
|
|
37
|
+
", ".join(sorted(included_by)),
|
|
38
|
+
)
|
|
39
|
+
warned = True
|
|
40
|
+
elif kind == "transform":
|
|
41
|
+
transform_ref = normalize_transform_ref(
|
|
42
|
+
str(target),
|
|
43
|
+
spec_path=target,
|
|
44
|
+
transforms_dir=paths.transforms_dir,
|
|
45
|
+
)
|
|
46
|
+
used_by_specs = specs_using_transform(transform_ref, paths.specs_dir)
|
|
47
|
+
if used_by_specs:
|
|
48
|
+
logger.warning(
|
|
49
|
+
"Transform %s is referenced by specs: %s",
|
|
50
|
+
target.name,
|
|
51
|
+
", ".join(sorted(used_by_specs)),
|
|
52
|
+
)
|
|
53
|
+
warned = True
|
|
54
|
+
return warned
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def rules_using_spec(spec_name: str, rules_dir: Path) -> Set[str]:
|
|
58
|
+
used_by: Set[str] = set()
|
|
59
|
+
if not rules_dir.exists():
|
|
60
|
+
return used_by
|
|
61
|
+
files = list(rules_dir.rglob("*.yaml")) + list(rules_dir.rglob("*.yml"))
|
|
62
|
+
for path in files:
|
|
63
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
64
|
+
if not isinstance(data, dict):
|
|
65
|
+
continue
|
|
66
|
+
for key in RULE_KEYS:
|
|
67
|
+
if key == "converter_hook":
|
|
68
|
+
continue
|
|
69
|
+
for item in data.get(key, []) or []:
|
|
70
|
+
if not isinstance(item, dict):
|
|
71
|
+
continue
|
|
72
|
+
use = item.get("use")
|
|
73
|
+
if isinstance(use, str) and Path(use).name == spec_name:
|
|
74
|
+
used_by.add(path.name)
|
|
75
|
+
return used_by
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def specs_including_spec(spec_name: str, specs_dir: Path) -> Set[str]:
|
|
79
|
+
included_by: Set[str] = set()
|
|
80
|
+
if not specs_dir.exists():
|
|
81
|
+
return included_by
|
|
82
|
+
files = list(specs_dir.rglob("*.yaml")) + list(specs_dir.rglob("*.yml"))
|
|
83
|
+
for path in files:
|
|
84
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
85
|
+
if not isinstance(data, dict):
|
|
86
|
+
continue
|
|
87
|
+
meta = data.get("__meta__")
|
|
88
|
+
include_list: List[str] = []
|
|
89
|
+
if isinstance(meta, dict) and "include" in meta:
|
|
90
|
+
include = meta.get("include")
|
|
91
|
+
if isinstance(include, str):
|
|
92
|
+
include_list = [include]
|
|
93
|
+
elif isinstance(include, list) and all(isinstance(item, str) for item in include):
|
|
94
|
+
include_list = include
|
|
95
|
+
if any(Path(item).name == spec_name for item in include_list):
|
|
96
|
+
included_by.add(path.name)
|
|
97
|
+
return included_by
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def extract_transforms_source(spec_data: Dict[str, Any]) -> List[str]:
|
|
101
|
+
"""Collect transforms_source entries from a spec mapping."""
|
|
102
|
+
sources: List[str] = []
|
|
103
|
+
meta = spec_data.get("__meta__")
|
|
104
|
+
if isinstance(meta, dict) and meta.get("transforms_source"):
|
|
105
|
+
src = meta["transforms_source"]
|
|
106
|
+
if isinstance(src, str):
|
|
107
|
+
sources.append(src)
|
|
108
|
+
elif isinstance(src, list) and all(isinstance(item, str) for item in src):
|
|
109
|
+
sources.extend(src)
|
|
110
|
+
else:
|
|
111
|
+
raise ValueError("transforms_source must be a string or list of strings.")
|
|
112
|
+
for value in spec_data.values():
|
|
113
|
+
if not isinstance(value, dict):
|
|
114
|
+
continue
|
|
115
|
+
child_meta = value.get("__meta__")
|
|
116
|
+
if isinstance(child_meta, dict) and child_meta.get("transforms_source"):
|
|
117
|
+
src = child_meta["transforms_source"]
|
|
118
|
+
if isinstance(src, str):
|
|
119
|
+
sources.append(src)
|
|
120
|
+
elif isinstance(src, list) and all(isinstance(item, str) for item in src):
|
|
121
|
+
sources.extend(src)
|
|
122
|
+
else:
|
|
123
|
+
raise ValueError("transforms_source must be a string or list of strings.")
|
|
124
|
+
return sources
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def collect_transforms_sources(spec_path: Path, stack: Optional[Set[Path]] = None) -> Set[str]:
|
|
128
|
+
"""Collect transforms_source entries from a spec file, including includes."""
|
|
129
|
+
if stack is None:
|
|
130
|
+
stack = set()
|
|
131
|
+
spec_path = spec_path.resolve()
|
|
132
|
+
if spec_path in stack:
|
|
133
|
+
return set()
|
|
134
|
+
stack.add(spec_path)
|
|
135
|
+
try:
|
|
136
|
+
spec_data = yaml.safe_load(spec_path.read_text(encoding="utf-8"))
|
|
137
|
+
if not isinstance(spec_data, dict):
|
|
138
|
+
return set()
|
|
139
|
+
sources = set(extract_transforms_source(spec_data))
|
|
140
|
+
meta = spec_data.get("__meta__")
|
|
141
|
+
include_list: List[str] = []
|
|
142
|
+
if isinstance(meta, dict) and "include" in meta:
|
|
143
|
+
include = meta.get("include")
|
|
144
|
+
if isinstance(include, str):
|
|
145
|
+
include_list = [include]
|
|
146
|
+
elif isinstance(include, list) and all(isinstance(item, str) for item in include):
|
|
147
|
+
include_list = include
|
|
148
|
+
for item in include_list:
|
|
149
|
+
inc_path = Path(item)
|
|
150
|
+
if not inc_path.is_absolute():
|
|
151
|
+
inc_path = (spec_path.parent / inc_path).resolve()
|
|
152
|
+
if not inc_path.exists():
|
|
153
|
+
logger.warning("Spec include not found while listing: %s", inc_path)
|
|
154
|
+
continue
|
|
155
|
+
sources.update(collect_transforms_sources(inc_path, stack))
|
|
156
|
+
return sources
|
|
157
|
+
finally:
|
|
158
|
+
stack.remove(spec_path)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def normalize_transform_ref(
|
|
162
|
+
src: str,
|
|
163
|
+
*,
|
|
164
|
+
spec_path: Path,
|
|
165
|
+
transforms_dir: Path,
|
|
166
|
+
) -> str:
|
|
167
|
+
candidate = Path(src)
|
|
168
|
+
if not candidate.is_absolute():
|
|
169
|
+
candidate = (spec_path.parent / candidate).resolve()
|
|
170
|
+
try:
|
|
171
|
+
return str(candidate.relative_to(transforms_dir))
|
|
172
|
+
except ValueError:
|
|
173
|
+
return candidate.name
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def specs_using_transform(transform_ref: str, specs_dir: Path) -> Set[str]:
|
|
177
|
+
used_by: Set[str] = set()
|
|
178
|
+
if not specs_dir.exists():
|
|
179
|
+
return used_by
|
|
180
|
+
files = list(specs_dir.rglob("*.yaml")) + list(specs_dir.rglob("*.yml"))
|
|
181
|
+
for path in files:
|
|
182
|
+
for src in collect_transforms_sources(path):
|
|
183
|
+
normalized = normalize_transform_ref(
|
|
184
|
+
src,
|
|
185
|
+
spec_path=path,
|
|
186
|
+
transforms_dir=specs_dir.parent / "transforms",
|
|
187
|
+
)
|
|
188
|
+
if normalized == transform_ref:
|
|
189
|
+
used_by.add(str(path.relative_to(specs_dir)))
|
|
190
|
+
break
|
|
191
|
+
return used_by
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def resolve_spec_path(use: str, base: Path) -> Path:
|
|
195
|
+
"""Resolve rule `use` values into absolute spec paths."""
|
|
196
|
+
candidate = Path(use)
|
|
197
|
+
if candidate.is_absolute():
|
|
198
|
+
return candidate
|
|
199
|
+
if candidate.parts and candidate.parts[0] == "specs":
|
|
200
|
+
return base / candidate
|
|
201
|
+
return base / "specs" / candidate
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def looks_like_spec_path(use: str) -> bool:
|
|
205
|
+
return (
|
|
206
|
+
"/" in use
|
|
207
|
+
or "\\" in use
|
|
208
|
+
or use.endswith(_SPEC_EXTS)
|
|
209
|
+
or use.startswith(".")
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def version_key(value: str) -> Tuple[Tuple[int, Union[int, str]], ...]:
|
|
214
|
+
parts = [p for p in re.split(r"[.\-_+]", value) if p]
|
|
215
|
+
key: List[Tuple[int, Union[int, str]]] = []
|
|
216
|
+
for part in parts:
|
|
217
|
+
if part.isdigit():
|
|
218
|
+
key.append((0, int(part)))
|
|
219
|
+
else:
|
|
220
|
+
key.append((1, part))
|
|
221
|
+
return tuple(key)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def select_latest(records: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
225
|
+
def _key(item: Dict[str, Any]) -> Tuple[Tuple[int, Union[int, str]], ...]:
|
|
226
|
+
version = item.get("version")
|
|
227
|
+
return version_key(version) if isinstance(version, str) else tuple()
|
|
228
|
+
|
|
229
|
+
best = max(records, key=_key)
|
|
230
|
+
best_key = _key(best)
|
|
231
|
+
tied = [item for item in records if _key(item) == best_key]
|
|
232
|
+
if len(tied) > 1:
|
|
233
|
+
files = ", ".join(sorted(item["file"] for item in tied))
|
|
234
|
+
raise ValueError(f"Multiple specs share the latest version: {files}")
|
|
235
|
+
return best
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def load_spec_meta(path: Path) -> Dict[str, str]:
|
|
239
|
+
"""Load __meta__ name/description from a spec file."""
|
|
240
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
241
|
+
if not isinstance(data, dict):
|
|
242
|
+
return {}
|
|
243
|
+
meta = data.get("__meta__")
|
|
244
|
+
if not isinstance(meta, dict):
|
|
245
|
+
return {}
|
|
246
|
+
name = meta.get("name")
|
|
247
|
+
desc = meta.get("description")
|
|
248
|
+
version = meta.get("version")
|
|
249
|
+
category = meta.get("category")
|
|
250
|
+
out: Dict[str, str] = {}
|
|
251
|
+
if isinstance(name, str):
|
|
252
|
+
out["name"] = name
|
|
253
|
+
if isinstance(desc, str):
|
|
254
|
+
out["description"] = desc
|
|
255
|
+
if isinstance(version, str):
|
|
256
|
+
out["version"] = version
|
|
257
|
+
if isinstance(category, str):
|
|
258
|
+
out["category"] = category
|
|
259
|
+
return out
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def load_spec_records(specs_dir: Path) -> List[Dict[str, Any]]:
|
|
263
|
+
records: List[Dict[str, Any]] = []
|
|
264
|
+
if not specs_dir.exists():
|
|
265
|
+
return records
|
|
266
|
+
spec_files = list(specs_dir.rglob("*.yml")) + list(specs_dir.rglob("*.yaml"))
|
|
267
|
+
for spec_path in sorted(spec_files):
|
|
268
|
+
relpath = str(spec_path.relative_to(specs_dir))
|
|
269
|
+
meta = load_spec_meta(spec_path)
|
|
270
|
+
records.append(
|
|
271
|
+
{
|
|
272
|
+
"file": relpath,
|
|
273
|
+
"path": spec_path,
|
|
274
|
+
"name": meta.get("name"),
|
|
275
|
+
"version": meta.get("version"),
|
|
276
|
+
"description": meta.get("description"),
|
|
277
|
+
"category": meta.get("category"),
|
|
278
|
+
}
|
|
279
|
+
)
|
|
280
|
+
return records
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def load_pruner_spec_records(specs_dir: Path) -> List[Dict[str, Any]]:
|
|
284
|
+
records: List[Dict[str, Any]] = []
|
|
285
|
+
if not specs_dir.exists():
|
|
286
|
+
return records
|
|
287
|
+
spec_files = list(specs_dir.rglob("*.yml")) + list(specs_dir.rglob("*.yaml"))
|
|
288
|
+
for spec_path in sorted(spec_files):
|
|
289
|
+
relpath = str(spec_path.relative_to(specs_dir))
|
|
290
|
+
meta = load_spec_meta(spec_path)
|
|
291
|
+
records.append(
|
|
292
|
+
{
|
|
293
|
+
"file": relpath,
|
|
294
|
+
"path": spec_path,
|
|
295
|
+
"name": meta.get("name"),
|
|
296
|
+
"version": meta.get("version"),
|
|
297
|
+
"description": meta.get("description"),
|
|
298
|
+
"category": meta.get("category"),
|
|
299
|
+
}
|
|
300
|
+
)
|
|
301
|
+
return records
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def resolve_spec_by_name(
|
|
305
|
+
name: str,
|
|
306
|
+
*,
|
|
307
|
+
category: Optional[str],
|
|
308
|
+
version: Optional[str],
|
|
309
|
+
base: Path,
|
|
310
|
+
) -> Path:
|
|
311
|
+
paths = config_core.paths(root=base)
|
|
312
|
+
records = [r for r in load_spec_records(paths.specs_dir) if r.get("name") == name]
|
|
313
|
+
label = f"{category}:{name}" if category else name
|
|
314
|
+
if not records:
|
|
315
|
+
raise FileNotFoundError(f"Spec name not found: {label}")
|
|
316
|
+
if category:
|
|
317
|
+
records = [r for r in records if r.get("category") == category]
|
|
318
|
+
if not records:
|
|
319
|
+
raise FileNotFoundError(f"Spec category/name not found: {label}")
|
|
320
|
+
if version:
|
|
321
|
+
matches = [r for r in records if r.get("version") == version]
|
|
322
|
+
if not matches:
|
|
323
|
+
raise FileNotFoundError(f"Spec name/version not found: {label}@{version}")
|
|
324
|
+
if len(matches) > 1:
|
|
325
|
+
files = ", ".join(sorted(item["file"] for item in matches))
|
|
326
|
+
raise ValueError(f"Multiple specs share the same version for {name}: {files}")
|
|
327
|
+
return matches[0]["path"]
|
|
328
|
+
selected = select_latest(records)
|
|
329
|
+
return selected["path"]
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def resolve_spec_reference(
|
|
333
|
+
use: str,
|
|
334
|
+
*,
|
|
335
|
+
category: Optional[str] = None,
|
|
336
|
+
version: Optional[str] = None,
|
|
337
|
+
root: Optional[Union[str, Path]] = None,
|
|
338
|
+
) -> Path:
|
|
339
|
+
base = config_core.resolve_root(root)
|
|
340
|
+
if looks_like_spec_path(use):
|
|
341
|
+
spec_path = resolve_spec_path(use, base)
|
|
342
|
+
if not spec_path.exists():
|
|
343
|
+
raise FileNotFoundError(
|
|
344
|
+
f"{spec_path} not found. Install the spec before adding rules."
|
|
345
|
+
)
|
|
346
|
+
return spec_path
|
|
347
|
+
return resolve_spec_by_name(use, category=category, version=version, base=base)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def resolve_pruner_spec_reference(
|
|
351
|
+
use: str,
|
|
352
|
+
*,
|
|
353
|
+
version: Optional[str] = None,
|
|
354
|
+
root: Optional[Union[str, Path]] = None,
|
|
355
|
+
) -> Path:
|
|
356
|
+
base = config_core.resolve_root(root)
|
|
357
|
+
candidate = Path(use)
|
|
358
|
+
if candidate.is_absolute():
|
|
359
|
+
return candidate
|
|
360
|
+
if candidate.parts and candidate.parts[0] == "pruner_specs":
|
|
361
|
+
spec_path = base / candidate
|
|
362
|
+
if spec_path.exists():
|
|
363
|
+
return spec_path
|
|
364
|
+
if candidate.suffix.lower() in _SPEC_EXTS:
|
|
365
|
+
spec_path = base / "pruner_specs" / candidate
|
|
366
|
+
if spec_path.exists():
|
|
367
|
+
return spec_path
|
|
368
|
+
paths = config_core.paths(root=base)
|
|
369
|
+
records = [r for r in load_pruner_spec_records(paths.pruner_specs_dir) if r.get("name") == use]
|
|
370
|
+
if not records:
|
|
371
|
+
raise FileNotFoundError(f"Pruner spec name not found: {use}")
|
|
372
|
+
if version:
|
|
373
|
+
matches = [r for r in records if r.get("version") == version]
|
|
374
|
+
if not matches:
|
|
375
|
+
raise FileNotFoundError(f"Pruner spec name/version not found: {use}@{version}")
|
|
376
|
+
if len(matches) > 1:
|
|
377
|
+
files = ", ".join(sorted(item["file"] for item in matches))
|
|
378
|
+
raise ValueError(f"Multiple pruner specs share the same version for {use}: {files}")
|
|
379
|
+
return matches[0]["path"]
|
|
380
|
+
selected = select_latest(records)
|
|
381
|
+
return selected["path"]
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
__all__ = [
|
|
385
|
+
"RULE_KEYS",
|
|
386
|
+
"warn_dependencies",
|
|
387
|
+
"rules_using_spec",
|
|
388
|
+
"specs_including_spec",
|
|
389
|
+
"extract_transforms_source",
|
|
390
|
+
"collect_transforms_sources",
|
|
391
|
+
"specs_using_transform",
|
|
392
|
+
"resolve_spec_path",
|
|
393
|
+
"looks_like_spec_path",
|
|
394
|
+
"version_key",
|
|
395
|
+
"select_latest",
|
|
396
|
+
"load_spec_meta",
|
|
397
|
+
"load_spec_records",
|
|
398
|
+
"load_pruner_spec_records",
|
|
399
|
+
"resolve_spec_by_name",
|
|
400
|
+
"resolve_spec_reference",
|
|
401
|
+
"resolve_pruner_spec_reference",
|
|
402
|
+
]
|