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/apps/hook/core.py
ADDED
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
"""Hook installer utilities for converter hook packages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib
|
|
6
|
+
import importlib.metadata
|
|
7
|
+
import logging
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union, cast
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
from importlib import resources
|
|
14
|
+
except ImportError: # pragma: no cover - fallback for Python 3.8
|
|
15
|
+
import importlib_resources as resources # type: ignore[assignment]
|
|
16
|
+
import yaml
|
|
17
|
+
|
|
18
|
+
from ...core import config as config_core
|
|
19
|
+
from ...core.entrypoints import list_entry_points
|
|
20
|
+
from ...specs.hook.logic import DEFAULT_GROUP
|
|
21
|
+
from ..addon import installation as addon_install
|
|
22
|
+
from ..addon import dependencies as addon_deps
|
|
23
|
+
from ..addon.io import write_file as _write_file
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger("brkraw")
|
|
26
|
+
|
|
27
|
+
REGISTRY_FILENAME = "hooks.yaml"
|
|
28
|
+
MANIFEST_NAMES = ("brkraw_hook.yaml", "brkraw_hook.yml")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _metadata_get(
|
|
32
|
+
dist: Optional[importlib.metadata.Distribution],
|
|
33
|
+
key: str,
|
|
34
|
+
default: Optional[str] = None,
|
|
35
|
+
) -> Optional[str]:
|
|
36
|
+
"""Typed helper to read distribution metadata."""
|
|
37
|
+
if dist is None:
|
|
38
|
+
return default
|
|
39
|
+
meta = cast(Mapping[str, str], dist.metadata)
|
|
40
|
+
return meta.get(key, default)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _packages_distributions() -> Mapping[str, List[str]]:
|
|
44
|
+
packages_distributions = getattr(importlib.metadata, "packages_distributions", None)
|
|
45
|
+
if packages_distributions is not None:
|
|
46
|
+
return cast(Callable[[], Mapping[str, List[str]]], packages_distributions)()
|
|
47
|
+
|
|
48
|
+
mapping: Dict[str, List[str]] = {}
|
|
49
|
+
for dist in importlib.metadata.distributions():
|
|
50
|
+
read_text = getattr(dist, "read_text", None)
|
|
51
|
+
top_level_text = ""
|
|
52
|
+
if callable(read_text):
|
|
53
|
+
top_level = read_text("top_level.txt")
|
|
54
|
+
if isinstance(top_level, str):
|
|
55
|
+
top_level_text = top_level
|
|
56
|
+
dist_name = _metadata_get(dist, "Name")
|
|
57
|
+
if not dist_name:
|
|
58
|
+
continue
|
|
59
|
+
for package in top_level_text.splitlines():
|
|
60
|
+
package = package.strip()
|
|
61
|
+
if package:
|
|
62
|
+
mapping.setdefault(package, []).append(dist_name)
|
|
63
|
+
return mapping
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def list_hooks(*, root: Optional[Union[str, Path]] = None) -> List[Dict[str, Any]]:
|
|
67
|
+
hooks = _collect_hooks()
|
|
68
|
+
registry = _load_registry(root=root)
|
|
69
|
+
installed = registry.get("hooks", {})
|
|
70
|
+
root_path = config_core.resolve_root(root)
|
|
71
|
+
for hook in hooks:
|
|
72
|
+
entry = installed.get(hook["name"])
|
|
73
|
+
hook["installed"] = bool(entry)
|
|
74
|
+
hook["installed_version"] = entry.get("version") if isinstance(entry, dict) else None
|
|
75
|
+
hook["install_status"] = _install_status(entry, root=root_path)
|
|
76
|
+
return hooks
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def install_all(
|
|
80
|
+
*,
|
|
81
|
+
root: Optional[Union[str, Path]] = None,
|
|
82
|
+
upgrade: bool = False,
|
|
83
|
+
force: bool = False,
|
|
84
|
+
) -> Dict[str, List[str]]:
|
|
85
|
+
hooks = _collect_hooks()
|
|
86
|
+
installed: List[str] = []
|
|
87
|
+
skipped: List[str] = []
|
|
88
|
+
for hook in hooks:
|
|
89
|
+
status = _install_hook(hook, root=root, upgrade=upgrade, force=force)
|
|
90
|
+
if status == "installed":
|
|
91
|
+
installed.append(hook["name"])
|
|
92
|
+
else:
|
|
93
|
+
skipped.append(hook["name"])
|
|
94
|
+
return {"installed": installed, "skipped": skipped}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def install_hook(
|
|
98
|
+
target: str,
|
|
99
|
+
*,
|
|
100
|
+
root: Optional[Union[str, Path]] = None,
|
|
101
|
+
upgrade: bool = False,
|
|
102
|
+
force: bool = False,
|
|
103
|
+
) -> str:
|
|
104
|
+
hook = _resolve_hook_target(target)
|
|
105
|
+
return _install_hook(hook, root=root, upgrade=upgrade, force=force)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def read_hook_docs(
|
|
109
|
+
target: str,
|
|
110
|
+
*,
|
|
111
|
+
root: Optional[Union[str, Path]] = None,
|
|
112
|
+
) -> Tuple[str, str]:
|
|
113
|
+
hook = _resolve_hook_target(target)
|
|
114
|
+
manifest_path, manifest = _load_manifest(hook["dist"], hook.get("packages"))
|
|
115
|
+
docs_path = _resolve_docs_path(manifest, manifest_path)
|
|
116
|
+
if docs_path is None:
|
|
117
|
+
raise FileNotFoundError(f"Hook docs not found for {hook['name']}")
|
|
118
|
+
return hook["name"], docs_path.read_text(encoding="utf-8")
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def uninstall_hook(
|
|
122
|
+
target: str,
|
|
123
|
+
*,
|
|
124
|
+
root: Optional[Union[str, Path]] = None,
|
|
125
|
+
force: bool = False,
|
|
126
|
+
) -> Tuple[str, Dict[str, List[str]], bool]:
|
|
127
|
+
registry = _load_registry(root=root)
|
|
128
|
+
hooks = registry.get("hooks", {})
|
|
129
|
+
hook_name = _resolve_hook_name(target)
|
|
130
|
+
entry = hooks.get(hook_name)
|
|
131
|
+
if entry is None:
|
|
132
|
+
entry_matches = [
|
|
133
|
+
name
|
|
134
|
+
for name, data in hooks.items()
|
|
135
|
+
if target in (data.get("entrypoints") or [])
|
|
136
|
+
]
|
|
137
|
+
if len(entry_matches) == 1:
|
|
138
|
+
hook_name = entry_matches[0]
|
|
139
|
+
entry = hooks.get(hook_name)
|
|
140
|
+
elif entry_matches:
|
|
141
|
+
names = ", ".join(sorted(entry_matches))
|
|
142
|
+
raise ValueError(f"Multiple hooks match {target}: {names}")
|
|
143
|
+
if entry is None:
|
|
144
|
+
raise LookupError(f"Hook not installed: {hook_name}")
|
|
145
|
+
module_missing = not list_entry_points(DEFAULT_GROUP, name=hook_name)
|
|
146
|
+
removed: Dict[str, List[str]] = {
|
|
147
|
+
"specs": [],
|
|
148
|
+
"pruner_specs": [],
|
|
149
|
+
"rules": [],
|
|
150
|
+
"transforms": [],
|
|
151
|
+
}
|
|
152
|
+
root_path = config_core.resolve_root(root)
|
|
153
|
+
for kind in ("specs", "pruner_specs", "rules", "transforms"):
|
|
154
|
+
for relpath in entry.get(kind, []) if isinstance(entry, dict) else []:
|
|
155
|
+
target_path = root_path / relpath
|
|
156
|
+
if not target_path.exists():
|
|
157
|
+
continue
|
|
158
|
+
if _has_dependencies(target_path, kind, root=root_path) and not force:
|
|
159
|
+
raise RuntimeError("Dependencies found; use --force to remove.")
|
|
160
|
+
target_path.unlink()
|
|
161
|
+
removed[kind].append(relpath)
|
|
162
|
+
hooks.pop(hook_name, None)
|
|
163
|
+
_save_registry(registry, root=root)
|
|
164
|
+
return hook_name, removed, module_missing
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _install_hook(
|
|
168
|
+
hook: Dict[str, Any],
|
|
169
|
+
*,
|
|
170
|
+
root: Optional[Union[str, Path]],
|
|
171
|
+
upgrade: bool,
|
|
172
|
+
force: bool,
|
|
173
|
+
) -> str:
|
|
174
|
+
registry = _load_registry(root=root)
|
|
175
|
+
hooks = registry.setdefault("hooks", {})
|
|
176
|
+
existing = hooks.get(hook["name"])
|
|
177
|
+
if isinstance(existing, dict):
|
|
178
|
+
if not upgrade and not force:
|
|
179
|
+
return "skipped"
|
|
180
|
+
if not force:
|
|
181
|
+
installed_version = existing.get("version")
|
|
182
|
+
if installed_version and not _is_version_newer(hook["version"], installed_version):
|
|
183
|
+
return "skipped"
|
|
184
|
+
manifest_path, manifest = _load_manifest(hook["dist"], hook.get("packages"))
|
|
185
|
+
namespace = _namespace_for_hook(hook["name"])
|
|
186
|
+
installed = _install_manifest(
|
|
187
|
+
manifest,
|
|
188
|
+
manifest_path,
|
|
189
|
+
root=root,
|
|
190
|
+
namespace=namespace,
|
|
191
|
+
)
|
|
192
|
+
hooks[hook["name"]] = {
|
|
193
|
+
"version": hook["version"],
|
|
194
|
+
"entrypoints": hook["entrypoints"],
|
|
195
|
+
"namespace": namespace,
|
|
196
|
+
**installed,
|
|
197
|
+
}
|
|
198
|
+
_save_registry(registry, root=root)
|
|
199
|
+
return "installed"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _kind_to_remove(kind: str) -> str:
|
|
203
|
+
if kind == "specs":
|
|
204
|
+
return "spec"
|
|
205
|
+
if kind == "pruner_specs":
|
|
206
|
+
return "pruner"
|
|
207
|
+
if kind == "rules":
|
|
208
|
+
return "rule"
|
|
209
|
+
if kind == "transforms":
|
|
210
|
+
return "transform"
|
|
211
|
+
raise ValueError(f"Unknown hook kind: {kind}")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _install_manifest(
|
|
215
|
+
manifest: Mapping[str, Any],
|
|
216
|
+
manifest_path: Path,
|
|
217
|
+
*,
|
|
218
|
+
root: Optional[Union[str, Path]],
|
|
219
|
+
namespace: str,
|
|
220
|
+
) -> Dict[str, List[str]]:
|
|
221
|
+
installed: Dict[str, List[str]] = {
|
|
222
|
+
"specs": [],
|
|
223
|
+
"pruner_specs": [],
|
|
224
|
+
"rules": [],
|
|
225
|
+
"transforms": [],
|
|
226
|
+
}
|
|
227
|
+
base_dir = manifest_path.parent
|
|
228
|
+
paths = config_core.paths(root=root)
|
|
229
|
+
spec_basenames = {
|
|
230
|
+
Path(item).name
|
|
231
|
+
for item in _normalize_manifest_list(manifest.get("specs"))
|
|
232
|
+
}
|
|
233
|
+
for spec in _normalize_manifest_list(manifest.get("specs")):
|
|
234
|
+
src = _resolve_manifest_path(base_dir, spec)
|
|
235
|
+
spec_data = _read_yaml(src)
|
|
236
|
+
paths_installed = addon_install.add_spec_data(
|
|
237
|
+
spec_data,
|
|
238
|
+
filename=str(Path(namespace) / src.name),
|
|
239
|
+
source_path=src,
|
|
240
|
+
root=root,
|
|
241
|
+
transforms_dir=paths.transforms_dir / namespace,
|
|
242
|
+
)
|
|
243
|
+
_record_installed_paths(paths_installed, installed, root=root)
|
|
244
|
+
for pruner_spec in _normalize_manifest_list(manifest.get("pruner_specs")):
|
|
245
|
+
src = _resolve_manifest_path(base_dir, pruner_spec)
|
|
246
|
+
spec_data = _read_yaml(src)
|
|
247
|
+
paths_installed = addon_install.add_pruner_spec_data(
|
|
248
|
+
spec_data,
|
|
249
|
+
filename=str(Path(namespace) / src.name),
|
|
250
|
+
source_path=src,
|
|
251
|
+
root=root,
|
|
252
|
+
)
|
|
253
|
+
_record_installed_paths(paths_installed, installed, root=root)
|
|
254
|
+
for rule in _normalize_manifest_list(manifest.get("rules")):
|
|
255
|
+
src = _resolve_manifest_path(base_dir, rule)
|
|
256
|
+
rule_data = _read_yaml(src)
|
|
257
|
+
_rewrite_rule_uses(rule_data, namespace, spec_basenames)
|
|
258
|
+
paths_installed = addon_install.add_rule_data(
|
|
259
|
+
rule_data,
|
|
260
|
+
filename=str(Path(namespace) / src.name),
|
|
261
|
+
source_path=src,
|
|
262
|
+
root=root,
|
|
263
|
+
)
|
|
264
|
+
_record_installed_paths(paths_installed, installed, root=root)
|
|
265
|
+
for transform in _normalize_manifest_list(manifest.get("transforms")):
|
|
266
|
+
src = _resolve_manifest_path(base_dir, transform)
|
|
267
|
+
target = paths.transforms_dir / namespace / src.name
|
|
268
|
+
_write_file(target, src.read_text(encoding="utf-8"))
|
|
269
|
+
installed["transforms"].append(_relative_to_root(target, root=root))
|
|
270
|
+
logger.info("Installed transforms: %s", target)
|
|
271
|
+
return installed
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _record_installed_paths(
|
|
275
|
+
paths: Sequence[Path],
|
|
276
|
+
installed: Dict[str, List[str]],
|
|
277
|
+
*,
|
|
278
|
+
root: Optional[Union[str, Path]],
|
|
279
|
+
) -> None:
|
|
280
|
+
config_paths = config_core.paths(root=root)
|
|
281
|
+
for path in paths:
|
|
282
|
+
relpath = _relative_to_root(path, root=root)
|
|
283
|
+
try:
|
|
284
|
+
path.relative_to(config_paths.specs_dir)
|
|
285
|
+
except ValueError:
|
|
286
|
+
pass
|
|
287
|
+
else:
|
|
288
|
+
installed["specs"].append(relpath)
|
|
289
|
+
continue
|
|
290
|
+
try:
|
|
291
|
+
path.relative_to(config_paths.pruner_specs_dir)
|
|
292
|
+
except ValueError:
|
|
293
|
+
pass
|
|
294
|
+
else:
|
|
295
|
+
installed["pruner_specs"].append(relpath)
|
|
296
|
+
continue
|
|
297
|
+
try:
|
|
298
|
+
path.relative_to(config_paths.rules_dir)
|
|
299
|
+
except ValueError:
|
|
300
|
+
pass
|
|
301
|
+
else:
|
|
302
|
+
installed["rules"].append(relpath)
|
|
303
|
+
continue
|
|
304
|
+
try:
|
|
305
|
+
path.relative_to(config_paths.transforms_dir)
|
|
306
|
+
except ValueError:
|
|
307
|
+
continue
|
|
308
|
+
installed["transforms"].append(relpath)
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _normalize_manifest_list(value: Any) -> List[str]:
|
|
312
|
+
if value is None:
|
|
313
|
+
return []
|
|
314
|
+
if isinstance(value, str):
|
|
315
|
+
return [value]
|
|
316
|
+
if isinstance(value, list):
|
|
317
|
+
items = [item for item in value if isinstance(item, str) and item.strip()]
|
|
318
|
+
return items
|
|
319
|
+
raise ValueError("Manifest entries must be a list of strings.")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _resolve_manifest_path(base_dir: Path, value: str) -> Path:
|
|
323
|
+
path = Path(value).expanduser()
|
|
324
|
+
if not path.is_absolute():
|
|
325
|
+
path = (base_dir / path).resolve()
|
|
326
|
+
if not path.exists():
|
|
327
|
+
raise FileNotFoundError(path)
|
|
328
|
+
return path
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _read_yaml(path: Path) -> Dict[str, Any]:
|
|
332
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
333
|
+
if data is None:
|
|
334
|
+
raise ValueError(f"Empty YAML file: {path}")
|
|
335
|
+
if not isinstance(data, dict):
|
|
336
|
+
raise ValueError(f"YAML file must be a mapping: {path}")
|
|
337
|
+
return data
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _collect_hooks() -> List[Dict[str, Any]]:
|
|
341
|
+
hooks: Dict[str, Dict[str, Any]] = {}
|
|
342
|
+
for ep in list_entry_points(DEFAULT_GROUP):
|
|
343
|
+
dist = _resolve_distribution(ep)
|
|
344
|
+
dist_name = _dist_name(dist) or ep.name
|
|
345
|
+
entry = hooks.setdefault(
|
|
346
|
+
dist_name,
|
|
347
|
+
{
|
|
348
|
+
"name": dist_name,
|
|
349
|
+
"entrypoints": [],
|
|
350
|
+
"packages": set(),
|
|
351
|
+
"dist": dist,
|
|
352
|
+
"version": _dist_version(dist),
|
|
353
|
+
"author": _dist_author(dist),
|
|
354
|
+
"description": _dist_description(dist),
|
|
355
|
+
},
|
|
356
|
+
)
|
|
357
|
+
entry["entrypoints"].append(ep.name)
|
|
358
|
+
pkg = _entrypoint_package(ep)
|
|
359
|
+
if pkg:
|
|
360
|
+
entry["packages"].add(pkg)
|
|
361
|
+
for hook in hooks.values():
|
|
362
|
+
packages = hook.get("packages")
|
|
363
|
+
if isinstance(packages, set):
|
|
364
|
+
hook["packages"] = sorted(packages)
|
|
365
|
+
return sorted(hooks.values(), key=lambda item: item["name"])
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def _resolve_hook_target(target: str) -> Dict[str, Any]:
|
|
369
|
+
hooks = _collect_hooks()
|
|
370
|
+
name_matches = [hook for hook in hooks if hook["name"] == target]
|
|
371
|
+
if len(name_matches) == 1:
|
|
372
|
+
return name_matches[0]
|
|
373
|
+
entry_matches = [
|
|
374
|
+
hook for hook in hooks if target in hook.get("entrypoints", [])
|
|
375
|
+
]
|
|
376
|
+
if len(entry_matches) == 1:
|
|
377
|
+
return entry_matches[0]
|
|
378
|
+
if not name_matches and not entry_matches:
|
|
379
|
+
raise LookupError(f"Unknown hook: {target}")
|
|
380
|
+
names = sorted({hook["name"] for hook in name_matches + entry_matches})
|
|
381
|
+
raise ValueError(f"Multiple hooks match {target}: {', '.join(names)}")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _resolve_hook_name(target: str) -> str:
|
|
385
|
+
try:
|
|
386
|
+
return _resolve_hook_target(target)["name"]
|
|
387
|
+
except LookupError:
|
|
388
|
+
return target
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _resolve_distribution(ep: importlib.metadata.EntryPoint) -> Optional[importlib.metadata.Distribution]:
|
|
392
|
+
dist = getattr(ep, "dist", None)
|
|
393
|
+
if dist is not None:
|
|
394
|
+
return dist
|
|
395
|
+
pkg = getattr(ep, "module", "").split(".")[0]
|
|
396
|
+
if not pkg:
|
|
397
|
+
return None
|
|
398
|
+
mapping = _packages_distributions()
|
|
399
|
+
dist_names = mapping.get(pkg, [])
|
|
400
|
+
if not dist_names:
|
|
401
|
+
return None
|
|
402
|
+
try:
|
|
403
|
+
return importlib.metadata.distribution(dist_names[0])
|
|
404
|
+
except importlib.metadata.PackageNotFoundError:
|
|
405
|
+
return None
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _dist_name(dist: Optional[importlib.metadata.Distribution]) -> Optional[str]:
|
|
409
|
+
if dist is None:
|
|
410
|
+
return None
|
|
411
|
+
return _metadata_get(dist, "Name")
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _dist_version(dist: Optional[importlib.metadata.Distribution]) -> str:
|
|
415
|
+
if dist is None:
|
|
416
|
+
return "<Unknown>"
|
|
417
|
+
return _metadata_get(dist, "Version", "<Unknown>") or "<Unknown>"
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _dist_description(dist: Optional[importlib.metadata.Distribution]) -> str:
|
|
421
|
+
if dist is None:
|
|
422
|
+
return "<Unknown>"
|
|
423
|
+
return _metadata_get(dist, "Summary", "<Unknown>") or "<Unknown>"
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _dist_author(dist: Optional[importlib.metadata.Distribution]) -> str:
|
|
427
|
+
if dist is None:
|
|
428
|
+
return "<Unknown>"
|
|
429
|
+
for key in ("Author", "Author-email", "Maintainer", "Maintainer-email"):
|
|
430
|
+
value = _metadata_get(dist, key)
|
|
431
|
+
if value:
|
|
432
|
+
return value
|
|
433
|
+
return "<Unknown>"
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _load_manifest(
|
|
437
|
+
dist: Optional[importlib.metadata.Distribution],
|
|
438
|
+
packages: Optional[Sequence[str]] = None,
|
|
439
|
+
) -> Tuple[Path, Dict[str, Any]]:
|
|
440
|
+
if dist is None:
|
|
441
|
+
raise FileNotFoundError("Hook distribution not available.")
|
|
442
|
+
manifest = _find_manifest(dist, packages=packages)
|
|
443
|
+
if manifest is None:
|
|
444
|
+
raise FileNotFoundError(
|
|
445
|
+
f"No hook manifest found in {_metadata_get(dist, 'Name', '<Unknown>')}"
|
|
446
|
+
)
|
|
447
|
+
data = _read_yaml(manifest)
|
|
448
|
+
return manifest, data
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _find_manifest(
|
|
452
|
+
dist: importlib.metadata.Distribution,
|
|
453
|
+
*,
|
|
454
|
+
packages: Optional[Sequence[str]] = None,
|
|
455
|
+
) -> Optional[Path]:
|
|
456
|
+
files = dist.files or []
|
|
457
|
+
for name in MANIFEST_NAMES:
|
|
458
|
+
for entry in files:
|
|
459
|
+
if entry.name == name:
|
|
460
|
+
return Path(str(dist.locate_file(entry)))
|
|
461
|
+
search_packages = list(packages or []) or _dist_top_level(dist)
|
|
462
|
+
for package in search_packages:
|
|
463
|
+
for name in MANIFEST_NAMES:
|
|
464
|
+
try:
|
|
465
|
+
candidate = resources.files(package).joinpath(name)
|
|
466
|
+
except Exception:
|
|
467
|
+
candidate = None
|
|
468
|
+
if candidate is not None and candidate.is_file():
|
|
469
|
+
return Path(str(candidate))
|
|
470
|
+
fallback = _find_manifest_in_module(package)
|
|
471
|
+
if fallback is not None:
|
|
472
|
+
return fallback
|
|
473
|
+
return None
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _entrypoint_package(ep: importlib.metadata.EntryPoint) -> Optional[str]:
|
|
477
|
+
module = getattr(ep, "module", None)
|
|
478
|
+
if not module:
|
|
479
|
+
value = getattr(ep, "value", "")
|
|
480
|
+
module = value.split(":", 1)[0] if value else ""
|
|
481
|
+
if not module:
|
|
482
|
+
return None
|
|
483
|
+
return module.split(".", 1)[0]
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _find_manifest_in_module(package: str) -> Optional[Path]:
|
|
487
|
+
try:
|
|
488
|
+
module = importlib.import_module(package)
|
|
489
|
+
except Exception:
|
|
490
|
+
return None
|
|
491
|
+
module_file = getattr(module, "__file__", None)
|
|
492
|
+
if not module_file:
|
|
493
|
+
return None
|
|
494
|
+
base = Path(module_file).resolve().parent
|
|
495
|
+
for name in MANIFEST_NAMES:
|
|
496
|
+
candidate = base / name
|
|
497
|
+
if candidate.is_file():
|
|
498
|
+
return candidate
|
|
499
|
+
return None
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _dist_top_level(dist: importlib.metadata.Distribution) -> List[str]:
|
|
503
|
+
raw = dist.read_text("top_level.txt")
|
|
504
|
+
if not raw:
|
|
505
|
+
return []
|
|
506
|
+
return [line.strip() for line in raw.splitlines() if line.strip()]
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _registry_path(root: Optional[Union[str, Path]]) -> Path:
|
|
510
|
+
base = config_core.resolve_root(root)
|
|
511
|
+
return base / REGISTRY_FILENAME
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _load_registry(*, root: Optional[Union[str, Path]]) -> Dict[str, Any]:
|
|
515
|
+
path = _registry_path(root)
|
|
516
|
+
if not path.exists():
|
|
517
|
+
return {"hooks": {}}
|
|
518
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
519
|
+
if not isinstance(data, dict):
|
|
520
|
+
return {"hooks": {}}
|
|
521
|
+
if "hooks" not in data:
|
|
522
|
+
data["hooks"] = {}
|
|
523
|
+
return data
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def _save_registry(data: Mapping[str, Any], *, root: Optional[Union[str, Path]]) -> None:
|
|
527
|
+
config_core.ensure_initialized(root=root, create_config=True, exist_ok=True)
|
|
528
|
+
path = _registry_path(root)
|
|
529
|
+
content = yaml.safe_dump(dict(data), sort_keys=False)
|
|
530
|
+
path.write_text(content, encoding="utf-8")
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
def _is_version_newer(version: str, installed: str) -> bool:
|
|
534
|
+
if version == "<Unknown>" or installed == "<Unknown>":
|
|
535
|
+
return True
|
|
536
|
+
try:
|
|
537
|
+
from packaging.version import Version # type: ignore
|
|
538
|
+
except Exception:
|
|
539
|
+
return _compare_fallback(version, installed) > 0
|
|
540
|
+
return Version(version) > Version(installed)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def _compare_fallback(a: str, b: str) -> int:
|
|
544
|
+
def _split(value: str) -> Tuple[Tuple[int, ...], str]:
|
|
545
|
+
nums = tuple(int(item) for item in re.findall(r"\d+", value))
|
|
546
|
+
return nums, value
|
|
547
|
+
|
|
548
|
+
a_nums, a_raw = _split(a)
|
|
549
|
+
b_nums, b_raw = _split(b)
|
|
550
|
+
if a_nums != b_nums:
|
|
551
|
+
return (a_nums > b_nums) - (a_nums < b_nums)
|
|
552
|
+
return (a_raw > b_raw) - (a_raw < b_raw)
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _namespace_for_hook(name: str) -> str:
|
|
556
|
+
return re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip("_") or "hook"
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def _rewrite_rule_uses(rule_data: Dict[str, Any], namespace: str, spec_basenames: set[str]) -> None:
|
|
560
|
+
for key in addon_deps.RULE_KEYS:
|
|
561
|
+
if key == "converter_hook":
|
|
562
|
+
continue
|
|
563
|
+
entries = rule_data.get(key) or []
|
|
564
|
+
if not isinstance(entries, list):
|
|
565
|
+
continue
|
|
566
|
+
for item in entries:
|
|
567
|
+
if not isinstance(item, dict):
|
|
568
|
+
continue
|
|
569
|
+
use = item.get("use")
|
|
570
|
+
if not isinstance(use, str):
|
|
571
|
+
continue
|
|
572
|
+
base = Path(use).name
|
|
573
|
+
if base in spec_basenames:
|
|
574
|
+
item["use"] = str(Path("specs") / namespace / base)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _relative_to_root(path: Path, *, root: Optional[Union[str, Path]]) -> str:
|
|
578
|
+
base = config_core.resolve_root(root)
|
|
579
|
+
try:
|
|
580
|
+
return str(path.resolve().relative_to(base))
|
|
581
|
+
except ValueError:
|
|
582
|
+
return str(path.name)
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _has_dependencies(
|
|
586
|
+
target: Path,
|
|
587
|
+
kind: str,
|
|
588
|
+
*,
|
|
589
|
+
root: Optional[Union[str, Path]],
|
|
590
|
+
) -> bool:
|
|
591
|
+
try:
|
|
592
|
+
return addon_deps.warn_dependencies(target, kind=_kind_to_remove(kind), root=root)
|
|
593
|
+
except Exception:
|
|
594
|
+
return False
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
def _resolve_docs_path(manifest: Mapping[str, Any], manifest_path: Path) -> Optional[Path]:
|
|
598
|
+
value = manifest.get("docs")
|
|
599
|
+
if not value:
|
|
600
|
+
value = manifest.get("readme")
|
|
601
|
+
if not isinstance(value, str):
|
|
602
|
+
return None
|
|
603
|
+
return _resolve_manifest_path(manifest_path.parent, value)
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _install_status(entry: Any, *, root: Path) -> str:
|
|
607
|
+
if not isinstance(entry, dict):
|
|
608
|
+
return "No"
|
|
609
|
+
paths: List[str] = []
|
|
610
|
+
for kind in ("specs", "pruner_specs", "rules", "transforms"):
|
|
611
|
+
items = entry.get(kind, [])
|
|
612
|
+
if isinstance(items, list):
|
|
613
|
+
paths.extend([item for item in items if isinstance(item, str) and item.strip()])
|
|
614
|
+
if not paths:
|
|
615
|
+
return "Yes"
|
|
616
|
+
missing = 0
|
|
617
|
+
for relpath in paths:
|
|
618
|
+
if not (root / relpath).exists():
|
|
619
|
+
missing += 1
|
|
620
|
+
if missing == 0:
|
|
621
|
+
return "Yes"
|
|
622
|
+
return "Partially"
|
|
623
|
+
|
|
624
|
+
a_nums, a_raw = _split(a)
|
|
625
|
+
b_nums, b_raw = _split(b)
|
|
626
|
+
if a_nums != b_nums:
|
|
627
|
+
return (a_nums > b_nums) - (a_nums < b_nums)
|
|
628
|
+
return (a_raw > b_raw) - (a_raw < b_raw)
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
__all__ = [
|
|
632
|
+
"install_all",
|
|
633
|
+
"install_hook",
|
|
634
|
+
"list_hooks",
|
|
635
|
+
"uninstall_hook",
|
|
636
|
+
]
|