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,220 @@
|
|
|
1
|
+
"""Typing helpers for app-level loader interfaces.
|
|
2
|
+
|
|
3
|
+
Last updated: 2025-12-30
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any, Union, Tuple, Dict, Optional, Protocol, Literal, Mapping, Callable, List, TYPE_CHECKING
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from typing_extensions import ParamSpec, TypeAlias
|
|
11
|
+
else:
|
|
12
|
+
try:
|
|
13
|
+
from typing import ParamSpec, TypeAlias
|
|
14
|
+
except ImportError: # pragma: no cover - fallback for Python 3.8
|
|
15
|
+
from typing_extensions import ParamSpec, TypeAlias
|
|
16
|
+
from ...dataclasses.study import Study
|
|
17
|
+
from ...dataclasses.scan import Scan
|
|
18
|
+
from ...dataclasses.reco import Reco
|
|
19
|
+
import numpy as np
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from ...core.parameters import Parameters
|
|
24
|
+
from ...resolver.image import ResolvedImage
|
|
25
|
+
from ...resolver.affine import ResolvedAffine, SubjectType, SubjectPose
|
|
26
|
+
from ...resolver.nifti import Nifti1HeaderContents, XYZUNIT, TUNIT
|
|
27
|
+
from nibabel.nifti1 import Nifti1Image
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
InfoScope = Literal['full', 'study', 'scan']
|
|
32
|
+
AffineReturn = Optional[Union[np.ndarray, Tuple[np.ndarray, ...]]]
|
|
33
|
+
AffineSpace = Literal["raw", "scanner", "subject_ras"]
|
|
34
|
+
|
|
35
|
+
P = ParamSpec("P")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class GetDataobjType(Protocol[P]):
|
|
39
|
+
"""Callable signature for get_dataobj overrides."""
|
|
40
|
+
def __call__(
|
|
41
|
+
self,
|
|
42
|
+
scan: "Scan",
|
|
43
|
+
reco_id: Optional[int],
|
|
44
|
+
*args: P.args,
|
|
45
|
+
**kwargs: P.kwargs
|
|
46
|
+
) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class GetAffineType(Protocol):
|
|
51
|
+
"""Callable signature for get_affine overrides."""
|
|
52
|
+
def __call__(
|
|
53
|
+
self,
|
|
54
|
+
scan: "Scan",
|
|
55
|
+
reco_id: Optional[int],
|
|
56
|
+
*,
|
|
57
|
+
space: AffineSpace,
|
|
58
|
+
override_subject_type: Optional[SubjectType],
|
|
59
|
+
override_subject_pose: Optional[SubjectPose],
|
|
60
|
+
decimals: Optional[int] = None,
|
|
61
|
+
**kwargs: Any
|
|
62
|
+
) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class GetNifti1ImageType(Protocol):
|
|
67
|
+
"""Callable signature for get_nifti1image overrides."""
|
|
68
|
+
def __call__(
|
|
69
|
+
self,
|
|
70
|
+
scan: "Scan",
|
|
71
|
+
reco_id: Optional[int] = None,
|
|
72
|
+
*,
|
|
73
|
+
override_header: Optional[Union[dict, "Nifti1HeaderContents"]],
|
|
74
|
+
space: AffineSpace,
|
|
75
|
+
override_subject_type: Optional[SubjectType],
|
|
76
|
+
override_subject_pose: Optional[SubjectPose],
|
|
77
|
+
flip_x: bool,
|
|
78
|
+
flatten_fg: bool,
|
|
79
|
+
xyz_units: XYZUNIT,
|
|
80
|
+
t_units: TUNIT,
|
|
81
|
+
**kwargs: Any,
|
|
82
|
+
) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
|
|
83
|
+
...
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class ConvertType(Protocol):
|
|
87
|
+
"""Callable signature for convert overrides."""
|
|
88
|
+
def __call__(
|
|
89
|
+
self,
|
|
90
|
+
scan: "Scan",
|
|
91
|
+
reco_id: Optional[int] = None,
|
|
92
|
+
*,
|
|
93
|
+
format: Union[Literal["nifti", "nifti1"], str],
|
|
94
|
+
override_header: Optional[Union[dict, "Nifti1HeaderContents"]],
|
|
95
|
+
space: AffineSpace,
|
|
96
|
+
override_subject_type: Optional[SubjectType],
|
|
97
|
+
override_subject_pose: Optional[SubjectPose],
|
|
98
|
+
flip_x: bool,
|
|
99
|
+
flatten_fg: bool,
|
|
100
|
+
xyz_units: XYZUNIT,
|
|
101
|
+
t_units: TUNIT,
|
|
102
|
+
**kwargs: Any,
|
|
103
|
+
) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
|
|
104
|
+
...
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class BaseLoader(Protocol):
|
|
108
|
+
"""Base protocol for loader types that can search parameters."""
|
|
109
|
+
def search_params(
|
|
110
|
+
self, key: str,
|
|
111
|
+
*,
|
|
112
|
+
file: Optional[Union[str, List[str]]] = None,
|
|
113
|
+
scan_id: Optional[int] = None,
|
|
114
|
+
reco_id: Optional[int] = None
|
|
115
|
+
) -> Optional[dict]:
|
|
116
|
+
...
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class StudyLoader(Study, BaseLoader):
|
|
120
|
+
"""Study with attached loader helpers."""
|
|
121
|
+
subject: Parameters
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class ScanLoader(Scan, BaseLoader):
|
|
125
|
+
"""Scan with attached loader helpers and conversion overrides."""
|
|
126
|
+
|
|
127
|
+
image_info: Dict[int, Optional["ResolvedImage"]]
|
|
128
|
+
affine_info: Dict[int, Optional["ResolvedAffine"]]
|
|
129
|
+
_converter_hook: Optional[ConverterHook]
|
|
130
|
+
_converter_hook_name: Optional[str]
|
|
131
|
+
|
|
132
|
+
def get_fid(self,
|
|
133
|
+
buffer_start: Optional[int],
|
|
134
|
+
buffer_size: Optional[int],
|
|
135
|
+
*,
|
|
136
|
+
as_complex: bool) -> Optional[np.ndarray]:
|
|
137
|
+
...
|
|
138
|
+
|
|
139
|
+
def get_dataobj(
|
|
140
|
+
self,
|
|
141
|
+
reco_id: Optional[int] = None
|
|
142
|
+
) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
|
|
143
|
+
...
|
|
144
|
+
|
|
145
|
+
def get_affine(
|
|
146
|
+
self,
|
|
147
|
+
reco_id: Optional[int] = None,
|
|
148
|
+
*,
|
|
149
|
+
space: AffineSpace = "subject_ras",
|
|
150
|
+
override_subject_type: Optional[SubjectType],
|
|
151
|
+
override_subject_pose: Optional[SubjectPose],
|
|
152
|
+
decimals: Optional[int] = None,
|
|
153
|
+
**kwargs: Any,
|
|
154
|
+
) -> Optional[Union[Tuple["np.ndarray", ...], "np.ndarray"]]:
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
def get_nifti1image(
|
|
158
|
+
self,
|
|
159
|
+
reco_id: Optional[int] = None,
|
|
160
|
+
*,
|
|
161
|
+
override_header: Optional[Union[dict, "Nifti1HeaderContents"]],
|
|
162
|
+
space: AffineSpace = "subject_ras",
|
|
163
|
+
override_subject_type: Optional[SubjectType],
|
|
164
|
+
override_subject_pose: Optional[SubjectPose],
|
|
165
|
+
flip_x: bool,
|
|
166
|
+
flatten_fg: bool,
|
|
167
|
+
xyz_units: XYZUNIT,
|
|
168
|
+
t_units: TUNIT
|
|
169
|
+
) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
def convert(
|
|
173
|
+
self,
|
|
174
|
+
reco_id: Optional[int] = None,
|
|
175
|
+
*,
|
|
176
|
+
format: Literal["nifti", "nifti1"],
|
|
177
|
+
override_header: Optional[Union[dict, "Nifti1HeaderContents"]],
|
|
178
|
+
space: AffineSpace = "subject_ras",
|
|
179
|
+
override_subject_type: Optional[SubjectType],
|
|
180
|
+
override_subject_pose: Optional[SubjectPose],
|
|
181
|
+
flip_x: bool,
|
|
182
|
+
flatten_fg: bool,
|
|
183
|
+
xyz_units: XYZUNIT,
|
|
184
|
+
t_units: TUNIT,
|
|
185
|
+
hook_args_by_name: Optional[Mapping[str, Mapping[str, Any]]] = None,
|
|
186
|
+
) -> Optional[Union[Tuple["Nifti1Image", ...], "Nifti1Image"]]:
|
|
187
|
+
...
|
|
188
|
+
|
|
189
|
+
def get_metadata(
|
|
190
|
+
self,
|
|
191
|
+
reco_id: Optional[int] = None,
|
|
192
|
+
spec: Optional[Union[Mapping[str, Any], str, "Path"]] = None,
|
|
193
|
+
context_map: Optional[Union[str, "Path"]] = None,
|
|
194
|
+
return_spec: bool = False,
|
|
195
|
+
) -> Optional[Union[dict, Tuple[Optional[dict], Optional[dict]]]]:
|
|
196
|
+
...
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class RecoLoader(Reco, BaseLoader):
|
|
200
|
+
"""Reco with attached loader helpers."""
|
|
201
|
+
...
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
ConverterHook: TypeAlias = Mapping[str, Union[GetDataobjType[Any], GetAffineType, GetNifti1ImageType, ConvertType]]
|
|
205
|
+
"""Mapping of converter hook keys to override callables."""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
__all__ = [
|
|
209
|
+
'GetDataobjType',
|
|
210
|
+
'GetAffineType',
|
|
211
|
+
'GetNifti1ImageType',
|
|
212
|
+
'ConvertType',
|
|
213
|
+
'ConverterHook',
|
|
214
|
+
'StudyLoader',
|
|
215
|
+
'ScanLoader',
|
|
216
|
+
'RecoLoader',
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
def __dir__() -> List[str]:
|
|
220
|
+
return sorted(__all__)
|
brkraw/cli/__init__.py
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Dict, Optional
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import logging
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from brkraw.core import config as config_core
|
|
10
|
+
from brkraw.core import formatter
|
|
11
|
+
from brkraw.apps import addon as addon_app
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger("brkraw")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def cmd_addon(args: argparse.Namespace) -> int:
|
|
17
|
+
handler = getattr(args, "addon_func", None)
|
|
18
|
+
if handler is None:
|
|
19
|
+
args.parser.print_help()
|
|
20
|
+
return 2
|
|
21
|
+
return handler(args)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_row(row: Dict[str, str]) -> Dict[str, object]:
|
|
25
|
+
name = row.get("name", "")
|
|
26
|
+
desc = row.get("description", "")
|
|
27
|
+
category = row.get("category", "")
|
|
28
|
+
version = row.get("version", "")
|
|
29
|
+
name_cell: object = name
|
|
30
|
+
desc_cell: object = desc
|
|
31
|
+
category_cell: object = category
|
|
32
|
+
version_cell: object = version
|
|
33
|
+
if row.get("name_unknown") == "1":
|
|
34
|
+
name_cell = {"value": name, "color": "gray"}
|
|
35
|
+
if row.get("version_unknown") == "1":
|
|
36
|
+
version_cell = {"value": version, "color": "gray"}
|
|
37
|
+
if row.get("description_unknown") == "1":
|
|
38
|
+
desc_cell = {"value": desc, "color": "gray"}
|
|
39
|
+
if row.get("category_unknown") == "1":
|
|
40
|
+
category_cell = {"value": category, "color": "gray"}
|
|
41
|
+
return {
|
|
42
|
+
"file": row.get("file", ""),
|
|
43
|
+
"category": category_cell,
|
|
44
|
+
"name": name_cell,
|
|
45
|
+
"version": version_cell,
|
|
46
|
+
"description": desc_cell,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
def _normalize_pruner_row(row: Dict[str, str]) -> Dict[str, object]:
|
|
50
|
+
name = row.get("name", "")
|
|
51
|
+
desc = row.get("description", "")
|
|
52
|
+
version = row.get("version", "")
|
|
53
|
+
name_cell: object = name
|
|
54
|
+
desc_cell: object = desc
|
|
55
|
+
version_cell: object = version
|
|
56
|
+
if row.get("name_unknown") == "1":
|
|
57
|
+
name_cell = {"value": name, "color": "gray"}
|
|
58
|
+
if row.get("version_unknown") == "1":
|
|
59
|
+
version_cell = {"value": version, "color": "gray"}
|
|
60
|
+
if row.get("description_unknown") == "1":
|
|
61
|
+
desc_cell = {"value": desc, "color": "gray"}
|
|
62
|
+
return {
|
|
63
|
+
"file": row.get("file", ""),
|
|
64
|
+
"name": name_cell,
|
|
65
|
+
"version": version_cell,
|
|
66
|
+
"description": desc_cell,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _normalize_rule_row(row: Dict[str, str]) -> Dict[str, object]:
|
|
71
|
+
name = row.get("name", "")
|
|
72
|
+
desc = row.get("description", "")
|
|
73
|
+
category = row.get("category", "")
|
|
74
|
+
name_cell: object = name
|
|
75
|
+
desc_cell: object = desc
|
|
76
|
+
category_cell: object = category
|
|
77
|
+
if row.get("name_unknown") == "1":
|
|
78
|
+
name_cell = {"value": name, "color": "gray"}
|
|
79
|
+
if row.get("description_unknown") == "1":
|
|
80
|
+
desc_cell = {"value": desc, "color": "gray"}
|
|
81
|
+
if row.get("category_unknown") == "1":
|
|
82
|
+
category_cell = {"value": category, "color": "gray"}
|
|
83
|
+
return {
|
|
84
|
+
"file": row.get("file", ""),
|
|
85
|
+
"category": category_cell,
|
|
86
|
+
"name": name_cell,
|
|
87
|
+
"description": desc_cell,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _normalize_transform_row(row: Dict[str, str]) -> Dict[str, object]:
|
|
92
|
+
spec = row.get("spec", "")
|
|
93
|
+
spec_cell: object = spec
|
|
94
|
+
if row.get("spec_unknown") == "1":
|
|
95
|
+
spec_cell = {"value": spec, "color": "gray"}
|
|
96
|
+
return {
|
|
97
|
+
"file": row.get("file", ""),
|
|
98
|
+
"spec": spec_cell,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def cmd_add(args: argparse.Namespace) -> int:
|
|
103
|
+
installed = addon_app.add(args.filename, root=args.root)
|
|
104
|
+
logger.info("Installed %d file(s).", len(installed))
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cmd_list(args: argparse.Namespace) -> int:
|
|
109
|
+
data = addon_app.list_installed(root=args.root)
|
|
110
|
+
width = config_core.output_width(root=args.root)
|
|
111
|
+
rules = data["rules"]
|
|
112
|
+
pruner_specs = data.get("pruner_specs", [])
|
|
113
|
+
columns = ("file", "category", "name", "version", "description")
|
|
114
|
+
pruner_columns = ("file", "name", "version", "description")
|
|
115
|
+
rule_columns = ("file", "category", "name", "description")
|
|
116
|
+
transform_columns = ("file", "spec")
|
|
117
|
+
spec_rows = [_normalize_row(row) for row in data["specs"]]
|
|
118
|
+
pruner_rows = [_normalize_pruner_row(row) for row in pruner_specs]
|
|
119
|
+
rules_rows = [_normalize_rule_row(row) for row in rules]
|
|
120
|
+
transform_rows = [_normalize_transform_row(row) for row in data["transforms"]]
|
|
121
|
+
category_order = {"info_spec": 0, "metadata_spec": 1, "converter_hook": 2, "<Unknown>": 9}
|
|
122
|
+
spec_rows.sort(
|
|
123
|
+
key=lambda row: (
|
|
124
|
+
category_order.get(str(row.get("category", "")), 9),
|
|
125
|
+
str(row.get("name", "")),
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
pruner_rows.sort(
|
|
129
|
+
key=lambda row: (
|
|
130
|
+
str(row.get("name", "")),
|
|
131
|
+
str(row.get("version", "")),
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
spec_widths = formatter.compute_column_widths(columns, spec_rows)
|
|
135
|
+
col_widths = formatter.compute_column_widths(columns, spec_rows)
|
|
136
|
+
pruner_widths = formatter.compute_column_widths(pruner_columns, pruner_rows)
|
|
137
|
+
spec_table = formatter.format_table(
|
|
138
|
+
"Specs",
|
|
139
|
+
columns,
|
|
140
|
+
spec_rows,
|
|
141
|
+
width=width,
|
|
142
|
+
colors={"file": "gray", "name": "cyan", "description": "gray"},
|
|
143
|
+
title_color="cyan",
|
|
144
|
+
col_widths=col_widths,
|
|
145
|
+
)
|
|
146
|
+
rules_table = formatter.format_table(
|
|
147
|
+
"Rules",
|
|
148
|
+
rule_columns,
|
|
149
|
+
rules_rows,
|
|
150
|
+
width=width,
|
|
151
|
+
colors={"file": "gray", "name": "yellow", "description": "gray"},
|
|
152
|
+
title_color="yellow",
|
|
153
|
+
col_widths=formatter.compute_column_widths(rule_columns, rules_rows),
|
|
154
|
+
)
|
|
155
|
+
pruner_table = formatter.format_table(
|
|
156
|
+
"Pruner Specs",
|
|
157
|
+
pruner_columns,
|
|
158
|
+
pruner_rows,
|
|
159
|
+
width=width,
|
|
160
|
+
colors={"file": "gray", "name": "magenta", "description": "gray"},
|
|
161
|
+
title_color="magenta",
|
|
162
|
+
col_widths=pruner_widths,
|
|
163
|
+
min_last_col_width=40,
|
|
164
|
+
)
|
|
165
|
+
transforms_table = formatter.format_table(
|
|
166
|
+
"Transforms",
|
|
167
|
+
transform_columns,
|
|
168
|
+
transform_rows,
|
|
169
|
+
width=width,
|
|
170
|
+
colors={"file": "gray", "spec": "cyan"},
|
|
171
|
+
title_color="cyan",
|
|
172
|
+
)
|
|
173
|
+
logger.info("%s", rules_table)
|
|
174
|
+
logger.info("")
|
|
175
|
+
logger.info("%s", spec_table)
|
|
176
|
+
if pruner_rows:
|
|
177
|
+
logger.info("")
|
|
178
|
+
logger.info("%s", pruner_table)
|
|
179
|
+
if transform_rows:
|
|
180
|
+
logger.info("")
|
|
181
|
+
logger.info("%s", transforms_table)
|
|
182
|
+
return 0
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def cmd_rm(args: argparse.Namespace) -> int:
|
|
186
|
+
try:
|
|
187
|
+
removed = addon_app.remove(
|
|
188
|
+
args.filename,
|
|
189
|
+
root=args.root,
|
|
190
|
+
kind=args.kind,
|
|
191
|
+
force=args.force,
|
|
192
|
+
)
|
|
193
|
+
except FileNotFoundError as exc:
|
|
194
|
+
logger.error("No matching addon files found: %s", exc)
|
|
195
|
+
return 2
|
|
196
|
+
except RuntimeError as exc:
|
|
197
|
+
logger.error("%s", exc)
|
|
198
|
+
return 2
|
|
199
|
+
logger.info("Removed %d file(s).", len(removed))
|
|
200
|
+
return 0
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def cmd_edit(args: argparse.Namespace) -> int:
|
|
206
|
+
editor = config_core.resolve_editor_binary(root=args.root)
|
|
207
|
+
if not editor:
|
|
208
|
+
logger.error("No editor configured. Set editor or $EDITOR.")
|
|
209
|
+
return 2
|
|
210
|
+
paths = config_core.paths(root=args.root)
|
|
211
|
+
target = _resolve_edit_target(
|
|
212
|
+
args.target,
|
|
213
|
+
kind=args.kind,
|
|
214
|
+
category=args.category,
|
|
215
|
+
root=args.root,
|
|
216
|
+
paths=paths,
|
|
217
|
+
)
|
|
218
|
+
cmd = shlex.split(editor) + [str(target)]
|
|
219
|
+
return subprocess.call(cmd)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _resolve_edit_target(
|
|
223
|
+
target: str,
|
|
224
|
+
*,
|
|
225
|
+
kind: Optional[str],
|
|
226
|
+
category: Optional[str],
|
|
227
|
+
root: Optional[str],
|
|
228
|
+
paths: config_core.ConfigPaths,
|
|
229
|
+
) -> Path:
|
|
230
|
+
candidate = Path(target).expanduser()
|
|
231
|
+
if candidate.exists():
|
|
232
|
+
return candidate.resolve()
|
|
233
|
+
if kind == "spec":
|
|
234
|
+
return addon_app.resolve_spec_reference(target, category=category, root=root)
|
|
235
|
+
if kind == "pruner":
|
|
236
|
+
return addon_app.resolve_pruner_spec_reference(target, root=root)
|
|
237
|
+
if kind == "rule":
|
|
238
|
+
return _resolve_rule_target(target, category=category, rules_dir=paths.rules_dir)
|
|
239
|
+
if kind == "transform":
|
|
240
|
+
return (paths.transforms_dir / target).resolve()
|
|
241
|
+
transform_candidate = (paths.transforms_dir / target).resolve()
|
|
242
|
+
if transform_candidate.exists():
|
|
243
|
+
return transform_candidate
|
|
244
|
+
pruner_candidate = (paths.pruner_specs_dir / target).resolve()
|
|
245
|
+
if pruner_candidate.exists():
|
|
246
|
+
return pruner_candidate
|
|
247
|
+
try:
|
|
248
|
+
return addon_app.resolve_spec_reference(target, category=category, root=root)
|
|
249
|
+
except Exception:
|
|
250
|
+
try:
|
|
251
|
+
return addon_app.resolve_pruner_spec_reference(target, root=root)
|
|
252
|
+
except Exception:
|
|
253
|
+
return _resolve_rule_target(target, category=category, rules_dir=paths.rules_dir)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _resolve_spec_path(target: str, *, category: Optional[str], root: Optional[str]) -> Path:
|
|
257
|
+
return addon_app.resolve_spec_reference(target, category=category, root=root)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _resolve_rule_target(target: str, *, category: Optional[str], rules_dir: Path) -> Path:
|
|
261
|
+
candidate = (rules_dir / target).resolve()
|
|
262
|
+
if candidate.exists():
|
|
263
|
+
return candidate
|
|
264
|
+
rules = addon_app.list_installed(root=str(rules_dir.parent)).get("rules", [])
|
|
265
|
+
matches = [
|
|
266
|
+
entry for entry in rules
|
|
267
|
+
if entry.get("name") == target and (category is None or entry.get("category") == category)
|
|
268
|
+
]
|
|
269
|
+
file_set = {
|
|
270
|
+
file
|
|
271
|
+
for entry in matches
|
|
272
|
+
if isinstance((file := entry.get("file")), str) and file
|
|
273
|
+
}
|
|
274
|
+
files = sorted(file_set)
|
|
275
|
+
if len(files) == 1:
|
|
276
|
+
return (rules_dir / files[0]).resolve()
|
|
277
|
+
if not files:
|
|
278
|
+
raise FileNotFoundError(target)
|
|
279
|
+
raise ValueError(f"Multiple rule files match {target}: {', '.join(files)}")
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def register(subparsers: argparse._SubParsersAction) -> None: # type: ignore[name-defined]
|
|
283
|
+
addon_parser = subparsers.add_parser(
|
|
284
|
+
"addon",
|
|
285
|
+
help="Manage info specs and rules.",
|
|
286
|
+
)
|
|
287
|
+
addon_parser.add_argument(
|
|
288
|
+
"--root",
|
|
289
|
+
help="Override config root directory (default: BRKRAW_CONFIG_HOME or ~/.brkraw).",
|
|
290
|
+
)
|
|
291
|
+
addon_parser.set_defaults(func=cmd_addon, parser=addon_parser)
|
|
292
|
+
addon_sub = addon_parser.add_subparsers(dest="addon_command")
|
|
293
|
+
|
|
294
|
+
add_parser = addon_sub.add_parser("add", help="Install a spec or rule file.")
|
|
295
|
+
add_parser.add_argument("filename", help="Spec/rule YAML.")
|
|
296
|
+
add_parser.set_defaults(addon_func=cmd_add)
|
|
297
|
+
|
|
298
|
+
list_parser = addon_sub.add_parser("list", help="List installed specs and rules.")
|
|
299
|
+
list_parser.set_defaults(addon_func=cmd_list)
|
|
300
|
+
|
|
301
|
+
rm_parser = addon_sub.add_parser("rm", help="Remove an installed spec or rule file.")
|
|
302
|
+
rm_parser.add_argument("filename", help="Spec/rule filename to remove.")
|
|
303
|
+
rm_parser.add_argument(
|
|
304
|
+
"--kind",
|
|
305
|
+
choices=["spec", "pruner", "rule", "transform"],
|
|
306
|
+
help="Limit removal to a specific kind.",
|
|
307
|
+
)
|
|
308
|
+
rm_parser.add_argument(
|
|
309
|
+
"--force",
|
|
310
|
+
action="store_true",
|
|
311
|
+
help="Remove even if dependencies are detected.",
|
|
312
|
+
)
|
|
313
|
+
rm_parser.set_defaults(addon_func=cmd_rm)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
edit_parser = addon_sub.add_parser("edit", help="Edit an installed spec or rule.")
|
|
317
|
+
edit_parser.add_argument("target", help="Spec/rule name or filename.")
|
|
318
|
+
edit_parser.add_argument(
|
|
319
|
+
"--kind",
|
|
320
|
+
choices=["spec", "pruner", "rule", "transform"],
|
|
321
|
+
help="Target kind (default: auto-detect).",
|
|
322
|
+
)
|
|
323
|
+
edit_parser.add_argument(
|
|
324
|
+
"--category",
|
|
325
|
+
help="Spec/rule category hint (e.g. info_spec, metadata_spec).",
|
|
326
|
+
)
|
|
327
|
+
edit_parser.set_defaults(addon_func=cmd_edit)
|