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.
Files changed (113) hide show
  1. brkraw/__init__.py +9 -3
  2. brkraw/apps/__init__.py +12 -0
  3. brkraw/apps/addon/__init__.py +30 -0
  4. brkraw/apps/addon/core.py +35 -0
  5. brkraw/apps/addon/dependencies.py +402 -0
  6. brkraw/apps/addon/installation.py +500 -0
  7. brkraw/apps/addon/io.py +21 -0
  8. brkraw/apps/hook/__init__.py +25 -0
  9. brkraw/apps/hook/core.py +636 -0
  10. brkraw/apps/loader/__init__.py +10 -0
  11. brkraw/apps/loader/core.py +622 -0
  12. brkraw/apps/loader/formatter.py +288 -0
  13. brkraw/apps/loader/helper.py +797 -0
  14. brkraw/apps/loader/info/__init__.py +11 -0
  15. brkraw/apps/loader/info/scan.py +85 -0
  16. brkraw/apps/loader/info/scan.yaml +90 -0
  17. brkraw/apps/loader/info/study.py +69 -0
  18. brkraw/apps/loader/info/study.yaml +156 -0
  19. brkraw/apps/loader/info/transform.py +92 -0
  20. brkraw/apps/loader/types.py +220 -0
  21. brkraw/cli/__init__.py +5 -0
  22. brkraw/cli/commands/__init__.py +2 -0
  23. brkraw/cli/commands/addon.py +327 -0
  24. brkraw/cli/commands/config.py +205 -0
  25. brkraw/cli/commands/convert.py +903 -0
  26. brkraw/cli/commands/hook.py +348 -0
  27. brkraw/cli/commands/info.py +74 -0
  28. brkraw/cli/commands/init.py +214 -0
  29. brkraw/cli/commands/params.py +106 -0
  30. brkraw/cli/commands/prune.py +288 -0
  31. brkraw/cli/commands/session.py +371 -0
  32. brkraw/cli/hook_args.py +80 -0
  33. brkraw/cli/main.py +83 -0
  34. brkraw/cli/utils.py +60 -0
  35. brkraw/core/__init__.py +13 -0
  36. brkraw/core/config.py +380 -0
  37. brkraw/core/entrypoints.py +25 -0
  38. brkraw/core/formatter.py +367 -0
  39. brkraw/core/fs.py +495 -0
  40. brkraw/core/jcamp.py +600 -0
  41. brkraw/core/layout.py +451 -0
  42. brkraw/core/parameters.py +781 -0
  43. brkraw/core/zip.py +1121 -0
  44. brkraw/dataclasses/__init__.py +14 -0
  45. brkraw/dataclasses/node.py +139 -0
  46. brkraw/dataclasses/reco.py +33 -0
  47. brkraw/dataclasses/scan.py +61 -0
  48. brkraw/dataclasses/study.py +131 -0
  49. brkraw/default/__init__.py +3 -0
  50. brkraw/default/pruner_specs/deid4share.yaml +42 -0
  51. brkraw/default/rules/00_default.yaml +4 -0
  52. brkraw/default/specs/metadata_dicom.yaml +236 -0
  53. brkraw/default/specs/metadata_transforms.py +92 -0
  54. brkraw/resolver/__init__.py +7 -0
  55. brkraw/resolver/affine.py +539 -0
  56. brkraw/resolver/datatype.py +69 -0
  57. brkraw/resolver/fid.py +90 -0
  58. brkraw/resolver/helpers.py +36 -0
  59. brkraw/resolver/image.py +188 -0
  60. brkraw/resolver/nifti.py +370 -0
  61. brkraw/resolver/shape.py +235 -0
  62. brkraw/schema/__init__.py +3 -0
  63. brkraw/schema/context_map.yaml +62 -0
  64. brkraw/schema/meta.yaml +57 -0
  65. brkraw/schema/niftiheader.yaml +95 -0
  66. brkraw/schema/pruner.yaml +55 -0
  67. brkraw/schema/remapper.yaml +128 -0
  68. brkraw/schema/rules.yaml +154 -0
  69. brkraw/specs/__init__.py +10 -0
  70. brkraw/specs/hook/__init__.py +12 -0
  71. brkraw/specs/hook/logic.py +31 -0
  72. brkraw/specs/hook/validator.py +22 -0
  73. brkraw/specs/meta/__init__.py +5 -0
  74. brkraw/specs/meta/validator.py +156 -0
  75. brkraw/specs/pruner/__init__.py +15 -0
  76. brkraw/specs/pruner/logic.py +361 -0
  77. brkraw/specs/pruner/validator.py +119 -0
  78. brkraw/specs/remapper/__init__.py +27 -0
  79. brkraw/specs/remapper/logic.py +924 -0
  80. brkraw/specs/remapper/validator.py +314 -0
  81. brkraw/specs/rules/__init__.py +6 -0
  82. brkraw/specs/rules/logic.py +263 -0
  83. brkraw/specs/rules/validator.py +103 -0
  84. brkraw-0.5.0.dist-info/METADATA +81 -0
  85. brkraw-0.5.0.dist-info/RECORD +88 -0
  86. {brkraw-0.3.11.dist-info → brkraw-0.5.0.dist-info}/WHEEL +1 -2
  87. brkraw-0.5.0.dist-info/entry_points.txt +13 -0
  88. brkraw/lib/__init__.py +0 -4
  89. brkraw/lib/backup.py +0 -641
  90. brkraw/lib/bids.py +0 -0
  91. brkraw/lib/errors.py +0 -125
  92. brkraw/lib/loader.py +0 -1220
  93. brkraw/lib/orient.py +0 -194
  94. brkraw/lib/parser.py +0 -48
  95. brkraw/lib/pvobj.py +0 -301
  96. brkraw/lib/reference.py +0 -245
  97. brkraw/lib/utils.py +0 -471
  98. brkraw/scripts/__init__.py +0 -0
  99. brkraw/scripts/brk_backup.py +0 -106
  100. brkraw/scripts/brkraw.py +0 -744
  101. brkraw/ui/__init__.py +0 -0
  102. brkraw/ui/config.py +0 -17
  103. brkraw/ui/main_win.py +0 -214
  104. brkraw/ui/previewer.py +0 -225
  105. brkraw/ui/scan_info.py +0 -72
  106. brkraw/ui/scan_list.py +0 -73
  107. brkraw/ui/subj_info.py +0 -128
  108. brkraw-0.3.11.dist-info/METADATA +0 -25
  109. brkraw-0.3.11.dist-info/RECORD +0 -28
  110. brkraw-0.3.11.dist-info/entry_points.txt +0 -3
  111. brkraw-0.3.11.dist-info/top_level.txt +0 -2
  112. tests/__init__.py +0 -0
  113. {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 .lib import *
1
+ from __future__ import annotations
2
2
 
3
- __version__ = '0.3.11'
4
- __all__ = ['BrukerLoader', '__version__']
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
+ ]
@@ -0,0 +1,12 @@
1
+ """Application helpers for BrkRaw.
2
+
3
+ Last updated: 2025-12-30
4
+ """
5
+ from __future__ import annotations
6
+
7
+
8
+ from .loader import BrukerLoader
9
+
10
+ __all__ = [
11
+ "BrukerLoader",
12
+ ]
@@ -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
+ ]