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/core/layout.py
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any, Dict, Iterable, Mapping, Optional, Union, List, Sequence, Tuple
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from ..apps.addon.core import resolve_spec_reference
|
|
10
|
+
from ..apps.loader import info as info_resolver
|
|
11
|
+
from ..specs.remapper import (
|
|
12
|
+
load_spec,
|
|
13
|
+
map_parameters,
|
|
14
|
+
load_context_map,
|
|
15
|
+
load_context_map_meta,
|
|
16
|
+
apply_context_map,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
_ENTRY_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_-]*$")
|
|
20
|
+
_DEFAULT_VALUE_PATTERN = r"[A-Za-z0-9._-]"
|
|
21
|
+
_SLICEPACK_TAG = re.compile(r"\{([^}]+)\}")
|
|
22
|
+
_LAYOUT_TAG = re.compile(r"\{([^}]+)\}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def render_layout(
|
|
26
|
+
loader: Any,
|
|
27
|
+
scan_id: int,
|
|
28
|
+
*,
|
|
29
|
+
layout_entries: Optional[Iterable[Mapping[str, Any]]] = None,
|
|
30
|
+
layout_template: Optional[str] = None,
|
|
31
|
+
context_map: Optional[Union[str, Path]] = None,
|
|
32
|
+
root: Optional[Union[str, Path]] = None,
|
|
33
|
+
reco_id: Optional[int] = None,
|
|
34
|
+
counter: Optional[int] = None,
|
|
35
|
+
override_info_spec: Optional[Union[str, Path]] = None,
|
|
36
|
+
override_metadata_spec: Optional[Union[str, Path]] = None,
|
|
37
|
+
) -> str:
|
|
38
|
+
if layout_entries is None and layout_template is None and context_map:
|
|
39
|
+
meta = load_layout_meta(context_map)
|
|
40
|
+
layout_entries = meta.get("layout_entries")
|
|
41
|
+
if layout_entries is None:
|
|
42
|
+
layout_entries = meta.get("layout_fields")
|
|
43
|
+
layout_template = meta.get("layout_template")
|
|
44
|
+
info = load_layout_info(
|
|
45
|
+
loader,
|
|
46
|
+
scan_id,
|
|
47
|
+
context_map=context_map,
|
|
48
|
+
root=root,
|
|
49
|
+
reco_id=reco_id,
|
|
50
|
+
override_info_spec=override_info_spec,
|
|
51
|
+
override_metadata_spec=override_metadata_spec,
|
|
52
|
+
)
|
|
53
|
+
if isinstance(layout_template, str) and layout_template:
|
|
54
|
+
return _render_layout_template(layout_template, info, scan_id, reco_id=reco_id, counter=counter)
|
|
55
|
+
return _render_fields(layout_entries, info, scan_id, reco_id=reco_id, counter=counter)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_layout_info(
|
|
59
|
+
loader: Any,
|
|
60
|
+
scan_id: int,
|
|
61
|
+
*,
|
|
62
|
+
context_map: Optional[Union[str, Path]],
|
|
63
|
+
root: Optional[Union[str, Path]] = None,
|
|
64
|
+
reco_id: Optional[int],
|
|
65
|
+
override_info_spec: Optional[Union[str, Path]] = None,
|
|
66
|
+
override_metadata_spec: Optional[Union[str, Path]] = None,
|
|
67
|
+
) -> Dict[str, Any]:
|
|
68
|
+
info, metadata = load_layout_info_parts(
|
|
69
|
+
loader,
|
|
70
|
+
scan_id,
|
|
71
|
+
context_map=context_map,
|
|
72
|
+
root=root,
|
|
73
|
+
reco_id=reco_id,
|
|
74
|
+
override_info_spec=override_info_spec,
|
|
75
|
+
override_metadata_spec=override_metadata_spec,
|
|
76
|
+
)
|
|
77
|
+
merged = dict(info)
|
|
78
|
+
merged.update(metadata)
|
|
79
|
+
return merged
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def load_layout_info_parts(
|
|
83
|
+
loader: Any,
|
|
84
|
+
scan_id: int,
|
|
85
|
+
*,
|
|
86
|
+
context_map: Optional[Union[str, Path]],
|
|
87
|
+
root: Optional[Union[str, Path]] = None,
|
|
88
|
+
reco_id: Optional[int],
|
|
89
|
+
override_info_spec: Optional[Union[str, Path]] = None,
|
|
90
|
+
override_metadata_spec: Optional[Union[str, Path]] = None,
|
|
91
|
+
) -> Tuple[Dict[str, Any], Dict[str, Any]]:
|
|
92
|
+
map_data = None
|
|
93
|
+
if context_map:
|
|
94
|
+
map_data = load_context_map(context_map)
|
|
95
|
+
scan = loader.get_scan(scan_id)
|
|
96
|
+
if override_info_spec:
|
|
97
|
+
spec_path = resolve_spec_reference(
|
|
98
|
+
str(override_info_spec),
|
|
99
|
+
category="info_spec",
|
|
100
|
+
root=root,
|
|
101
|
+
)
|
|
102
|
+
spec_data, transforms = load_spec(spec_path, validate=False)
|
|
103
|
+
params_map = _build_params_map(loader, scan, reco_id=reco_id)
|
|
104
|
+
mapped = map_parameters(
|
|
105
|
+
params_map,
|
|
106
|
+
spec_data,
|
|
107
|
+
transforms,
|
|
108
|
+
validate=False,
|
|
109
|
+
context_map=None,
|
|
110
|
+
context={"scan_id": scan_id, "reco_id": reco_id},
|
|
111
|
+
)
|
|
112
|
+
if not isinstance(mapped, dict):
|
|
113
|
+
raise ValueError("override_info_spec must resolve to a mapping.")
|
|
114
|
+
info = mapped
|
|
115
|
+
else:
|
|
116
|
+
study_info = info_resolver.study(loader) or {}
|
|
117
|
+
scan_info = info_resolver.scan(scan) or {}
|
|
118
|
+
if isinstance(study_info, dict):
|
|
119
|
+
scan_info = dict(scan_info)
|
|
120
|
+
if "Study" in study_info:
|
|
121
|
+
scan_info["Study"] = study_info["Study"]
|
|
122
|
+
if "Subject" in study_info:
|
|
123
|
+
scan_info["Subject"] = study_info["Subject"]
|
|
124
|
+
info = scan_info if isinstance(scan_info, dict) else {}
|
|
125
|
+
if map_data:
|
|
126
|
+
info = apply_context_map(
|
|
127
|
+
info,
|
|
128
|
+
map_data,
|
|
129
|
+
target="info_spec",
|
|
130
|
+
context={"scan_id": scan_id, "reco_id": reco_id},
|
|
131
|
+
)
|
|
132
|
+
metadata: Dict[str, Any] = {}
|
|
133
|
+
if map_data or override_metadata_spec:
|
|
134
|
+
meta = loader.get_metadata(
|
|
135
|
+
scan_id,
|
|
136
|
+
reco_id=reco_id,
|
|
137
|
+
context_map=context_map,
|
|
138
|
+
spec=override_metadata_spec,
|
|
139
|
+
)
|
|
140
|
+
if isinstance(meta, dict):
|
|
141
|
+
metadata = meta
|
|
142
|
+
return info, metadata
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def load_layout_meta(context_map: Optional[Union[str, Path]]) -> Dict[str, Any]:
|
|
146
|
+
if not context_map:
|
|
147
|
+
return {}
|
|
148
|
+
return load_context_map_meta(context_map)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def render_slicepack_suffixes(
|
|
152
|
+
info: Mapping[str, Any],
|
|
153
|
+
*,
|
|
154
|
+
count: int,
|
|
155
|
+
template: str = "_slpack{index}",
|
|
156
|
+
counter: Optional[int] = None,
|
|
157
|
+
) -> List[str]:
|
|
158
|
+
suffixes: List[str] = []
|
|
159
|
+
for idx in range(count):
|
|
160
|
+
suffixes.append(_render_slicepack_suffix(template, info, idx, counter=counter))
|
|
161
|
+
return suffixes
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _render_slicepack_suffix(
|
|
165
|
+
template: str,
|
|
166
|
+
info: Mapping[str, Any],
|
|
167
|
+
idx: int,
|
|
168
|
+
*,
|
|
169
|
+
counter: Optional[int],
|
|
170
|
+
) -> str:
|
|
171
|
+
def _replace(match: re.Match[str]) -> str:
|
|
172
|
+
tag = match.group(1)
|
|
173
|
+
if tag.lower() == "index":
|
|
174
|
+
return str(idx + 1)
|
|
175
|
+
value = _resolve_tag(tag, info, idx + 1, reco_id=None, counter=counter)
|
|
176
|
+
chosen = _select_indexed_value(value, idx)
|
|
177
|
+
if chosen is None:
|
|
178
|
+
return str(idx + 1)
|
|
179
|
+
rendered = _format_value_with_options(
|
|
180
|
+
chosen,
|
|
181
|
+
value_pattern=None,
|
|
182
|
+
value_replace="",
|
|
183
|
+
max_length=None,
|
|
184
|
+
)
|
|
185
|
+
return rendered or str(idx + 1)
|
|
186
|
+
|
|
187
|
+
return _SLICEPACK_TAG.sub(_replace, template)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _select_indexed_value(value: Any, idx: int) -> Any:
|
|
191
|
+
if value is None:
|
|
192
|
+
return None
|
|
193
|
+
if isinstance(value, np.ndarray):
|
|
194
|
+
items = value.tolist()
|
|
195
|
+
return items[idx] if idx < len(items) else None
|
|
196
|
+
if isinstance(value, (list, tuple)):
|
|
197
|
+
return value[idx] if idx < len(value) else None
|
|
198
|
+
return value
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _build_params_map(loader: Any, scan: Any, *, reco_id: Optional[int]) -> Dict[str, Any]:
|
|
202
|
+
params_map: Dict[str, Any] = {
|
|
203
|
+
"method": getattr(scan, "method", None),
|
|
204
|
+
"acqp": getattr(scan, "acqp", None),
|
|
205
|
+
}
|
|
206
|
+
study = getattr(loader, "_study", None)
|
|
207
|
+
subject = getattr(study, "subject", None)
|
|
208
|
+
if subject is not None:
|
|
209
|
+
params_map["subject"] = subject
|
|
210
|
+
|
|
211
|
+
if reco_id is None:
|
|
212
|
+
reco_ids = list(getattr(scan, "avail", {}).keys())
|
|
213
|
+
reco_id = reco_ids[0] if reco_ids else None
|
|
214
|
+
if reco_id is not None:
|
|
215
|
+
reco = scan.get_reco(reco_id)
|
|
216
|
+
params_map["visu_pars"] = {reco_id: reco.visu_pars}
|
|
217
|
+
params_map["reco"] = {reco_id: reco.reco}
|
|
218
|
+
return params_map
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _render_fields(
|
|
222
|
+
fields: Optional[Iterable[Mapping[str, Any]]],
|
|
223
|
+
info: Mapping[str, Any],
|
|
224
|
+
scan_id: int,
|
|
225
|
+
*,
|
|
226
|
+
reco_id: Optional[int],
|
|
227
|
+
counter: Optional[int],
|
|
228
|
+
) -> str:
|
|
229
|
+
parts: List[str] = []
|
|
230
|
+
seps: List[Optional[str]] = []
|
|
231
|
+
entry_values: Dict[str, Any] = {}
|
|
232
|
+
|
|
233
|
+
for field in fields or []:
|
|
234
|
+
if not isinstance(field, Mapping):
|
|
235
|
+
continue
|
|
236
|
+
key = field.get("key")
|
|
237
|
+
use_entry = field.get("use_entry")
|
|
238
|
+
hide = bool(field.get("hide"))
|
|
239
|
+
entry = field.get("entry")
|
|
240
|
+
sep = field.get("sep")
|
|
241
|
+
value_pattern = field.get("value_pattern")
|
|
242
|
+
value_replace = field.get("value_replace", "")
|
|
243
|
+
max_length = field.get("max_length")
|
|
244
|
+
|
|
245
|
+
if key is not None and use_entry is not None:
|
|
246
|
+
continue
|
|
247
|
+
if key is None and use_entry is None:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
value_str: Optional[str] = None
|
|
251
|
+
entry_clean: Optional[str] = None
|
|
252
|
+
|
|
253
|
+
if key is not None:
|
|
254
|
+
if not isinstance(key, str) or not key.strip():
|
|
255
|
+
continue
|
|
256
|
+
if isinstance(entry, str) and entry.strip():
|
|
257
|
+
entry_clean = entry.strip()
|
|
258
|
+
if not _ENTRY_PATTERN.match(entry_clean):
|
|
259
|
+
continue
|
|
260
|
+
elif not hide:
|
|
261
|
+
entry_clean = key.replace(".", "").lower()
|
|
262
|
+
if not _ENTRY_PATTERN.match(entry_clean):
|
|
263
|
+
continue
|
|
264
|
+
value = _resolve_tag(key, info, scan_id, reco_id=reco_id, counter=counter)
|
|
265
|
+
value_str = _format_value_with_options(
|
|
266
|
+
value,
|
|
267
|
+
value_pattern=value_pattern,
|
|
268
|
+
value_replace=value_replace,
|
|
269
|
+
max_length=max_length,
|
|
270
|
+
)
|
|
271
|
+
if value_str is None:
|
|
272
|
+
continue
|
|
273
|
+
if entry_clean:
|
|
274
|
+
entry_values[entry_clean] = value
|
|
275
|
+
else:
|
|
276
|
+
if not isinstance(use_entry, str) or not use_entry.strip():
|
|
277
|
+
continue
|
|
278
|
+
entry_clean = use_entry.strip()
|
|
279
|
+
if not _ENTRY_PATTERN.match(entry_clean):
|
|
280
|
+
continue
|
|
281
|
+
raw_value = entry_values.get(entry_clean)
|
|
282
|
+
if raw_value is None:
|
|
283
|
+
continue
|
|
284
|
+
value_str = _format_value_with_options(
|
|
285
|
+
raw_value,
|
|
286
|
+
value_pattern=value_pattern,
|
|
287
|
+
value_replace=value_replace,
|
|
288
|
+
max_length=max_length,
|
|
289
|
+
)
|
|
290
|
+
if value_str is None:
|
|
291
|
+
continue
|
|
292
|
+
|
|
293
|
+
if hide:
|
|
294
|
+
parts.append(value_str)
|
|
295
|
+
else:
|
|
296
|
+
parts.append(f"{entry_clean}-{value_str}")
|
|
297
|
+
if isinstance(sep, str) and sep:
|
|
298
|
+
seps.append(sep)
|
|
299
|
+
else:
|
|
300
|
+
seps.append(None)
|
|
301
|
+
|
|
302
|
+
if not parts:
|
|
303
|
+
return f"scan-{scan_id}"
|
|
304
|
+
result = parts[0]
|
|
305
|
+
for idx in range(1, len(parts)):
|
|
306
|
+
joiner = seps[idx - 1] if seps[idx - 1] is not None else "_"
|
|
307
|
+
result = f"{result}{joiner}{parts[idx]}"
|
|
308
|
+
return result
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _render_layout_template(
|
|
312
|
+
template: str,
|
|
313
|
+
info: Mapping[str, Any],
|
|
314
|
+
scan_id: int,
|
|
315
|
+
*,
|
|
316
|
+
reco_id: Optional[int],
|
|
317
|
+
counter: Optional[int],
|
|
318
|
+
) -> str:
|
|
319
|
+
if not _LAYOUT_TAG.search(template):
|
|
320
|
+
return template
|
|
321
|
+
rendered = _LAYOUT_TAG.sub(
|
|
322
|
+
lambda m: _resolve_layout_tag(m, info, scan_id, reco_id=reco_id, counter=counter),
|
|
323
|
+
template,
|
|
324
|
+
)
|
|
325
|
+
return rendered or template
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _resolve_layout_tag(
|
|
329
|
+
match: re.Match[str],
|
|
330
|
+
info: Mapping[str, Any],
|
|
331
|
+
scan_id: int,
|
|
332
|
+
*,
|
|
333
|
+
reco_id: Optional[int],
|
|
334
|
+
counter: Optional[int],
|
|
335
|
+
) -> str:
|
|
336
|
+
tag = match.group(1) or ""
|
|
337
|
+
if not tag:
|
|
338
|
+
return ""
|
|
339
|
+
value = _resolve_tag(tag.strip(), info, scan_id, reco_id=reco_id, counter=counter)
|
|
340
|
+
rendered = _format_value(value)
|
|
341
|
+
return rendered or ""
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _resolve_tag(
|
|
345
|
+
tag: str,
|
|
346
|
+
info: Mapping[str, Any],
|
|
347
|
+
scan_id: int,
|
|
348
|
+
*,
|
|
349
|
+
reco_id: Optional[int] = None,
|
|
350
|
+
counter: Optional[int] = None,
|
|
351
|
+
) -> Any:
|
|
352
|
+
if tag in {"ScanID", "scan_id", "scanid"}:
|
|
353
|
+
return scan_id
|
|
354
|
+
if tag in {"RecoID", "reco_id", "recoid"}:
|
|
355
|
+
return reco_id
|
|
356
|
+
if tag in {"Counter", "counter"}:
|
|
357
|
+
return counter
|
|
358
|
+
if "." in tag:
|
|
359
|
+
root_key, rest = tag.split(".", 1)
|
|
360
|
+
root_val = info.get(root_key)
|
|
361
|
+
if isinstance(root_val, Mapping):
|
|
362
|
+
return _resolve_nested(root_val, rest)
|
|
363
|
+
return None
|
|
364
|
+
return info.get(tag)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _format_value(value: Any) -> Optional[str]:
|
|
368
|
+
return _format_value_with_options(
|
|
369
|
+
value,
|
|
370
|
+
value_pattern=None,
|
|
371
|
+
value_replace="",
|
|
372
|
+
max_length=None,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _format_value_with_options(
|
|
377
|
+
value: Any,
|
|
378
|
+
*,
|
|
379
|
+
value_pattern: Optional[str],
|
|
380
|
+
value_replace: Optional[str],
|
|
381
|
+
max_length: Optional[Any],
|
|
382
|
+
) -> Optional[str]:
|
|
383
|
+
if value is None:
|
|
384
|
+
return None
|
|
385
|
+
if isinstance(value, Mapping):
|
|
386
|
+
parts = []
|
|
387
|
+
for k, v in value.items():
|
|
388
|
+
k_str = _sanitize_value(k, value_pattern, value_replace, None)
|
|
389
|
+
v_str = _format_value_with_options(
|
|
390
|
+
v,
|
|
391
|
+
value_pattern=value_pattern,
|
|
392
|
+
value_replace=value_replace,
|
|
393
|
+
max_length=None,
|
|
394
|
+
)
|
|
395
|
+
if k_str and v_str:
|
|
396
|
+
parts.append(f"{k_str}-{v_str}")
|
|
397
|
+
raw = "-".join(parts)
|
|
398
|
+
elif isinstance(value, np.ndarray):
|
|
399
|
+
raw = "-".join(str(v) for v in value.tolist())
|
|
400
|
+
elif isinstance(value, (list, tuple)):
|
|
401
|
+
items = [
|
|
402
|
+
v for v in (
|
|
403
|
+
_format_value_with_options(
|
|
404
|
+
v,
|
|
405
|
+
value_pattern=value_pattern,
|
|
406
|
+
value_replace=value_replace,
|
|
407
|
+
max_length=None,
|
|
408
|
+
)
|
|
409
|
+
for v in value
|
|
410
|
+
)
|
|
411
|
+
if v
|
|
412
|
+
]
|
|
413
|
+
raw = "-".join(items)
|
|
414
|
+
else:
|
|
415
|
+
raw = str(value).strip()
|
|
416
|
+
if not raw:
|
|
417
|
+
return None
|
|
418
|
+
cleaned = _sanitize_value(raw, value_pattern, value_replace, max_length)
|
|
419
|
+
return cleaned or None
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _sanitize_value(
|
|
423
|
+
raw: Any,
|
|
424
|
+
value_pattern: Optional[str],
|
|
425
|
+
value_replace: Optional[str],
|
|
426
|
+
max_length: Optional[Any],
|
|
427
|
+
) -> Optional[str]:
|
|
428
|
+
text = str(raw).strip()
|
|
429
|
+
if not text:
|
|
430
|
+
return None
|
|
431
|
+
pattern = value_pattern or _DEFAULT_VALUE_PATTERN
|
|
432
|
+
try:
|
|
433
|
+
regex = re.compile(pattern)
|
|
434
|
+
except re.error as exc:
|
|
435
|
+
raise ValueError(f"Invalid value_pattern: {pattern!r}") from exc
|
|
436
|
+
repl = "" if value_replace is None else str(value_replace)
|
|
437
|
+
cleaned = "".join(
|
|
438
|
+
ch if regex.fullmatch(ch) else repl for ch in text
|
|
439
|
+
)
|
|
440
|
+
if isinstance(max_length, int) and max_length > 0:
|
|
441
|
+
cleaned = cleaned[:max_length]
|
|
442
|
+
return cleaned or None
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _resolve_nested(value: Mapping[str, Any], dotted: str) -> Any:
|
|
446
|
+
current: Any = value
|
|
447
|
+
for part in dotted.split("."):
|
|
448
|
+
if not isinstance(current, Mapping):
|
|
449
|
+
return None
|
|
450
|
+
current = current.get(part)
|
|
451
|
+
return current
|