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 +88 -0
- rsrf/__main__.py +7 -0
- rsrf/api.py +272 -0
- rsrf/band_specs.py +47 -0
- rsrf/cli.py +6 -0
- rsrf/commands/__init__.py +6 -0
- rsrf/commands/app.py +49 -0
- rsrf/commands/handlers.py +213 -0
- rsrf/commands/parser.py +144 -0
- rsrf/commands/render.py +88 -0
- rsrf/convolve.py +95 -0
- rsrf/ingest.py +324 -0
- rsrf/io.py +196 -0
- rsrf/manifests.py +108 -0
- rsrf/models.py +834 -0
- rsrf/parsers/__init__.py +32 -0
- rsrf/parsers/band_spec_table.py +169 -0
- rsrf/parsers/common.py +188 -0
- rsrf/parsers/emit.py +115 -0
- rsrf/parsers/enmap.py +119 -0
- rsrf/parsers/landsat_tirs.py +97 -0
- rsrf/parsers/modis.py +114 -0
- rsrf/parsers/multiband_curve_csv.py +117 -0
- rsrf/parsers/obpg.py +222 -0
- rsrf/parsers/olci.py +54 -0
- rsrf/parsers/prisma.py +168 -0
- rsrf/parsers/probav.py +75 -0
- rsrf/parsers/sentinel2.py +265 -0
- rsrf/parsers/usgs_json.py +171 -0
- rsrf/parsers/viirs.py +128 -0
- rsrf/planning.py +193 -0
- rsrf/plotting.py +211 -0
- rsrf/qa.py +697 -0
- rsrf/realize.py +114 -0
- rsrf/registry.py +462 -0
- rsrf/resample.py +28 -0
- rsrf/validate.py +52 -0
- rsrf/visualization.py +334 -0
- rsrf-0.0.1.dist-info/METADATA +207 -0
- rsrf-0.0.1.dist-info/RECORD +43 -0
- rsrf-0.0.1.dist-info/WHEEL +5 -0
- rsrf-0.0.1.dist-info/entry_points.txt +2 -0
- rsrf-0.0.1.dist-info/top_level.txt +1 -0
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
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
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
|