From 51884b10a0dde2516c2a5c76dfa651a83577c699 Mon Sep 17 00:00:00 2001 From: aladinor Date: Sun, 3 May 2026 21:47:38 -0500 Subject: [PATCH 1/7] FIX: NEXRAD Level 2 LDM stride drops MSG_31 records (#376) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NEXRADRecordFile.init_record hard-coded a 120-message stride between LDM-compressed records. The NEXRAD ICD 2620010J §7.3.4 mandates a *variable* count: each LDM holds 120 radial messages (MSG_31) plus 0 or more RDA Status messages (MSG_2). When MSG_2 is interleaved with the 120 MSG_31s, xradar's mod-120 budget exhausts on the first 118 MSG_31 + 2 MSG_2, then jumps to the next LDM, silently dropping the trailing 2 MSG_31s. On KILX20230629_154426_V06, sweep_10 (elev_num=11) reports 358 rays on-wire-correct 360. The two missing radials sit at azimuth_numbers 119 and 120 in LDM 49 (122 messages = 120 MSG_31 + 2 MSG_2). Independent bzip2 byte-walks, danielway/nexrad 1.0.0-rc.7, and radish's internal decoder all read 6840 records on this fixture; xradar reports 6838. Replace the synthetic (recnum - 134) // 120 LDM lookup and the (recnum - 134) % 120 in-LDM offset with a true byte-walk: - _load_ldm(idx): decompress + cache LDM idx (extracted from init_record's previous inline block). - _resolve_compressed_data_position(recnum): compute (ldm, byte_offset) via cache-hit / first-compressed-call / sequential-advance branches. Cross-LDM advancement detects end-of-LDM by buffer length, not message count. - init_record: three-branch dispatch (metadata / uncompressed-data / compressed-data) with a shared trailer. The recnum-position cache is populated on every successful walk, so non-sequential init_record(stored_recnum) jumps from get_data's per-moment replay (line 775) restore from cache. Extend test_bz2_compressed_buffer_path_real to assert the cold-entry at recnum=134 transitions cleanly into a sequential walk — pins the byte-walker contract against future regressions. --- tests/io/test_nexrad_level2.py | 6 ++ xradar/io/backends/nexrad_level2.py | 149 +++++++++++++++++++--------- 2 files changed, 108 insertions(+), 47 deletions(-) diff --git a/tests/io/test_nexrad_level2.py b/tests/io/test_nexrad_level2.py index 563d4b25..7ced0830 100644 --- a/tests/io/test_nexrad_level2.py +++ b/tests/io/test_nexrad_level2.py @@ -981,6 +981,12 @@ def test_bz2_compressed_buffer_path_real(nexradlevel2_bzfile): assert isinstance(fh._ldm[1], np.ndarray) assert fh._ldm[1].dtype == np.uint8 + # Cold entry at recnum=134 must transition cleanly into a sequential + # walk — pins the byte-walker against future regressions in the + # cross-LDM-stride logic (#376). + assert fh.init_next_record() + assert fh.record_number == 135 + def test_nexradlevel2_missing_msg2_metadata(): """ diff --git a/xradar/io/backends/nexrad_level2.py b/xradar/io/backends/nexrad_level2.py index 7d27c7b6..bf2df679 100644 --- a/xradar/io/backends/nexrad_level2.py +++ b/xradar/io/backends/nexrad_level2.py @@ -340,6 +340,13 @@ def __init__(self, filename, **kwargs): self._rc = None self._ldm = dict() self._record_number = None + # Compressed-data byte-walker state (ICD 2620010J §7.3.4). + # An LDM block holds 120 MSG_31 + 0..N MSG_2 messages — variable count, + # not a fixed 120 stride. Track the active LDM and a recnum->position + # cache so sequential walks advance by message size and non-sequential + # jumps (e.g. per-moment replay in get_data) restore from cache. + self._current_ldm = None + self._recnum_pos_cache = {} @property def rh(self): @@ -387,60 +394,66 @@ def get_end(self, buf): size = size if size >= RECORD_BYTES else RECORD_BYTES return size - def init_record(self, recnum): - """Initialize record using given number.""" + def _load_ldm(self, ldm_idx): + """Decompress LDM `ldm_idx` into ``self._ldm`` if not already loaded. - # map record numbers to ldm compressed records - def get_ldm(recnum): - if recnum < 134: - return 0 - mod = ((recnum - 134) // 120) + 1 - return mod + Returns ``True`` on success, ``False`` if ``ldm_idx`` is past the last LDM. + """ + if ldm_idx >= len(self.bz2_record_indices): + return False + if self._ldm.get(ldm_idx) is not None: + return True + start = self.bz2_record_indices[ldm_idx] + size = int(self._fh[start : start + 4].view(dtype=">u4")[0]) + if self._fp is not None: + self._fp.seek(start + 4) + compressed = self._fp.read(size) + else: + compressed = self._fh[start + 4 : start + 4 + size].tobytes() + dec = bz2.BZ2Decompressor() + self._ldm[ldm_idx] = np.frombuffer(dec.decompress(compressed), dtype=np.uint8) + return True - if self.is_compressed: - ldm = get_ldm(recnum) - # get uncompressed ldm record - if self._ldm.get(ldm, None) is None: - # otherwise extract wanted ldm compressed record - if ldm >= len(self.bz2_record_indices): - return False - start = self.bz2_record_indices[ldm] - size = self._fh[start : start + 4].view(dtype=">u4")[0] - if self._fp is not None: - self._fp.seek(start + 4) - compressed = self._fp.read(size) - else: - compressed = self._fh[start + 4 : start + 4 + size].tobytes() - dec = bz2.BZ2Decompressor() - self._ldm[ldm] = np.frombuffer( - dec.decompress(compressed), dtype=np.uint8 - ) + def init_record(self, recnum): + """Initialize record using given number. - # retrieve wanted record and put into self.rh + Per ICD 2620010J §7.3.4, an LDM Compressed Record contains a variable + number of messages (120 MSG_31 + 0..N MSG_2 RDA Status). Cross-LDM + boundaries are detected by reaching the end of the decompressed buffer, + not by a fixed message-count stride. + """ + # Branch A: metadata records (always in LDM 0, fixed RECORD_BYTES stride) if recnum < 134: + if self.is_compressed and not self._load_ldm(0): + return False start = recnum * RECORD_BYTES - if not self.is_compressed: - # Only add volume header offset if header exists - if self.volume_header is not None: - start += 24 + if not self.is_compressed and self.volume_header is not None: + start += 24 stop = start + RECORD_BYTES + ldm = 0 + + # Branch B: uncompressed data records (sequential advance) + elif not self.is_compressed: + start = self.record_size + self.filepos + buf = self.fh[start + 12 : start + 12 + LEN_MSG_HEADER] + size = self.get_end(buf) + if not size: + return False + stop = start + size + + # Branch C: compressed data records (byte-walk with recnum-position cache) else: - if self.is_compressed: - # get index into current compressed ldm record - rnum = (recnum - 134) % 120 - start = self.record_size + self.filepos if rnum else 0 - buf = self._ldm[ldm][start + 12 : start + 12 + LEN_MSG_HEADER] - size = self.get_end(buf) - if not size: - return False - stop = start + size - else: - start = self.record_size + self.filepos - buf = self.fh[start + 12 : start + 12 + LEN_MSG_HEADER] - size = self.get_end(buf) - if not size: - return False - stop = start + size + ldm, start = self._resolve_compressed_data_position(recnum) + if ldm is None: + return False + buf = self._ldm[ldm][start + 12 : start + 12 + LEN_MSG_HEADER] + size = self.get_end(buf) + if not size: + return False + stop = start + size + self._current_ldm = ldm + self._recnum_pos_cache[recnum] = (ldm, start) + self.record_number = recnum self.record_size = stop - start if self.is_compressed: @@ -450,6 +463,48 @@ def get_ldm(recnum): self.filepos = start return self._check_record() + def _resolve_compressed_data_position(self, recnum): + """Return ``(ldm_idx, byte_offset)`` for compressed-data ``recnum >= 134``. + + Returns ``(None, None)`` past the last LDM. Raises if ``recnum`` is + non-sequential and not in the cache (current callers don't trigger this). + """ + # Cache hit: non-sequential jump (e.g. get_data per-moment replay). + # The cache is only populated after a successful _load_ldm, and the + # backend never evicts from self._ldm, so the LDM is already resident. + if recnum in self._recnum_pos_cache: + return self._recnum_pos_cache[recnum] + + # First compressed call: either cold entry at recnum=134 or sequential + # transition from metadata. Either way, start at LDM 1 byte 0. + if self._current_ldm is None: + if recnum != 134: + raise ValueError( + f"first compressed init_record must be recnum=134, got {recnum}" + ) + if not self._load_ldm(1): + return (None, None) + return (1, 0) + + # Otherwise must be a sequential advance from the prior record. + if recnum != self._record_number + 1: + raise ValueError( + f"non-sequential init_record({recnum}) into uncached recnum" + ) + + # Continue from the previous in-LDM byte position; advance to next + # LDM if the current buffer can no longer hold a message header. + ldm = self._current_ldm + start = self.filepos + self.record_size + while ldm < len(self.bz2_record_indices): + if not self._load_ldm(ldm): + return (None, None) + if start + 12 + LEN_MSG_HEADER <= len(self._ldm[ldm]): + return (ldm, start) + ldm += 1 + start = 0 + return (None, None) + def init_record_by_filepos(self, recnum, filepos): """Initialize record using given record number and position.""" start = filepos From 976c81da5c93e542f8d98dba66955c5c4e5009df Mon Sep 17 00:00:00 2001 From: aladinor Date: Sun, 3 May 2026 21:48:40 -0500 Subject: [PATCH 2/7] DOC: add changelog entry for #376 / #377 --- docs/history.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/history.md b/docs/history.md index c5567005..4972e8de 100644 --- a/docs/history.md +++ b/docs/history.md @@ -2,6 +2,7 @@ ## Development +* FIX: NEXRAD Level 2 reader drops MSG_31 records when an LDM Compressed Record carries MSG_2 (RDA Status) alongside the 120 radials — ``NEXRADRecordFile.init_record`` hard-coded a 120-message stride; per ICD ``2620010J`` §7.3.4 the LDM holds "120 radial messages (type 31) plus 0 or more RDA Status messages (type 2)". Replace the synthetic mod-120 stride with a byte-walk that detects end-of-LDM by buffer length, with a recnum-position cache for ``get_data``'s per-moment replay ({issue}`376`, {pull}`377`) by [@aladinor](https://github.com/aladinor) * FIX: ensure `to_cfradial2` correctly selects the default storage engine when none is provided, ({pull}`378`) by [@chfer](https://github.com/chfer) * MNT: Add ``cfradial1_sgp_file`` session fixture and refactor 8 tests in ``test_util.py``/``test_accessors.py`` to share it instead of inlining ``DATASETS.fetch("sample_sgp_data.nc")``. Fixture returns the filename so each test opens its own DataTree, avoiding cross-test mutation ({issue}`346`, {pull}`347`) by [@aladinor](https://github.com/aladinor) * FIX: IRIS reader rotates the first-loaded moment in each sweep by 1 ray — ``IrisRawFile._get_ray_record_offsets_and_data`` initialised ``j = -1`` so the first matching ray of the first-loaded moment was written to ``raw_data[-1]``; affects files without ``DB_XHDR`` (data-type bit 0) where ``DB_DBT`` becomes the rotated moment ({issue}`357`, {pull}`375`) by [@aladinor](https://github.com/aladinor) From dad27c86c235ab4a318d469699628ab994366f22 Mon Sep 17 00:00:00 2001 From: aladinor Date: Sun, 3 May 2026 21:57:52 -0500 Subject: [PATCH 3/7] TST: cover invariant-violation and EOF paths in init_record --- tests/io/test_nexrad_level2.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/io/test_nexrad_level2.py b/tests/io/test_nexrad_level2.py index 7ced0830..72901ab6 100644 --- a/tests/io/test_nexrad_level2.py +++ b/tests/io/test_nexrad_level2.py @@ -988,6 +988,40 @@ def test_bz2_compressed_buffer_path_real(nexradlevel2_bzfile): assert fh.record_number == 135 +def test_init_record_cold_compressed_non_134_raises(nexradlevel2_bzfile): + """Cold init_record into compressed data must start at recnum=134 (#376).""" + with open(nexradlevel2_bzfile, "rb") as f: + file_bytes = f.read() + with NEXRADLevel2File(file_bytes) as fh: + assert fh.is_compressed + with pytest.raises(ValueError, match="recnum=134"): + fh.init_record(200) + + +def test_init_record_non_sequential_uncached_raises(nexradlevel2_bzfile): + """init_record(N) past a sequential walk, with N uncached, must raise (#376).""" + with open(nexradlevel2_bzfile, "rb") as f: + file_bytes = f.read() + with NEXRADLevel2File(file_bytes) as fh: + fh.init_record(134) + fh.init_next_record() # populates cache for 135 + with pytest.raises(ValueError, match="non-sequential"): + fh.init_record(9999) # never visited, not in cache + + +def test_init_record_past_last_ldm_returns_false(nexradlevel2_bzfile): + """init_record past the last LDM returns False (EOF semantics) (#376).""" + with open(nexradlevel2_bzfile, "rb") as f: + file_bytes = f.read() + with NEXRADLevel2File(file_bytes) as fh: + # Walk to the very end so subsequent advances run out of LDMs. + rec = 134 + while fh.init_record(rec): + rec += 1 + # Next sequential advance must report EOF, not raise. + assert fh.init_record(rec) is False + + def test_nexradlevel2_missing_msg2_metadata(): """ Test backward compatibility when msg_2 metadata is missing. From ecb190974846611235318e0c59007891de228dd5 Mon Sep 17 00:00:00 2001 From: aladinor Date: Sun, 3 May 2026 22:21:30 -0500 Subject: [PATCH 4/7] TST: cover EOF paths in _load_ldm and metadata-only files --- tests/io/test_nexrad_level2.py | 41 +++++++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/tests/io/test_nexrad_level2.py b/tests/io/test_nexrad_level2.py index 72901ab6..c537e538 100644 --- a/tests/io/test_nexrad_level2.py +++ b/tests/io/test_nexrad_level2.py @@ -8,7 +8,7 @@ import os import warnings from collections import OrderedDict -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock, PropertyMock, patch import numpy as np import pytest @@ -1022,6 +1022,45 @@ def test_init_record_past_last_ldm_returns_false(nexradlevel2_bzfile): assert fh.init_record(rec) is False +def test_load_ldm_past_end_returns_false(nexradlevel2_bzfile): + """_load_ldm reports EOF when the requested LDM index is past the last (#376).""" + with open(nexradlevel2_bzfile, "rb") as f: + file_bytes = f.read() + with NEXRADLevel2File(file_bytes) as fh: + assert fh._load_ldm(len(fh.bz2_record_indices)) is False + + +def test_init_record_metadata_propagates_ldm_load_failure(nexradlevel2_bzfile): + """Branch A propagates _load_ldm failure when no LDM is available (#376).""" + with open(nexradlevel2_bzfile, "rb") as f: + file_bytes = f.read() + with NEXRADLevel2File(file_bytes) as fh: + # Empty the LDM index so even LDM 0 fails to load. + with patch.object( + type(fh), + "bz2_record_indices", + new_callable=PropertyMock, + return_value=np.array([], dtype=int), + ): + assert fh.init_record(0) is False + + +def test_first_compressed_call_with_metadata_only_returns_false(nexradlevel2_bzfile): + """First compressed init_record returns False when no data LDM exists (#376).""" + with open(nexradlevel2_bzfile, "rb") as f: + file_bytes = f.read() + with NEXRADLevel2File(file_bytes) as fh: + # Truncate the LDM list to just the metadata LDM (index 0). + only_meta = fh.bz2_record_indices[:1] + with patch.object( + type(fh), + "bz2_record_indices", + new_callable=PropertyMock, + return_value=only_meta, + ): + assert fh.init_record(134) is False + + def test_nexradlevel2_missing_msg2_metadata(): """ Test backward compatibility when msg_2 metadata is missing. From 21fa5fb96016ddd3ad33dac7274fe2af445b6a4b Mon Sep 17 00:00:00 2001 From: aladinor Date: Fri, 8 May 2026 10:22:30 -0500 Subject: [PATCH 5/7] TST: add KILX regression test pinning #376 fix Address @kmuehlbauer's review on #377. KILX20230629_154426_V06 has landed in open-radar-data 0.8.0 (openradar/open-radar-data#98), so we can now ship a targeted regression test against the exact file that exposed the bug. LDM 49 of this file contains 120 MSG_31 + 2 MSG_2 = 122 messages. Pre-fix the mod-120 stride dropped the trailing 2 MSG_31s, so sweep_10 reported 358 rays (on-wire-correct is 360) and the volume total was 6838 instead of 6840. - Add ``nexradlevel2_kilx_ldm_stride_file`` session fixture in ``tests/conftest.py``. - Add ``test_kilx_over_120_ldm_decodes_all_msg31`` asserting per-sweep ray counts == [720]*6 + [360]*7, total == 6840, and sweep_10 == 360 against the file. - Bump open-radar-data minimum version to 0.8.0 across ``requirements_dev.txt``, ``environment.yml``, ``ci/unittests.yml``, and ``ci/notebooktests.yml``. --- ci/notebooktests.yml | 2 +- ci/unittests.yml | 2 +- environment.yml | 2 +- requirements_dev.txt | 2 +- tests/conftest.py | 11 +++++++++++ tests/io/test_nexrad_level2.py | 17 +++++++++++++++++ 6 files changed, 32 insertions(+), 4 deletions(-) diff --git a/ci/notebooktests.yml b/ci/notebooktests.yml index 0ee020be..77df211e 100644 --- a/ci/notebooktests.yml +++ b/ci/notebooktests.yml @@ -18,7 +18,7 @@ dependencies: - netCDF4 - notebook - numpy - - open-radar-data>=0.7.0 + - open-radar-data>=0.8.0 - pip - pyproj - pytest diff --git a/ci/unittests.yml b/ci/unittests.yml index 4f6807d8..f47c989e 100644 --- a/ci/unittests.yml +++ b/ci/unittests.yml @@ -14,7 +14,7 @@ dependencies: - lat_lon_parser - netCDF4 - numpy - - open-radar-data>=0.7.0 + - open-radar-data>=0.8.0 - pip - pyproj - pytest diff --git a/environment.yml b/environment.yml index b6fcda93..e328cae1 100644 --- a/environment.yml +++ b/environment.yml @@ -16,7 +16,7 @@ dependencies: - h5py - lat_lon_parser - netCDF4 - - open-radar-data>=0.7.0 + - open-radar-data>=0.8.0 - pyproj - pip: - xarray >= 2026.4.0 diff --git a/requirements_dev.txt b/requirements_dev.txt index 6ba6a7c5..34b99134 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -11,7 +11,7 @@ fsspec ruff nbconvert notebook -open_radar_data>=0.7.0 +open_radar_data>=0.8.0 boto3 cartopy s3fs diff --git a/tests/conftest.py b/tests/conftest.py index d76b510b..cf07796f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,6 +83,17 @@ def nexradlevel2_file(): return DATASETS.fetch("KATX20130717_195021_V06") +@pytest.fixture(scope="session") +def nexradlevel2_kilx_ldm_stride_file(): + """KILX volume with an over-120-message LDM (#376 regression fixture). + + LDM 49 of this file contains 120 MSG_31 + 2 MSG_2 = 122 messages. + Pre-fix, xradar's mod-120 stride dropped the trailing 2 MSG_31s, + yielding sweep_10 = 358 (on-wire is 360). + """ + return DATASETS.fetch("KILX20230629_154426_V06") + + @pytest.fixture(scope="session") def nexrad_chunks_klot(tmp_path_factory): import tarfile diff --git a/tests/io/test_nexrad_level2.py b/tests/io/test_nexrad_level2.py index c537e538..686a2c41 100644 --- a/tests/io/test_nexrad_level2.py +++ b/tests/io/test_nexrad_level2.py @@ -1061,6 +1061,23 @@ def test_first_compressed_call_with_metadata_only_returns_false(nexradlevel2_bzf assert fh.init_record(134) is False +def test_kilx_over_120_ldm_decodes_all_msg31(nexradlevel2_kilx_ldm_stride_file): + """KILX file regression for #376. + + LDM 49 has 122 messages (120 MSG_31 + 2 MSG_2). Pre-fix, xradar's + mod-120 stride dropped the trailing 2 MSG_31s; sweep_10 reported 358 + instead of the on-wire-correct 360. This pins the fix end-to-end. + """ + with NEXRADLevel2File(nexradlevel2_kilx_ldm_stride_file, loaddata=False) as nex: + nex.get_data_header() + per_sweep = [len(s) for s in nex.msg_31_header] + + # 13 sweeps total: 6 super-res (720 rays) + 7 standard (360 rays). + assert per_sweep == [720] * 6 + [360] * 7 + assert sum(per_sweep) == 6840 + assert per_sweep[10] == 360 # the formerly-truncated sweep + + def test_nexradlevel2_missing_msg2_metadata(): """ Test backward compatibility when msg_2 metadata is missing. From 3aff30fae5b6a1587453d7c50367d066abd2195e Mon Sep 17 00:00:00 2001 From: aladinor Date: Fri, 8 May 2026 10:48:55 -0500 Subject: [PATCH 6/7] TST: simplify #376 regression test per audit feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop redundant nex.get_data_header() call — msg_31_header property auto-triggers it (xradar/io/backends/nexrad_level2.py:616-625) - Rename fixture/test to drop the kilx site prefix; the LDM stride is the salient property, not the ICAO - Trim redundant sum() and per_sweep[10] assertions — list-equality subsumes them and pytest's diff already pinpoints any regressed index - Drop "the formerly-truncated sweep" inline comment already covered by the docstring --- tests/conftest.py | 2 +- tests/io/test_nexrad_level2.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index cf07796f..eb35b8c9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -84,7 +84,7 @@ def nexradlevel2_file(): @pytest.fixture(scope="session") -def nexradlevel2_kilx_ldm_stride_file(): +def nexradlevel2_ldm_stride_file(): """KILX volume with an over-120-message LDM (#376 regression fixture). LDM 49 of this file contains 120 MSG_31 + 2 MSG_2 = 122 messages. diff --git a/tests/io/test_nexrad_level2.py b/tests/io/test_nexrad_level2.py index 686a2c41..936d9f47 100644 --- a/tests/io/test_nexrad_level2.py +++ b/tests/io/test_nexrad_level2.py @@ -1061,21 +1061,19 @@ def test_first_compressed_call_with_metadata_only_returns_false(nexradlevel2_bzf assert fh.init_record(134) is False -def test_kilx_over_120_ldm_decodes_all_msg31(nexradlevel2_kilx_ldm_stride_file): +def test_ldm_stride_decodes_all_msg31(nexradlevel2_ldm_stride_file): """KILX file regression for #376. LDM 49 has 122 messages (120 MSG_31 + 2 MSG_2). Pre-fix, xradar's mod-120 stride dropped the trailing 2 MSG_31s; sweep_10 reported 358 instead of the on-wire-correct 360. This pins the fix end-to-end. """ - with NEXRADLevel2File(nexradlevel2_kilx_ldm_stride_file, loaddata=False) as nex: - nex.get_data_header() + with NEXRADLevel2File(nexradlevel2_ldm_stride_file, loaddata=False) as nex: per_sweep = [len(s) for s in nex.msg_31_header] # 13 sweeps total: 6 super-res (720 rays) + 7 standard (360 rays). + # Pre-fix, sweep_10 was 358 — the list-equality diff pinpoints the regression. assert per_sweep == [720] * 6 + [360] * 7 - assert sum(per_sweep) == 6840 - assert per_sweep[10] == 360 # the formerly-truncated sweep def test_nexradlevel2_missing_msg2_metadata(): From d97f9b37bcbb2be4aefbf993a9be7ef016f67bb8 Mon Sep 17 00:00:00 2001 From: aladinor Date: Fri, 8 May 2026 11:57:39 -0500 Subject: [PATCH 7/7] CI: bump open-radar-data >=0.8.0 in xarray-nightly job MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The xarray-nightly job in .github/workflows/ci.yml hard-codes its own conda create-args list (rather than reading from ci/unittests.yml), so the earlier 0.7.0 → 0.8.0 bump didn't reach it. The job loaded an older open-radar-data without the KILX fixture, causing test_ldm_stride_decodes_all_msg31 to error in the registry lookup. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cbc27f9..3188caa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -176,7 +176,7 @@ jobs: h5py lat_lon_parser netCDF4 - open-radar-data>=0.7.0 + open-radar-data>=0.8.0 packaging pandas pip