Skip to content

Commit af12232

Browse files
jordanpadamsclaude
andcommitted
Consolidate missing products CSVs into single file with superseded column
Instead of generating three separate CSVs per product type (overall, latest, superseded), generate one CSV with a `superseded` column (true/false) indicating whether each LIDVID is the latest version for its LID. History counts and README metrics table are unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c7aeae8 commit af12232

2 files changed

Lines changed: 52 additions & 58 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ Both use the NASA-PDS Roundup action for building and releasing.
196196
- Requires AWS/Cognito credentials via `~/.pds/.registry-client` or `.env`
197197
- Uses `pds-registry-client` from the **same venv as the running Python** (resolved via `sys.executable`); do not rely on shell PATH
198198
- Queries `conf/status/*.json` OpenSearch DSL files against the legacy and current registry indices
199-
- For missing products, generates three CSVs per type: overall, `*_latest_*` (highest version per LID), `*_superseded_*` (older versions)
199+
- For missing products, generates one CSV per type with a `superseded` column (`true`/`false`) indicating whether a LIDVID is the latest version for its LID or an older version
200200
- Appends one row to `docs/status/counts_history.csv` on every run for burndown tracking — this file is **append-only, never overwritten**
201201
- Run with `--no-commit` to generate locally without pushing
202202

scripts/generate_registry_status_reports.py

Lines changed: 51 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
This script queries OpenSearch using pds-registry-client and generates CSV reports
66
for missing and staged bundles and collections per node.
77
8-
For missing products, three CSVs are produced per product type:
9-
- overall (all versions)
10-
- latest (highest version per LID)
11-
- superseded (all older versions per LID)
8+
For missing products, one CSV is produced per product type with a ``superseded``
9+
column (``true``/``false``) indicating whether a given LIDVID is the latest version
10+
for its LID or an older, superseded version.
1211
1312
Environment variables required:
1413
REQUEST_SIGNER_AWS_ACCOUNT
@@ -151,20 +150,20 @@ def write_rows_to_csv(rows: list[tuple], output_file: Path) -> int:
151150
return len(rows)
152151

153152

