RSRF 0.0.1__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.
rsrf/__init__.py ADDED
@@ -0,0 +1,88 @@
1
+ """Bootstrap package for the spectral response function repository."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import PackageNotFoundError, version
6
+ from pathlib import Path
7
+
8
+ from .api import get_metadata, list_bands, list_sensors, load_band_spec, load_curve, load_response_definition
9
+ from .manifests import iter_source_manifest_paths, manifest_path, resolve_manifest_path
10
+ from .planning import list_planned_sensors, register_planned_sensor_catalog
11
+ from .qa import validate_sensor, write_validation_artifacts
12
+ from .realize import realize_curve
13
+ from .visualization import export_docs_visualization_assets
14
+
15
+ PACKAGE_ROOT = Path(__file__).resolve().parent
16
+
17
+
18
+ def _metadata_version_from_path(metadata_path: Path) -> str | None:
19
+ if not metadata_path.exists():
20
+ return None
21
+ for line in metadata_path.read_text(encoding="utf-8").splitlines():
22
+ if line.startswith("Version: "):
23
+ return line.split("Version: ", 1)[1].strip()
24
+ return None
25
+
26
+
27
+ def _pyproject_version(pyproject_path: Path) -> str | None:
28
+ if not pyproject_path.exists():
29
+ return None
30
+
31
+ in_project_block = False
32
+ for raw_line in pyproject_path.read_text(encoding="utf-8").splitlines():
33
+ line = raw_line.strip()
34
+ if line.startswith("["):
35
+ in_project_block = line == "[project]"
36
+ continue
37
+ if in_project_block and line.startswith("version = "):
38
+ version_literal = line.split("=", 1)[1].strip()
39
+ if version_literal.startswith('"') and version_literal.endswith('"'):
40
+ return version_literal[1:-1]
41
+ return None
42
+
43
+
44
+ def _local_distribution_version() -> str | None:
45
+ repo_version = _pyproject_version(PACKAGE_ROOT.parent.parent / "pyproject.toml")
46
+ if repo_version:
47
+ return repo_version
48
+
49
+ candidates = [PACKAGE_ROOT.parent / "RSRF.egg-info" / "PKG-INFO"]
50
+ candidates.extend(sorted(PACKAGE_ROOT.parent.glob("RSRF-*.dist-info/METADATA")))
51
+ for candidate in candidates:
52
+ local_version = _metadata_version_from_path(candidate)
53
+ if local_version:
54
+ return local_version
55
+ return None
56
+
57
+
58
+ _local_version = _local_distribution_version()
59
+ if _local_version:
60
+ __version__ = _local_version
61
+ else:
62
+ __version__ = "0.0.1"
63
+ for _distribution_name in ("RSRF", "spectral-response-function"):
64
+ try:
65
+ __version__ = version(_distribution_name)
66
+ break
67
+ except PackageNotFoundError:
68
+ continue
69
+
70
+ __all__ = [
71
+ "PACKAGE_ROOT",
72
+ "__version__",
73
+ "export_docs_visualization_assets",
74
+ "get_metadata",
75
+ "iter_source_manifest_paths",
76
+ "list_bands",
77
+ "list_planned_sensors",
78
+ "list_sensors",
79
+ "load_band_spec",
80
+ "load_curve",
81
+ "load_response_definition",
82
+ "manifest_path",
83
+ "realize_curve",
84
+ "resolve_manifest_path",
85
+ "register_planned_sensor_catalog",
86
+ "validate_sensor",
87
+ "write_validation_artifacts",
88
+ ]
rsrf/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Module entrypoint for ``python -m rsrf``."""
2
+
3
+ from .commands.app import main
4
+
5
+
6
+ if __name__ == "__main__":
7
+ raise SystemExit(main())
rsrf/api.py ADDED
@@ -0,0 +1,272 @@
1
+ """Read API for registry-backed spectral response data."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from .io import read_json
10
+ from .models import BandSpec, ContentKind, SampledCurve
11
+ from .registry import read_registry_table, representation_variant_dir
12
+
13
+
14
+ def list_sensors(root: Path | None = None) -> list[dict[str, Any]]:
15
+ """List registered sensor representations."""
16
+
17
+ frame = _available_sensor_rows(root)
18
+ frame = frame.sort_values(["sensor_unit_id", "representation_variant"])
19
+ return frame.to_dict(orient="records")
20
+
21
+
22
+ def list_bands(
23
+ sensor_unit_id: str,
24
+ representation_variant: str | None = None,
25
+ *,
26
+ root: Path | None = None,
27
+ ) -> list[dict[str, Any]]:
28
+ """List bands for a sensor representation."""
29
+
30
+ sensor_row = _resolve_sensor_variant(
31
+ sensor_unit_id,
32
+ representation_variant,
33
+ root=root,
34
+ )
35
+ resolved_variant = str(sensor_row["representation_variant"])
36
+ frame = read_registry_table(root, "bands")
37
+ frame = frame[
38
+ (frame["sensor_unit_id"] == sensor_unit_id)
39
+ & (frame["representation_variant"] == resolved_variant)
40
+ ]
41
+ if "band_index" in frame.columns:
42
+ frame = frame.sort_values(["band_index", "band_id"], na_position="last")
43
+ return frame.to_dict(orient="records")
44
+
45
+
46
+ def get_metadata(
47
+ sensor_unit_id: str,
48
+ representation_variant: str | None = None,
49
+ *,
50
+ root: Path | None = None,
51
+ ) -> dict[str, Any]:
52
+ """Load the canonical metadata sidecar for a sensor representation."""
53
+
54
+ sensor_row = _resolve_sensor_variant(
55
+ sensor_unit_id,
56
+ representation_variant,
57
+ root=root,
58
+ )
59
+ metadata_path = (
60
+ _representation_dir(root, sensor_row)
61
+ / "metadata.json"
62
+ )
63
+ return read_json(metadata_path)
64
+
65
+
66
+ def load_curve(
67
+ sensor_unit_id: str,
68
+ band_id: str,
69
+ representation_variant: str | None = None,
70
+ *,
71
+ root: Path | None = None,
72
+ ) -> SampledCurve:
73
+ """Load a canonical sampled curve for a band."""
74
+
75
+ sensor_row = _resolve_sensor_variant(
76
+ sensor_unit_id,
77
+ representation_variant,
78
+ root=root,
79
+ )
80
+ resolved_variant = str(sensor_row["representation_variant"])
81
+ content_kind = ContentKind(str(sensor_row["content_kind"]))
82
+ if content_kind != ContentKind.SAMPLED_CURVE:
83
+ raise ValueError(
84
+ f"{sensor_unit_id}/{resolved_variant} is {content_kind.value}, not sampled_curve"
85
+ )
86
+
87
+ curves_path = (
88
+ _representation_dir(root, sensor_row)
89
+ / "curves.parquet"
90
+ )
91
+ frame = _read_parquet(curves_path)
92
+ frame = frame[
93
+ (frame["sensor_unit_id"] == sensor_unit_id)
94
+ & (frame["representation_variant"] == resolved_variant)
95
+ & (frame["band_id"] == band_id)
96
+ ]
97
+ if frame.empty:
98
+ raise KeyError(f"curve not found for {sensor_unit_id}/{resolved_variant}/{band_id}")
99
+ frame = frame.sort_values("wavelength_nm")
100
+ return SampledCurve(
101
+ band_id=band_id,
102
+ wavelength_nm=frame["wavelength_nm"].to_numpy(),
103
+ response=frame["response"].to_numpy(),
104
+ source_variant=resolved_variant,
105
+ )
106
+
107
+
108
+ def load_band_spec(
109
+ sensor_unit_id: str,
110
+ band_id: str,
111
+ representation_variant: str | None = None,
112
+ *,
113
+ root: Path | None = None,
114
+ ) -> BandSpec:
115
+ """Load a canonical band specification for a band."""
116
+
117
+ sensor_row = _resolve_sensor_variant(
118
+ sensor_unit_id,
119
+ representation_variant,
120
+ root=root,
121
+ )
122
+ resolved_variant = str(sensor_row["representation_variant"])
123
+ content_kind = ContentKind(str(sensor_row["content_kind"]))
124
+ if content_kind != ContentKind.BAND_SPEC:
125
+ raise ValueError(f"{sensor_unit_id}/{resolved_variant} is not a band_spec representation")
126
+
127
+ band_specs_path = (
128
+ _representation_dir(root, sensor_row)
129
+ / "band_specs.parquet"
130
+ )
131
+ frame = _read_parquet(band_specs_path)
132
+ frame = frame[
133
+ (frame["sensor_unit_id"] == sensor_unit_id)
134
+ & (frame["representation_variant"] == resolved_variant)
135
+ & (frame["band_id"] == band_id)
136
+ ]
137
+ if frame.empty:
138
+ raise KeyError(f"band spec not found for {sensor_unit_id}/{resolved_variant}/{band_id}")
139
+ row = frame.iloc[0]
140
+ band_registry_row = _lookup_band_registry_row(
141
+ sensor_unit_id,
142
+ band_id,
143
+ resolved_variant,
144
+ root=root,
145
+ )
146
+ shape_param_json = row["shape_param_json"]
147
+ return BandSpec(
148
+ band_id=str(row["band_id"]),
149
+ center_wavelength_nm=float(row["center_wavelength_nm"]),
150
+ fwhm_nm=float(row["fwhm_nm"]),
151
+ band_index=None if _is_nullish(row["band_index"]) else int(row["band_index"]),
152
+ band_name=(
153
+ str(band_registry_row["band_name"])
154
+ if band_registry_row is not None and not _is_nullish(band_registry_row["band_name"])
155
+ else str(row["band_id"])
156
+ ),
157
+ band_status="nominal" if _is_nullish(row["band_status"]) else str(row["band_status"]),
158
+ published_shape_type=str(row["published_shape_type"]),
159
+ shape_param_json=(
160
+ {}
161
+ if _is_nullish(shape_param_json) or not shape_param_json
162
+ else json.loads(shape_param_json)
163
+ ),
164
+ )
165
+
166
+
167
+ def load_response_definition(
168
+ sensor_unit_id: str,
169
+ band_id: str,
170
+ representation_variant: str | None = None,
171
+ *,
172
+ root: Path | None = None,
173
+ ) -> SampledCurve | BandSpec:
174
+ """Load either a sampled curve or a canonical band spec for a band."""
175
+
176
+ sensor_row = _resolve_sensor_variant(
177
+ sensor_unit_id,
178
+ representation_variant,
179
+ root=root,
180
+ )
181
+ resolved_variant = str(sensor_row["representation_variant"])
182
+ content_kind = ContentKind(str(sensor_row["content_kind"]))
183
+ if content_kind == ContentKind.SAMPLED_CURVE:
184
+ return load_curve(sensor_unit_id, band_id, resolved_variant, root=root)
185
+ if content_kind == ContentKind.BAND_SPEC:
186
+ return load_band_spec(sensor_unit_id, band_id, resolved_variant, root=root)
187
+ raise NotImplementedError(f"content_kind not supported: {content_kind.value}")
188
+
189
+
190
+ def _resolve_sensor_variant(
191
+ sensor_unit_id: str,
192
+ representation_variant: str | None,
193
+ *,
194
+ root: Path | None = None,
195
+ ) -> Any:
196
+ sensors = _available_sensor_rows(root)
197
+ frame = sensors[sensors["sensor_unit_id"] == sensor_unit_id]
198
+ if representation_variant is not None:
199
+ frame = frame[frame["representation_variant"] == representation_variant]
200
+ if frame.empty:
201
+ raise KeyError(f"sensor representation not found: {sensor_unit_id}/{representation_variant}")
202
+ if len(frame) > 1:
203
+ raise ValueError(
204
+ f"multiple representations found for {sensor_unit_id}; representation_variant is required"
205
+ )
206
+ return frame.iloc[0]
207
+
208
+
209
+ def _lookup_band_registry_row(
210
+ sensor_unit_id: str,
211
+ band_id: str,
212
+ representation_variant: str,
213
+ *,
214
+ root: Path | None = None,
215
+ ):
216
+ frame = read_registry_table(root, "bands")
217
+ frame = frame[
218
+ (frame["sensor_unit_id"] == sensor_unit_id)
219
+ & (frame["representation_variant"] == representation_variant)
220
+ & (frame["band_id"] == band_id)
221
+ ]
222
+ if frame.empty:
223
+ return None
224
+ if "band_index" in frame.columns:
225
+ frame = frame.sort_values(["band_index", "band_id"], na_position="last")
226
+ return frame.iloc[0]
227
+
228
+
229
+ def _available_sensor_rows(root: Path | None):
230
+ frame = read_registry_table(root, "sensors")
231
+ mask = frame.apply(_row_has_backing_artifact, axis=1, root=root)
232
+ return frame[mask].copy()
233
+
234
+
235
+ def _row_has_backing_artifact(row, *, root: Path | None) -> bool:
236
+ content_kind = ContentKind(str(row["content_kind"]))
237
+ artifact_name = _primary_artifact_name(content_kind)
238
+ if artifact_name is None:
239
+ return False
240
+ artifact_path = _representation_dir(root, row) / artifact_name
241
+ return artifact_path.exists()
242
+
243
+
244
+ def _representation_dir(root: Path | None, row) -> Path:
245
+ return representation_variant_dir(
246
+ root,
247
+ sensor_unit_id=str(row["sensor_unit_id"]),
248
+ representation_variant=str(row["representation_variant"]),
249
+ content_kind=str(row["content_kind"]),
250
+ realization_kind=str(row.get("realization_kind", "none")),
251
+ )
252
+
253
+
254
+ def _primary_artifact_name(content_kind: ContentKind) -> str | None:
255
+ if content_kind == ContentKind.SAMPLED_CURVE:
256
+ return "curves.parquet"
257
+ if content_kind == ContentKind.BAND_SPEC:
258
+ return "band_specs.parquet"
259
+ return None
260
+
261
+
262
+ def _is_nullish(value: Any) -> bool:
263
+ return value is None or value != value
264
+
265
+
266
+ def _read_parquet(path: Path):
267
+ try:
268
+ import pandas as pd
269
+ except ImportError as exc:
270
+ raise RuntimeError("pandas is required for read API operations") from exc
271
+
272
+ return pd.read_parquet(path)
rsrf/band_specs.py ADDED
@@ -0,0 +1,47 @@
1
+ """Helpers for metadata-only band specifications."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .models import BandSpec
6
+
7
+
8
+ def default_band_id(band_index: int) -> str:
9
+ """Build a stable band identifier for unnamed hyperspectral bands."""
10
+
11
+ if band_index < 0:
12
+ raise ValueError("band_index must be non-negative")
13
+ return f"B{band_index:03d}"
14
+
15
+
16
+ def build_band_spec(
17
+ center_wavelength_nm: float,
18
+ fwhm_nm: float,
19
+ *,
20
+ band_index: int | None = None,
21
+ band_id: str | None = None,
22
+ band_name: str | None = None,
23
+ band_status: str = "nominal",
24
+ published_shape_type: str = "unknown",
25
+ ) -> BandSpec:
26
+ """Create a bootstrap band specification."""
27
+
28
+ if center_wavelength_nm <= 0:
29
+ raise ValueError("center_wavelength_nm must be positive")
30
+ if fwhm_nm <= 0:
31
+ raise ValueError("fwhm_nm must be positive")
32
+
33
+ resolved_band_id = band_id
34
+ if resolved_band_id is None:
35
+ if band_index is None:
36
+ raise ValueError("band_id or band_index must be provided")
37
+ resolved_band_id = default_band_id(band_index)
38
+
39
+ return BandSpec(
40
+ band_id=resolved_band_id,
41
+ center_wavelength_nm=center_wavelength_nm,
42
+ fwhm_nm=fwhm_nm,
43
+ band_index=band_index,
44
+ band_name=band_name,
45
+ band_status=band_status,
46
+ published_shape_type=published_shape_type,
47
+ )
rsrf/cli.py ADDED
@@ -0,0 +1,6 @@
1
+ """Compatibility wrapper for the CLI entrypoint."""
2
+
3
+ from .commands.app import main
4
+ from .commands.parser import build_parser
5
+
6
+ __all__ = ["build_parser", "main"]
@@ -0,0 +1,6 @@
1
+ """CLI application package."""
2
+
3
+ from .app import main
4
+ from .parser import build_parser
5
+
6
+ __all__ = ["build_parser", "main"]
rsrf/commands/app.py ADDED
@@ -0,0 +1,49 @@
1
+ """Application entrypoint for the ``rsrf`` CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Callable, Sequence
6
+
7
+ from .handlers import (
8
+ handle_export_validation,
9
+ handle_list_bands,
10
+ handle_list_planned_sensors,
11
+ handle_list_sensors,
12
+ handle_register_manifest,
13
+ handle_register_planned_sensors,
14
+ handle_show_layout,
15
+ handle_show_metadata,
16
+ handle_show_registry_rows,
17
+ handle_show_response,
18
+ handle_validate_manifest,
19
+ handle_validate_sensor,
20
+ )
21
+ from .parser import build_parser
22
+
23
+ COMMAND_HANDLERS: dict[str, Callable[..., int]] = {
24
+ "show-layout": handle_show_layout,
25
+ "list-sensors": handle_list_sensors,
26
+ "list-planned-sensors": handle_list_planned_sensors,
27
+ "list-bands": handle_list_bands,
28
+ "show-metadata": handle_show_metadata,
29
+ "show-response": handle_show_response,
30
+ "validate-sensor": handle_validate_sensor,
31
+ "export-validation": handle_export_validation,
32
+ "validate-manifest": handle_validate_manifest,
33
+ "show-registry-rows": handle_show_registry_rows,
34
+ "register-manifest": handle_register_manifest,
35
+ "register-planned-sensors": handle_register_planned_sensors,
36
+ }
37
+
38
+
39
+ def main(argv: Sequence[str] | None = None) -> int:
40
+ """Run the CLI."""
41
+
42
+ parser = build_parser()
43
+ args = parser.parse_args(list(argv) if argv is not None else None)
44
+
45
+ handler = COMMAND_HANDLERS.get(args.command)
46
+ if handler is None:
47
+ parser.print_help()
48
+ return 0
49
+ return handler(args)
@@ -0,0 +1,213 @@
1
+ """Command handlers for the ``rsrf`` CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from argparse import Namespace
6
+ from pathlib import Path
7
+
8
+ from ..api import get_metadata, list_bands, list_sensors, load_response_definition
9
+ from ..models import ManifestSummary, ManifestValidationError
10
+ from ..planning import list_planned_sensors, register_planned_sensor_catalog
11
+ from ..qa import validate_sensor, write_validation_artifacts
12
+ from ..registry import build_repo_layout, manifest_registry_rows, register_manifest
13
+ from ..validate import parse_manifest_file
14
+ from .render import (
15
+ exception_message,
16
+ print_json,
17
+ print_manifest_errors,
18
+ summarize_response_definition,
19
+ )
20
+
21
+
22
+ def handle_show_layout(args: Namespace) -> int:
23
+ layout = build_repo_layout(args.root)
24
+ entries = {
25
+ "root": layout.root,
26
+ "package_root": layout.package_root,
27
+ "registry_root": layout.registry_root,
28
+ "canonical_root": layout.canonical_root,
29
+ "realized_root": layout.realized_root,
30
+ "common_grid_root": layout.common_grid_root,
31
+ "source_manifests_root": layout.source_manifests_root,
32
+ "official_source_manifests_root": layout.official_source_manifests_root,
33
+ "planning_source_manifests_root": layout.planning_source_manifests_root,
34
+ "template_source_manifests_root": layout.template_source_manifests_root,
35
+ "ingest_scripts_root": layout.ingest_scripts_root,
36
+ "tests_root": layout.tests_root,
37
+ }
38
+ for label, path in entries.items():
39
+ print(f"{label}: {path}")
40
+ return 0
41
+
42
+
43
+ def handle_list_sensors(args: Namespace) -> int:
44
+ try:
45
+ sensors = list_sensors(root=args.root)
46
+ except (FileNotFoundError, KeyError, RuntimeError, ValueError) as exc:
47
+ print(exception_message(exc))
48
+ return 1
49
+ return print_json(sensors)
50
+
51
+
52
+ def handle_list_planned_sensors(args: Namespace) -> int:
53
+ try:
54
+ sensors = list_planned_sensors(root=args.root, catalog_path=args.catalog_path)
55
+ except (FileNotFoundError, KeyError, RuntimeError, ValueError) as exc:
56
+ print(exception_message(exc))
57
+ return 1
58
+ return print_json(sensors)
59
+
60
+
61
+ def handle_list_bands(args: Namespace) -> int:
62
+ try:
63
+ bands = list_bands(
64
+ args.sensor_unit_id,
65
+ args.representation_variant,
66
+ root=args.root,
67
+ )
68
+ except (FileNotFoundError, KeyError, RuntimeError, ValueError) as exc:
69
+ print(exception_message(exc))
70
+ return 1
71
+ return print_json(bands)
72
+
73
+
74
+ def handle_show_metadata(args: Namespace) -> int:
75
+ try:
76
+ metadata = get_metadata(
77
+ args.sensor_unit_id,
78
+ args.representation_variant,
79
+ root=args.root,
80
+ )
81
+ except (FileNotFoundError, KeyError, RuntimeError, ValueError) as exc:
82
+ print(exception_message(exc))
83
+ return 1
84
+ return print_json(metadata)
85
+
86
+
87
+ def handle_show_response(args: Namespace) -> int:
88
+ try:
89
+ metadata = get_metadata(
90
+ args.sensor_unit_id,
91
+ args.representation_variant,
92
+ root=args.root,
93
+ )
94
+ resolved_variant = str(metadata["representation_variant"])
95
+ response_definition = load_response_definition(
96
+ args.sensor_unit_id,
97
+ args.band_id,
98
+ resolved_variant,
99
+ root=args.root,
100
+ )
101
+ except (FileNotFoundError, KeyError, RuntimeError, ValueError) as exc:
102
+ print(exception_message(exc))
103
+ return 1
104
+
105
+ payload = summarize_response_definition(
106
+ args.sensor_unit_id,
107
+ resolved_variant,
108
+ response_definition,
109
+ )
110
+ return print_json(payload)
111
+
112
+
113
+ def handle_validate_sensor(args: Namespace) -> int:
114
+ try:
115
+ report = validate_sensor(
116
+ args.sensor_unit_id,
117
+ args.representation_variant,
118
+ root=args.root,
119
+ )
120
+ except (FileNotFoundError, KeyError, RuntimeError, ValueError, NotImplementedError) as exc:
121
+ print(exception_message(exc))
122
+ return 1
123
+ print_json(report)
124
+ return 0 if report.get("passed") else 1
125
+
126
+
127
+ def handle_export_validation(args: Namespace) -> int:
128
+ try:
129
+ written = write_validation_artifacts(
130
+ args.sensor_unit_id,
131
+ args.representation_variant,
132
+ root=args.root,
133
+ output_dir=args.output_dir,
134
+ )
135
+ except (FileNotFoundError, KeyError, RuntimeError, ValueError, NotImplementedError) as exc:
136
+ print(exception_message(exc))
137
+ return 1
138
+ for label, path in written.items():
139
+ print(f"{label}: {path}")
140
+ return 0
141
+
142
+
143
+ def handle_validate_manifest(args: Namespace) -> int:
144
+ manifest_path = Path(args.manifest_path)
145
+ try:
146
+ manifest = parse_manifest_file(manifest_path, root=args.root)
147
+ except ManifestValidationError as exc:
148
+ return print_manifest_errors(manifest_path, exc.errors)
149
+
150
+ summary = ManifestSummary.from_manifest(manifest)
151
+ print(
152
+ "Manifest OK: "
153
+ f"{summary.sensor_unit_id} "
154
+ f"[{summary.representation_variant}] "
155
+ f"{summary.content_kind} "
156
+ f"(tier {summary.source_tier})"
157
+ )
158
+ return 0
159
+
160
+
161
+ def handle_show_registry_rows(args: Namespace) -> int:
162
+ manifest_path = Path(args.manifest_path)
163
+ try:
164
+ manifest = parse_manifest_file(manifest_path, root=args.root)
165
+ except ManifestValidationError as exc:
166
+ return print_manifest_errors(manifest_path, exc.errors)
167
+
168
+ rows = manifest_registry_rows(manifest)
169
+ for table_name, table_rows in rows.items():
170
+ print(f"[{table_name}]")
171
+ if not table_rows:
172
+ print("[]")
173
+ continue
174
+ for row in table_rows:
175
+ print_json(dict(row))
176
+ return 0
177
+
178
+
179
+ def handle_register_manifest(args: Namespace) -> int:
180
+ manifest_path = Path(args.manifest_path)
181
+ try:
182
+ manifest = parse_manifest_file(manifest_path, root=args.root)
183
+ except ManifestValidationError as exc:
184
+ return print_manifest_errors(manifest_path, exc.errors)
185
+
186
+ try:
187
+ written = register_manifest(args.root, manifest)
188
+ except RuntimeError as exc:
189
+ print(str(exc))
190
+ return 1
191
+
192
+ if not written:
193
+ print("No registry rows were written.")
194
+ return 0
195
+
196
+ for table_name, path in written.items():
197
+ print(f"{table_name}: {path}")
198
+ return 0
199
+
200
+
201
+ def handle_register_planned_sensors(args: Namespace) -> int:
202
+ try:
203
+ written = register_planned_sensor_catalog(root=args.root, catalog_path=args.catalog_path)
204
+ except (FileNotFoundError, KeyError, RuntimeError, ValueError) as exc:
205
+ print(exception_message(exc))
206
+ return 1
207
+
208
+ if written is None:
209
+ print("No planned sensor rows were written.")
210
+ return 0
211
+
212
+ print(f"sensors: {written}")
213
+ return 0