Skip to content

Commit 5a9943d

Browse files
authored
Merge branch 'main' into dependabot/pip/pytest-gte-9.1.1
2 parents b31efbc + 9d3e09e commit 5a9943d

5 files changed

Lines changed: 348 additions & 24 deletions

File tree

next_pass.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,11 @@ def create_parser() -> argparse.ArgumentParser:
134134
"in format YYYY-MM-DDTHH:MM"
135135
),
136136
)
137+
parser.add_argument(
138+
"--include-hls",
139+
action="store_true",
140+
help="Fetch and append corresponding source HLS scenes for OPERA HLS products.",
141+
)
137142
return parser
138143

139144

@@ -256,6 +261,7 @@ def run_next_pass(
256261
compute_tide: bool = False,
257262
products: List[str] | str | None = None,
258263
satellites: List[str] | str | None = None,
264+
include_hls: bool = False,
259265
):
260266
"""
261267
Programmatic entry point for next_pass.
@@ -286,6 +292,8 @@ def run_next_pass(
286292
cli_args.append("-c")
287293
if compute_tide:
288294
cli_args.append("-t")
295+
if include_hls:
296+
cli_args.append("--include-hls")
289297

290298
if date:
291299
cli_args += ["-d", date]
@@ -389,7 +397,10 @@ def main(cli_args: Any = None):
389397
timestamp_dir,
390398
)
391399
export_opera_products(
392-
results_opera, timestamp_dir, compute_cloudiness=args.cloudiness
400+
results_opera,
401+
timestamp_dir,
402+
compute_cloudiness=args.cloudiness,
403+
include_hls=getattr(args, "include_hls", False),
393404
)
394405
make_opera_granule_map(results_opera, args.bbox, timestamp_dir)
395406

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ yagmail
1212
openpyxl>=3.1.5
1313
timezonefinder
1414
pre-commit>=4.6.1
15-
black>=24.4.0
15+
black>=26.5.1
1616
isort>=8.0.1
1717
flake8>=7.0.0
1818
pytest>=9.1.1

tests/test_hls.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import earthaccess
2+
import pytest
3+
4+
from utils.opera_products import fetch_hls_granule_links
5+
6+
7+
@pytest.mark.integration
8+
def test_live_cmr_fallback_search():
9+
"""
10+
Hit the live CMR API to ensure earthaccess.search_data still accepts
11+
our specific combination of arguments without throwing an error.
12+
"""
13+
try:
14+
results = earthaccess.search_data(
15+
short_name=["HLSS30", "HLSL30"],
16+
temporal=("2023-01-01T00:00:00", "2023-01-01T23:59:59"),
17+
bounding_box=(-120.0, 30.0, -119.0, 31.0),
18+
)
19+
assert isinstance(results, list)
20+
except Exception as e:
21+
pytest.fail(f"Live CMR search failed! The API contract may have changed: {e}")
22+
23+
24+
@pytest.mark.integration
25+
def test_live_hls_granule_fetch():
26+
"""
27+
Hit the live CMR API to ensure our fetch_hls_granule_links function
28+
successfully queries and extracts data links for a known collection.
29+
"""
30+
known_granule_prefix = "HLS.S30.T11SLT.2023001"
31+
32+
try:
33+
links = fetch_hls_granule_links(known_granule_prefix)
34+
assert isinstance(links, list)
35+
except Exception as e:
36+
pytest.fail(f"Live CMR granule fetch failed: {e}")

tests/test_opera_products.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import zipfile
55
from datetime import datetime, timezone
6+
from unittest.mock import MagicMock, patch
67

78
import pytest
89

@@ -121,3 +122,135 @@ def test_export_opera_products_writes_workbook_and_skips_cloudiness_when_disable
121122
assert payload[0][0] == "Dataset"
122123
assert payload[1][1] == "granule-1"
123124
assert payload[1][5] == "https://example.com/file_B01_WTR.tif"
125+
126+
127+
@patch("utils.opera_products.earthaccess.search_data")
128+
def test_fetch_hls_granule_links(mock_search):
129+
# Setup mock successful response
130+
mock_result = MagicMock()
131+
mock_result.data_links.return_value = ["https://example.com/B04.tif"]
132+
mock_search.return_value = [mock_result]
133+
134+
# Test 1: Successful fetch
135+
links = opera_products.fetch_hls_granule_links("HLS.S30.T11SLT.20230101.v2.0")
136+
assert links == ["https://example.com/B04.tif"]
137+
mock_search.assert_called_with(
138+
short_name="HLSS30", granule_name="HLS.S30.T11SLT.20230101.v2.0"
139+
)
140+
141+
# Test 2: Error case (API failure)
142+
mock_search.side_effect = Exception("API Timeout")
143+
error_links = opera_products.fetch_hls_granule_links("HLS.S30.T11SLT.20230101.v2.0")
144+
assert error_links is None
145+
146+
147+
@patch("utils.opera_products.fetch_hls_granule_links")
148+
def test_export_hls_band_mapping_and_input_granules(mock_fetch_links, tmp_path):
149+
# Simulate an S30 and L30 response with their specific band names
150+
mock_fetch_links.side_effect = [
151+
[
152+
"https://fake/B02.tif",
153+
"https://fake/B03.tif",
154+
"https://fake/B04.tif",
155+
"https://fake/B8A.tif",
156+
"https://fake/Fmask.tif",
157+
], # S30 response
158+
[
159+
"https://fake/B02.tif",
160+
"https://fake/B03.tif",
161+
"https://fake/B04.tif",
162+
"https://fake/B05.tif",
163+
"https://fake/Fmask.tif",
164+
], # L30 response
165+
]
166+
167+
mock_results = {
168+
"OPERA_L3_DSWX-HLS_V1": {
169+
"results": [
170+
{
171+
"umm": {
172+
"GranuleUR": "OPERA_S30",
173+
"InputGranules": ["HLS.S30.T11SLT"],
174+
}
175+
},
176+
{
177+
"umm": {
178+
"GranuleUR": "OPERA_L30",
179+
"InputGranules": ["HLS.L30.T11SLT"],
180+
}
181+
},
182+
],
183+
"gdf": None, # Skipping geometry for simplicity
184+
}
185+
}
186+
187+
# Run the export function
188+
opera_products.export_opera_products(
189+
mock_results, tmp_path, compute_cloudiness=False, include_hls=True
190+
)
191+
192+
# Read the generated Excel file to verify mappings
193+
from openpyxl import load_workbook
194+
195+
wb = load_workbook(tmp_path / "opera_products_metadata.xlsx")
196+
ws = wb.active
197+
198+
# Row 2 is S30 (B8A for NIR)
199+
assert ws.cell(row=2, column=18).value == "https://fake/B04.tif" # Red
200+
assert ws.cell(row=2, column=21).value == "https://fake/B8A.tif" # NIR
201+
assert ws.cell(row=2, column=22).value == "https://fake/Fmask.tif"
202+
203+
# Row 3 is L30 (B05 for NIR)
204+
assert ws.cell(row=3, column=21).value == "https://fake/B05.tif" # NIR for Landsat
205+
206+
207+
@patch("utils.opera_products.earthaccess.search_data")
208+
def test_export_hls_fallback_cmr_search(mock_cmr_search, tmp_path):
209+
# Setup mock geometry and mock CMR response
210+
mock_geom = MagicMock()
211+
mock_geom.bounds = (-120, 30, -119, 31)
212+
mock_geom.wkt = "POLYGON((-120 30, -119 30, -119 31, -120 31, -120 30))"
213+
214+
mock_hls_result = MagicMock()
215+
mock_hls_result.get.return_value = {"GranuleUR": "HLS.S30.T11SLT.123"}
216+
mock_hls_result.data_links.return_value = ["https://fallback/B04.tif"]
217+
mock_cmr_search.return_value = [mock_hls_result]
218+
219+
# Provide results missing "InputGranules", forcing the fallback logic
220+
mock_results = {
221+
"OPERA_L3_DIST-ALERT-HLS_V1": {
222+
"results": [
223+
{
224+
"umm": {
225+
"GranuleUR": "OPERA_L3_DIST-ALERT-HLS_T11SLT_20230101",
226+
"TemporalExtent": {
227+
"RangeDateTime": {
228+
"BeginningDateTime": "2023-01-01T00:00:00Z"
229+
}
230+
},
231+
}
232+
}
233+
],
234+
"gdf": FakeFrame([{"geometry": mock_geom}]),
235+
}
236+
}
237+
238+
opera_products.export_opera_products(
239+
mock_results, tmp_path, compute_cloudiness=False, include_hls=True
240+
)
241+
242+
# Verify fallback search triggered with bounds and date
243+
mock_cmr_search.assert_called_once()
244+
kwargs = mock_cmr_search.call_args.kwargs
245+
assert kwargs["bounding_box"] == (-120, 30, -119, 31)
246+
assert kwargs["temporal"] == ("2023-01-01T00:00:00", "2023-01-01T23:59:59")
247+
248+
# Verify the fallback successfully mapped the data
249+
from openpyxl import load_workbook
250+
251+
wb = load_workbook(tmp_path / "opera_products_metadata.xlsx")
252+
ws = wb.active
253+
assert (
254+
ws.cell(row=2, column=17).value == "HLS.S30.T11SLT.123"
255+
) # Extracted Fallback ID
256+
assert ws.cell(row=2, column=18).value == "https://fallback/B04.tif"

0 commit comments

Comments
 (0)