154-
def split_by_version(rows: list[tuple]) -> tuple[list[tuple], list[tuple]]:
155-
"""Split rows into latest and superseded versions grouped by LID.
153+
def annotate_version_status(rows: list[tuple]) -> list[tuple]:
154+
"""Annotate rows with a ``superseded`` column based on version comparison per LID.
156155
157156
Expects lidvid as the second element (index 1) of each row, in the form
158157
``urn:nasa:pds:<lid>::<major>.<minor>``. Version comparison is numeric so
159158
that e.g. ``3.9 < 3.13``.
160159
161-
Returns:
162-
(latest_rows, superseded_rows) where latest_rows contains only the
163-
highest-versioned row per LID and superseded_rows contains all others.
160+
Returns a new list of rows with an additional ``superseded`` column appended
161+
(string ``"true"`` or ``"false"``). The highest-versioned row per LID gets
162+
``"false"``; all others get ``"true"``.
164163
"""
165-
by_lid: dict[str, list[tuple[tuple[int, ...], tuple]]] = defaultdict(list)
164+
by_lid: dict[str, list[tuple[tuple[int, ...], int]]] = defaultdict(list)
166165

167-
for row in rows:
166+
for idx, row in enumerate(rows):
168167
lidvid = row[1]
169168
if "::" in lidvid:
170169
lid, ver_str = lidvid.rsplit("::", 1)
@@ -176,38 +175,48 @@ def split_by_version(rows: list[tuple]) -> tuple[list[tuple], list[tuple]]:
176175
except ValueError:
177176
ver_key = (0,)
178177

179-
by_lid[lid].append((ver_key, row))
178+
by_lid[lid].append((ver_key, idx))
180179

181-
latest_rows: list[tuple] = []
182-
superseded_rows: list[tuple] = []
180+
superseded_indices: set[int] = set()
181+
for version_entries in by_lid.values():
182+
sorted_entries = sorted(version_entries, key=lambda x: x[0], reverse=True)
183+
for _, idx in sorted_entries[1:]:
184+
superseded_indices.add(idx)
183185

184-
for version_rows in by_lid.values():
185-
sorted_rows = sorted(version_rows, key=lambda x: x[0], reverse=True)
186-
latest_rows.append(sorted_rows[0][1])
187-
superseded_rows.extend(r for _, r in sorted_rows[1:])
186+
return [row + ("true" if idx in superseded_indices else "false",) for idx, row in enumerate(rows)]
188187

189-
return latest_rows, superseded_rows
190188

189+
def _count_by_node(csv_path: Path, superseded: bool | None = None) -> dict[str, int]:
190+
"""Return a node→count mapping from a CSV file (node is the first column).
191191
192-
def _count_by_node(csv_path: Path) -> dict[str, int]:
193-
"""Return a node→count mapping from a CSV file (node is the first column)."""
192+
If ``superseded`` is ``None``, all rows are counted. Pass ``True`` or
193+
``False`` to count only the rows whose last column matches ``"true"`` or
194+
``"false"`` respectively (used for missing-product CSVs which carry a
195+
superseded annotation in their last column).
196+
"""
194197
counts: dict[str, int] = defaultdict(int)
195198
if csv_path.exists():
196199
with open(csv_path, "r") as f:
197200
for row in csv.reader(f):
198-
if row:
199-
counts[row[0]] += 1
201+
if not row:
202+
continue
203+
if superseded is not None:
204+
if len(row) < 4:
205+
continue
206+
if (row[-1].lower() == "true") != superseded:
207+
continue
208+
counts[row[0]] += 1
200209
return counts
201210

202211

203212
def generate_metrics_from_csvs(csv_files: dict[str, Path]) -> str:
204213
"""Generate metrics summary markdown from CSV files."""
205214
mb = _count_by_node(csv_files["missing_bundles"])
206-
mb_latest = _count_by_node(csv_files["missing_bundles_latest"])
207-
mb_superseded = _count_by_node(csv_files["missing_bundles_superseded"])
215+
mb_latest = _count_by_node(csv_files["missing_bundles"], superseded=False)
216+
mb_superseded = _count_by_node(csv_files["missing_bundles"], superseded=True)
208217
mc = _count_by_node(csv_files["missing_collections"])
209-
mc_latest = _count_by_node(csv_files["missing_collections_latest"])
210-
mc_superseded = _count_by_node(csv_files["missing_collections_superseded"])
218+
mc_latest = _count_by_node(csv_files["missing_collections"], superseded=False)
219+
mc_superseded = _count_by_node(csv_files["missing_collections"], superseded=True)
211220
sb = _count_by_node(csv_files["staged_bundles"])
212221
sc = _count_by_node(csv_files["staged_collections"])
213222

@@ -295,14 +304,20 @@ def append_history_row(history_file: Path, csv_files: dict[str, Path]) -> None:
295304
def total(path: Path) -> int:
296305
return sum(_count_by_node(path).values())
297306

307+
def latest(path: Path) -> int:
308+
return sum(_count_by_node(path, superseded=False).values())
309+
310+
def superseded_total(path: Path) -> int:
311+
return sum(_count_by_node(path, superseded=True).values())
312+
298313
row = ",".join([
299314
datetime.now(timezone.utc).strftime("%Y-%m-%d"),
300315
str(total(csv_files["missing_bundles"])),
301-
str(total(csv_files["missing_bundles_latest"])),
302-
str(total(csv_files["missing_bundles_superseded"])),
316+
str(latest(csv_files["missing_bundles"])),
317+
str(superseded_total(csv_files["missing_bundles"])),
303318
str(total(csv_files["missing_collections"])),
304-
str(total(csv_files["missing_collections_latest"])),
305-
str(total(csv_files["missing_collections_superseded"])),
319+
str(latest(csv_files["missing_collections"])),
320+
str(superseded_total(csv_files["missing_collections"])),
306321
str(total(csv_files["staged_bundles"])),
307322
str(total(csv_files["staged_collections"])),
308323
])
@@ -522,30 +537,13 @@ def main() -> int:
522537
data = run_query(query_file, endpoint)
523538
rows = extract_rows(data, include_harvest_date)
524539

525-
# Always write the overall CSV
526-
count = write_rows_to_csv(rows, output_file)
527-
print_info(f" overall → {output_file.name} ({count} records)")
528-
output_files.append(output_file)
529-
530540
if split_versions:
531-
# Derive sibling paths: missing_bundles_in_registry.csv
532-
# → missing_bundles_latest_in_registry.csv
533-
# → missing_bundles_superseded_in_registry.csv
534-
stem = output_file.stem # e.g. "missing_bundles_in_registry"
535-
suffix = output_file.suffix
536-
base = stem.replace("_in_registry", "") # "missing_bundles"
537-
latest_file = output_dir / f"{base}_latest_in_registry{suffix}"
538-
superseded_file = output_dir / f"{base}_superseded_in_registry{suffix}"
539-
540-
latest_rows, superseded_rows = split_by_version(rows)
541-
542-
latest_count = write_rows_to_csv(latest_rows, latest_file)
543-
print_info(f" latest → {latest_file.name} ({latest_count} records)")
544-
output_files.append(latest_file)
541+
# Annotate each row with a superseded column before writing
542+
rows = annotate_version_status(rows)
545543

546-
superseded_count = write_rows_to_csv(superseded_rows, superseded_file)
547-
print_info(f" superseded {superseded_file.name} ({superseded_count} records)")
548-
output_files.append(superseded_file)
544+
count = write_rows_to_csv(rows, output_file)
545+
print_info(f" → {output_file.name} ({count} records)")
546+
output_files.append(output_file)
549547

550548
except subprocess.CalledProcessError as e:
551549
print_error(f"Failed to generate {description} report: {e.stderr}")
@@ -560,11 +558,7 @@ def main() -> int:
560558
print_info("Updating metrics in README...")
561559
csv_files = {
562560
"missing_bundles": output_dir / "missing_bundles_in_registry.csv",
563-
"missing_bundles_latest": output_dir / "missing_bundles_latest_in_registry.csv",
564-
"missing_bundles_superseded": output_dir / "missing_bundles_superseded_in_registry.csv",
565561
"missing_collections": output_dir / "missing_collections_in_registry.csv",
566-
"missing_collections_latest": output_dir / "missing_collections_latest_in_registry.csv",
567-
"missing_collections_superseded": output_dir / "missing_collections_superseded_in_registry.csv",
568562
"staged_bundles": output_dir / "staged_bundles_in_registry.csv",
569563
"staged_collections": output_dir / "staged_collections_in_registry.csv",
570564
}

0 commit comments

Comments
 (0)