From a729837f2d66de041ed8f3d8325485035a574be6 Mon Sep 17 00:00:00 2001 From: LindsayWiard Date: Tue, 25 Aug 2026 10:57:08 -0600 Subject: [PATCH 1/6] Add lfps_available to fbfm13 and fccs LANDFIRE_VERSIONS --- services/lib/lib/landfire/config.py | 2 ++ services/lib/tests/test_landfire_config.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/services/lib/lib/landfire/config.py b/services/lib/lib/landfire/config.py index 7fe65c7e..332b99ed 100644 --- a/services/lib/lib/landfire/config.py +++ b/services/lib/lib/landfire/config.py @@ -17,6 +17,7 @@ LANDFIRE_VERSIONS: dict[str, dict[str, list[str] | str]] = { "fbfm13": { "available": ["2023", "2024"], + "lfps_available": ["2025"], "default": "2024", }, "fbfm40": { @@ -26,6 +27,7 @@ }, "fccs": { "available": ["2023"], + "lfps_available": ["2025"], "default": "2023", }, } diff --git a/services/lib/tests/test_landfire_config.py b/services/lib/tests/test_landfire_config.py index 2b28ce30..69372d63 100644 --- a/services/lib/tests/test_landfire_config.py +++ b/services/lib/tests/test_landfire_config.py @@ -22,6 +22,7 @@ def test_default_is_always_in_available(self): def test_fbfm13_versions(self): assert LANDFIRE_VERSIONS["fbfm13"]["available"] == ["2023", "2024"] + assert LANDFIRE_VERSIONS["fbfm13"]["lfps_available"] == ["2025"] assert LANDFIRE_VERSIONS["fbfm13"]["default"] == "2024" def test_fbfm40_versions(self): @@ -37,6 +38,7 @@ def test_fbfm40_versions(self): def test_fccs_versions(self): assert LANDFIRE_VERSIONS["fccs"]["available"] == ["2023"] + assert LANDFIRE_VERSIONS["fccs"]["lfps_available"] == ["2025"] assert LANDFIRE_VERSIONS["fccs"]["default"] == "2023" From dd89863f5f07cf1f21a40af4100f73aa04abf422 Mon Sep 17 00:00:00 2001 From: LindsayWiard Date: Tue, 25 Aug 2026 11:00:15 -0600 Subject: [PATCH 2/6] Route fuel models through LFPS for lfps_available versions --- services/griddle/griddle/dispatch.py | 2 + services/griddle/griddle/handlers/landfire.py | 83 +++++++++---- .../griddle/tests/handlers/test_landfire.py | 115 +++++++++++++++++- services/griddle/tests/test_dispatch.py | 3 + 4 files changed, 178 insertions(+), 25 deletions(-) diff --git a/services/griddle/griddle/dispatch.py b/services/griddle/griddle/dispatch.py index 7b113429..6648cc4a 100644 --- a/services/griddle/griddle/dispatch.py +++ b/services/griddle/griddle/dispatch.py @@ -151,6 +151,7 @@ def handle_landfire( extent_buffer_cells=extent_buffer_cells, alignment=alignment, target_grid_doc=target_grid_doc, + progress=progress, ) case "fbfm40": version = source.get("version", LANDFIRE_VERSIONS["fbfm40"]["default"]) @@ -178,6 +179,7 @@ def handle_landfire( extent_buffer_cells=extent_buffer_cells, alignment=alignment, target_grid_doc=target_grid_doc, + progress=progress, ) case "topography": version = source.get("version", "2020") diff --git a/services/griddle/griddle/handlers/landfire.py b/services/griddle/griddle/handlers/landfire.py index 75e9b333..71ade3e4 100644 --- a/services/griddle/griddle/handlers/landfire.py +++ b/services/griddle/griddle/handlers/landfire.py @@ -54,6 +54,11 @@ def _landfire_cog_url(product: str, version: str) -> str: return f"gs://{RASTERS_BUCKET}/LF{version}_{product}_CONUS.tif" +def _needs_lfps(product: str, version: str) -> bool: + """Whether `version` is only served on-demand via LFPS, not as a staged COG.""" + return version in LANDFIRE_VERSIONS[product].get("lfps_available", ()) + + def _fetch_landfire_raster( roi: gpd.GeoDataFrame, url: str, @@ -145,6 +150,7 @@ def fetch_fbfm13( extent_buffer_cells: int = 0, alignment: dict | None = None, target_grid_doc: dict | None = None, + progress: Callable[[str, int | None], None] | None = None, ) -> xr.Dataset: """Fetch LANDFIRE FBFM13 fuel model codes. @@ -159,23 +165,38 @@ def fetch_fbfm13( ``{"target": "domain"}`` when omitted. target_grid_doc: Loaded grid document used when ``alignment["target"] == "grid"``. + progress: Progress callback, only used when `version` is fetched via + LFPS (submit/wait/download reports through it). Returns: Dataset with a single "fbfm13" variable (int16 categorical codes, 1-13 plus non-burnable 91/92/93/98/99) """ - product = "fbfm13" - validate_landfire_version(product, version) alignment = alignment or {"target": "domain"} - url = _landfire_cog_url(product, version) - data = _fetch_landfire_raster( - roi, - url, - extent_buffer_cells, - alignment, - target_grid_doc, - is_categorical=True, - ) + product = "fbfm13" + if _needs_lfps(product, version): + source = landfire_lfps.fetch_lfps( + roi, + product, + version, + alignment, + target_grid_doc, + extent_buffer_cells, + progress, + ) + else: + validate_landfire_version(product, version) + source = nullcontext(_landfire_cog_url(product, version)) + + with source as url: + data = _fetch_landfire_raster( + roi, + url, + extent_buffer_cells, + alignment, + target_grid_doc, + is_categorical=True, + ) if remove_non_burnable: non_burnable_keys = [NB_CODE_MAP[code] for code in remove_non_burnable] @@ -218,7 +239,7 @@ def fetch_fbfm40( """ alignment = alignment or {"target": "domain"} product = "fbfm40" - if season is not None: + if season is not None or _needs_lfps(product, version): source = landfire_lfps.fetch_lfps( roi, product, @@ -259,6 +280,7 @@ def fetch_fccs( extent_buffer_cells: int = 0, alignment: dict | None = None, target_grid_doc: dict | None = None, + progress: Callable[[str, int | None], None] | None = None, ) -> xr.Dataset: """Fetch LANDFIRE FCCS fuel model codes. @@ -273,22 +295,37 @@ def fetch_fccs( ``{"target": "domain"}`` when omitted. target_grid_doc: Loaded grid document used when ``alignment["target"] == "grid"``. + progress: Progress callback, only used when `version` is fetched via + LFPS (submit/wait/download reports through it). Returns: Dataset with a single "fccs" variable (int32 categorical codes) """ - product = "fccs" - validate_landfire_version(product, version) alignment = alignment or {"target": "domain"} - url = _landfire_cog_url(product, version) - data = _fetch_landfire_raster( - roi, - url, - extent_buffer_cells, - alignment, - target_grid_doc, - is_categorical=True, - ) + product = "fccs" + if _needs_lfps(product, version): + source = landfire_lfps.fetch_lfps( + roi, + product, + version, + alignment, + target_grid_doc, + extent_buffer_cells, + progress, + ) + else: + validate_landfire_version(product, version) + source = nullcontext(_landfire_cog_url(product, version)) + + with source as url: + data = _fetch_landfire_raster( + roi, + url, + extent_buffer_cells, + alignment, + target_grid_doc, + is_categorical=True, + ) if remove_bare_ground: filtered = _remove_non_burnable_blocks(data.values, [0]) diff --git a/services/griddle/tests/handlers/test_landfire.py b/services/griddle/tests/handlers/test_landfire.py index 9373efcf..2e116c06 100644 --- a/services/griddle/tests/handlers/test_landfire.py +++ b/services/griddle/tests/handlers/test_landfire.py @@ -15,6 +15,7 @@ LANDFIRE_EXTRA_NODATA, _fetch_landfire_raster, _most_frequent, + _needs_lfps, _remove_non_burnable_blocks, fetch_fbfm13, fetch_fbfm40, @@ -184,6 +185,21 @@ def test_direction_only_extra_sentinel_cells_change(self, mock_raster_cls, roi): np.testing.assert_array_equal(result.values, [[55, 55], [55, 55]]) +class TestNeedsLfps: + """Unit tests for _needs_lfps.""" + + def test_staged_version_does_not_need_lfps(self): + assert _needs_lfps("fbfm40", "2024") is False + + def test_lfps_only_version_needs_lfps(self): + assert _needs_lfps("fbfm40", "2025") is True + + def test_product_without_lfps_available_key_returns_false(self): + """Falls back cleanly if a product's config has no lfps_available key + at all, rather than KeyError.""" + assert _needs_lfps("fbfm13", "1999") is False + + class TestFetchFbfm13: """Integration tests for fetch_fbfm13.""" @@ -220,6 +236,43 @@ def test_fbfm13_values_in_valid_set(self, roi): assert set(np.unique(values)).issubset(valid_codes) +class TestFetchFbfm13LfpsPath: + """Unit tests for fetch_fbfm13's LFPS branch -- routing on lfps_available versions.""" + + @patch("griddle.handlers.landfire._fetch_landfire_raster") + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") + def test_lfps_available_version_routes_through_lfps( + self, mock_fetch_lfps, mock_fetch_raster, roi + ): + mock_cm = MagicMock() + mock_cm.__enter__.return_value = "/tmp/lfps_xyz/result.tif" + mock_fetch_lfps.return_value = mock_cm + mock_fetch_raster.return_value = _make_canopy_raster( + np.zeros((4, 4), dtype=np.int16) + ) + progress = MagicMock() + + result = fetch_fbfm13(roi, version="2025", progress=progress) + + mock_fetch_lfps.assert_called_once_with( + roi, "fbfm13", "2025", {"target": "domain"}, None, 0, progress + ) + mock_fetch_raster.assert_called_once_with( + roi, + "/tmp/lfps_xyz/result.tif", + 0, + {"target": "domain"}, + None, + is_categorical=True, + ) + assert "fbfm13" in result.data_vars + + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") + def test_staged_version_does_not_call_lfps(self, mock_fetch_lfps, roi): + fetch_fbfm13(roi, version="2024") + mock_fetch_lfps.assert_not_called() + + class TestFetchFbfm40: """Integration tests for fetch_fbfm40.""" @@ -308,8 +361,9 @@ def test_fbfm_values_in_range(self, roi): assert result["fbfm"].values.max() <= 204 -class TestFetchFbfm40SeasonalPath: - """Unit tests for fetch_fbfm40's season branch -- routing to LFPS.""" +class TestFetchFbfm40LfpsPath: + """Unit tests for fetch_fbfm40's LFPS branch -- routing to LFPS for a + season, an lfps_available version, or both.""" @patch("griddle.handlers.landfire._fetch_landfire_raster") @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") @@ -337,6 +391,26 @@ def test_season_routes_through_lfps(self, mock_fetch_lfps, mock_fetch_raster, ro ) assert "fbfm" in result.data_vars + @patch("griddle.handlers.landfire._fetch_landfire_raster") + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") + def test_lfps_available_version_without_season_routes_through_lfps( + self, mock_fetch_lfps, mock_fetch_raster, roi + ): + mock_cm = MagicMock() + mock_cm.__enter__.return_value = "/tmp/lfps_xyz/result.tif" + mock_fetch_lfps.return_value = mock_cm + mock_fetch_raster.return_value = _make_canopy_raster( + np.zeros((4, 4), dtype=np.int16) + ) + progress = MagicMock() + + result = fetch_fbfm40(roi, version="2025", progress=progress) + + mock_fetch_lfps.assert_called_once_with( + roi, "fbfm40", "2025", {"target": "domain"}, None, 0, progress, None + ) + assert "fbfm" in result.data_vars + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") def test_no_season_does_not_call_lfps(self, mock_fetch_lfps, roi): fetch_fbfm40(roi, version="2024") @@ -403,6 +477,43 @@ def test_remove_bare_ground_false_by_default(self, roi): ) +class TestFetchFccsLfpsPath: + """Unit tests for fetch_fccs's LFPS branch -- routing on lfps_available versions.""" + + @patch("griddle.handlers.landfire._fetch_landfire_raster") + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") + def test_lfps_available_version_routes_through_lfps( + self, mock_fetch_lfps, mock_fetch_raster, roi + ): + mock_cm = MagicMock() + mock_cm.__enter__.return_value = "/tmp/lfps_xyz/result.tif" + mock_fetch_lfps.return_value = mock_cm + mock_fetch_raster.return_value = _make_canopy_raster( + np.zeros((4, 4), dtype=np.int16) + ) + progress = MagicMock() + + result = fetch_fccs(roi, version="2025", progress=progress) + + mock_fetch_lfps.assert_called_once_with( + roi, "fccs", "2025", {"target": "domain"}, None, 0, progress + ) + mock_fetch_raster.assert_called_once_with( + roi, + "/tmp/lfps_xyz/result.tif", + 0, + {"target": "domain"}, + None, + is_categorical=True, + ) + assert "fccs" in result.data_vars + + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") + def test_staged_version_does_not_call_lfps(self, mock_fetch_lfps, roi): + fetch_fccs(roi, version="2023") + mock_fetch_lfps.assert_not_called() + + class TestFetchTopography: """Integration tests for fetch_topography.""" diff --git a/services/griddle/tests/test_dispatch.py b/services/griddle/tests/test_dispatch.py index 4dc7c4b5..38c35da8 100644 --- a/services/griddle/tests/test_dispatch.py +++ b/services/griddle/tests/test_dispatch.py @@ -58,6 +58,7 @@ def test_routes_fbfm13_to_handler(self, mock_fetch): extent_buffer_cells=0, alignment={"target": "domain"}, target_grid_doc=None, + progress=progress, ) assert result == mock_result @@ -173,6 +174,7 @@ def test_routes_fccs_to_handler(self, mock_fetch): extent_buffer_cells=0, alignment={"target": "domain"}, target_grid_doc=None, + progress=progress, ) assert result == mock_result @@ -195,6 +197,7 @@ def test_routes_fccs_with_remove_bare_ground(self, mock_fetch): extent_buffer_cells=0, alignment={"target": "domain"}, target_grid_doc=None, + progress=progress, ) assert result == mock_result From 2e6bb55de483b3b0f21929421f987ba46c64d503 Mon Sep 17 00:00:00 2001 From: LindsayWiard Date: Tue, 25 Aug 2026 17:00:48 -0600 Subject: [PATCH 3/6] Add integration tests for annual LFPS fetch --- .../tests/integration/test_landfire_lfps.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/services/griddle/tests/integration/test_landfire_lfps.py b/services/griddle/tests/integration/test_landfire_lfps.py index 20a26ce1..af7edd7c 100644 --- a/services/griddle/tests/integration/test_landfire_lfps.py +++ b/services/griddle/tests/integration/test_landfire_lfps.py @@ -12,7 +12,7 @@ import pytest from griddle.handlers import landfire -from lib.landfire import list_products +from lib.landfire import LANDFIRE_VERSIONS, list_products from lib.testing import SHARED_TEST_DOMAINS_DIR @@ -72,3 +72,28 @@ def test_seasonal_fbfm40(roi): fbfm_valid = _assert_valid_data(ds, "fbfm") assert fbfm_valid.max() <= 204 # matches test_landfire.py's FBFM40 range check + + +@pytest.mark.parametrize( + ("product", "fetch_fn", "band"), + [ + ("fbfm13", landfire.fetch_fbfm13, "fbfm13"), + ("fbfm40", landfire.fetch_fbfm40, "fbfm"), + ("fccs", landfire.fetch_fccs, "fccs"), + ], +) +def test_annual_lfps_available_version(roi, product, fetch_fn, band): + """Submit a real annual (non-seasonal) LFPS job for each product's + configured lfps_available version -- confirms LANDFIRE Product Service + is actually serving what our config says it should.""" + lfps_versions = LANDFIRE_VERSIONS[product].get("lfps_available", []) + if not lfps_versions: + pytest.skip(f"No lfps_available version configured for {product}.") + + ds = fetch_fn(roi, version=lfps_versions[0], progress=lambda *a, **k: None) + + assert band in ds.data_vars + assert ds[band].dims == ("y", "x") + assert ds[band].rio.nodata is not None + assert ds.rio.height > 0 + assert ds.rio.width > 0 From f98e2a1ae6c19e73bec6685b70b0a57eefaa8fb5 Mon Sep 17 00:00:00 2001 From: LindsayWiard Date: Wed, 26 Aug 2026 11:23:38 -0600 Subject: [PATCH 4/6] Fix TypeError when progress is omitted on an LFPS-routed fetch --- services/griddle/griddle/handlers/landfire.py | 10 +++- .../griddle/tests/handlers/test_landfire.py | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/services/griddle/griddle/handlers/landfire.py b/services/griddle/griddle/handlers/landfire.py index 71ade3e4..eef77402 100644 --- a/services/griddle/griddle/handlers/landfire.py +++ b/services/griddle/griddle/handlers/landfire.py @@ -143,6 +143,10 @@ def _to_dataset(variables: dict[str, DataArray]) -> xr.Dataset: return ds +def _default_progress(message: str, pct: int | None = None) -> None: + """No-op progress callback used when a caller doesn't provide one.""" + + def fetch_fbfm13( roi: gpd.GeoDataFrame, version: str = LANDFIRE_VERSIONS["fbfm13"]["default"], @@ -182,7 +186,7 @@ def fetch_fbfm13( alignment, target_grid_doc, extent_buffer_cells, - progress, + progress or _default_progress, ) else: validate_landfire_version(product, version) @@ -247,7 +251,7 @@ def fetch_fbfm40( alignment, target_grid_doc, extent_buffer_cells, - progress, + progress or _default_progress, season, ) else: @@ -311,7 +315,7 @@ def fetch_fccs( alignment, target_grid_doc, extent_buffer_cells, - progress, + progress or _default_progress, ) else: validate_landfire_version(product, version) diff --git a/services/griddle/tests/handlers/test_landfire.py b/services/griddle/tests/handlers/test_landfire.py index 2e116c06..6f565688 100644 --- a/services/griddle/tests/handlers/test_landfire.py +++ b/services/griddle/tests/handlers/test_landfire.py @@ -267,6 +267,25 @@ def test_lfps_available_version_routes_through_lfps( ) assert "fbfm13" in result.data_vars + @patch("griddle.handlers.landfire._fetch_landfire_raster") + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") + def test_lfps_available_version_without_progress_does_not_raise( + self, mock_fetch_lfps, mock_fetch_raster, roi + ): + """Omitting progress must not crash -- fetch_lfps calls it unconditionally.""" + mock_cm = MagicMock() + mock_cm.__enter__.return_value = "/tmp/lfps_xyz/result.tif" + mock_fetch_lfps.return_value = mock_cm + mock_fetch_raster.return_value = _make_canopy_raster( + np.zeros((4, 4), dtype=np.int16) + ) + + result = fetch_fbfm13(roi, version="2025") # no progress passed + + assert "fbfm13" in result.data_vars + # the substituted callback must actually be callable + mock_fetch_lfps.call_args[0][6]("test", 0) + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") def test_staged_version_does_not_call_lfps(self, mock_fetch_lfps, roi): fetch_fbfm13(roi, version="2024") @@ -411,6 +430,25 @@ def test_lfps_available_version_without_season_routes_through_lfps( ) assert "fbfm" in result.data_vars + @patch("griddle.handlers.landfire._fetch_landfire_raster") + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") + def test_lfps_available_version_without_progress_does_not_raise( + self, mock_fetch_lfps, mock_fetch_raster, roi + ): + """Omitting progress must not crash -- fetch_lfps calls it unconditionally.""" + mock_cm = MagicMock() + mock_cm.__enter__.return_value = "/tmp/lfps_xyz/result.tif" + mock_fetch_lfps.return_value = mock_cm + mock_fetch_raster.return_value = _make_canopy_raster( + np.zeros((4, 4), dtype=np.int16) + ) + + result = fetch_fbfm40(roi, version="2025") # no progress passed + + assert "fbfm" in result.data_vars + # the substituted callback must actually be callable + mock_fetch_lfps.call_args[0][6]("test", 0) + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") def test_no_season_does_not_call_lfps(self, mock_fetch_lfps, roi): fetch_fbfm40(roi, version="2024") @@ -508,6 +546,25 @@ def test_lfps_available_version_routes_through_lfps( ) assert "fccs" in result.data_vars + @patch("griddle.handlers.landfire._fetch_landfire_raster") + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") + def test_lfps_available_version_without_progress_does_not_raise( + self, mock_fetch_lfps, mock_fetch_raster, roi + ): + """Omitting progress must not crash -- fetch_lfps calls it unconditionally.""" + mock_cm = MagicMock() + mock_cm.__enter__.return_value = "/tmp/lfps_xyz/result.tif" + mock_fetch_lfps.return_value = mock_cm + mock_fetch_raster.return_value = _make_canopy_raster( + np.zeros((4, 4), dtype=np.int16) + ) + + result = fetch_fccs(roi, version="2025") # no progress passed + + assert "fccs" in result.data_vars + # the substituted callback must actually be callable + mock_fetch_lfps.call_args[0][6]("test", 0) + @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") def test_staged_version_does_not_call_lfps(self, mock_fetch_lfps, roi): fetch_fccs(roi, version="2023") From 6e4c6bed36992a2b6318c9997811eb34a9122f07 Mon Sep 17 00:00:00 2001 From: LindsayWiard Date: Wed, 26 Aug 2026 11:52:38 -0600 Subject: [PATCH 5/6] Give progress a no-op default instead of None --- services/griddle/griddle/handlers/landfire.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/services/griddle/griddle/handlers/landfire.py b/services/griddle/griddle/handlers/landfire.py index eef77402..b4da1ff2 100644 --- a/services/griddle/griddle/handlers/landfire.py +++ b/services/griddle/griddle/handlers/landfire.py @@ -154,7 +154,7 @@ def fetch_fbfm13( extent_buffer_cells: int = 0, alignment: dict | None = None, target_grid_doc: dict | None = None, - progress: Callable[[str, int | None], None] | None = None, + progress: Callable[[str, int | None], None] = _default_progress, ) -> xr.Dataset: """Fetch LANDFIRE FBFM13 fuel model codes. @@ -186,7 +186,7 @@ def fetch_fbfm13( alignment, target_grid_doc, extent_buffer_cells, - progress or _default_progress, + progress, ) else: validate_landfire_version(product, version) @@ -217,7 +217,7 @@ def fetch_fbfm40( extent_buffer_cells: int = 0, alignment: dict | None = None, target_grid_doc: dict | None = None, - progress: Callable[[str, int | None], None] | None = None, + progress: Callable[[str, int | None], None] = _default_progress, season: str | None = None, ) -> xr.Dataset: """Fetch LANDFIRE FBFM40 fuel model codes. @@ -251,7 +251,7 @@ def fetch_fbfm40( alignment, target_grid_doc, extent_buffer_cells, - progress or _default_progress, + progress, season, ) else: @@ -284,7 +284,7 @@ def fetch_fccs( extent_buffer_cells: int = 0, alignment: dict | None = None, target_grid_doc: dict | None = None, - progress: Callable[[str, int | None], None] | None = None, + progress: Callable[[str, int | None], None] = _default_progress, ) -> xr.Dataset: """Fetch LANDFIRE FCCS fuel model codes. @@ -315,7 +315,7 @@ def fetch_fccs( alignment, target_grid_doc, extent_buffer_cells, - progress or _default_progress, + progress, ) else: validate_landfire_version(product, version) From 7a5ffe6019839eacc00d123e67e545625073deed Mon Sep 17 00:00:00 2001 From: LindsayWiard Date: Wed, 26 Aug 2026 13:44:29 -0600 Subject: [PATCH 6/6] Require progress as an argument on LFPS fetch functions --- services/griddle/griddle/dispatch.py | 6 +- services/griddle/griddle/handlers/landfire.py | 10 +- .../tests/handlers/test_alignment_handlers.py | 17 +- .../griddle/tests/handlers/test_landfire.py | 179 +++++++++--------- services/griddle/tests/test_dispatch.py | 16 +- 5 files changed, 121 insertions(+), 107 deletions(-) diff --git a/services/griddle/griddle/dispatch.py b/services/griddle/griddle/dispatch.py index 6648cc4a..3865e989 100644 --- a/services/griddle/griddle/dispatch.py +++ b/services/griddle/griddle/dispatch.py @@ -146,12 +146,12 @@ def handle_landfire( remove_non_burnable = source.get("remove_non_burnable") return landfire.fetch_fbfm13( domain_gdf, + progress, version, remove_non_burnable=remove_non_burnable, extent_buffer_cells=extent_buffer_cells, alignment=alignment, target_grid_doc=target_grid_doc, - progress=progress, ) case "fbfm40": version = source.get("version", LANDFIRE_VERSIONS["fbfm40"]["default"]) @@ -160,13 +160,13 @@ def handle_landfire( season = source.get("season") return landfire.fetch_fbfm40( domain_gdf, + progress, version, remove_non_burnable=remove_non_burnable, extent_buffer_cells=extent_buffer_cells, alignment=alignment, target_grid_doc=target_grid_doc, season=season, - progress=progress, ) case "fccs": version = source.get("version", LANDFIRE_VERSIONS["fccs"]["default"]) @@ -174,12 +174,12 @@ def handle_landfire( remove_bare_ground = source.get("remove_bare_ground", False) return landfire.fetch_fccs( domain_gdf, + progress, version, remove_bare_ground=remove_bare_ground, extent_buffer_cells=extent_buffer_cells, alignment=alignment, target_grid_doc=target_grid_doc, - progress=progress, ) case "topography": version = source.get("version", "2020") diff --git a/services/griddle/griddle/handlers/landfire.py b/services/griddle/griddle/handlers/landfire.py index b4da1ff2..21a3270e 100644 --- a/services/griddle/griddle/handlers/landfire.py +++ b/services/griddle/griddle/handlers/landfire.py @@ -143,18 +143,14 @@ def _to_dataset(variables: dict[str, DataArray]) -> xr.Dataset: return ds -def _default_progress(message: str, pct: int | None = None) -> None: - """No-op progress callback used when a caller doesn't provide one.""" - - def fetch_fbfm13( roi: gpd.GeoDataFrame, + progress: Callable[[str, int | None], None], version: str = LANDFIRE_VERSIONS["fbfm13"]["default"], remove_non_burnable: list[str] | None = None, extent_buffer_cells: int = 0, alignment: dict | None = None, target_grid_doc: dict | None = None, - progress: Callable[[str, int | None], None] = _default_progress, ) -> xr.Dataset: """Fetch LANDFIRE FBFM13 fuel model codes. @@ -212,12 +208,12 @@ def fetch_fbfm13( def fetch_fbfm40( roi: gpd.GeoDataFrame, + progress: Callable[[str, int | None], None], version: str = LANDFIRE_VERSIONS["fbfm40"]["default"], remove_non_burnable: list[str] | None = None, extent_buffer_cells: int = 0, alignment: dict | None = None, target_grid_doc: dict | None = None, - progress: Callable[[str, int | None], None] = _default_progress, season: str | None = None, ) -> xr.Dataset: """Fetch LANDFIRE FBFM40 fuel model codes. @@ -279,12 +275,12 @@ def fetch_fbfm40( def fetch_fccs( roi: gpd.GeoDataFrame, + progress: Callable[[str, int | None], None], version: str = LANDFIRE_VERSIONS["fccs"]["default"], remove_bare_ground: bool = False, extent_buffer_cells: int = 0, alignment: dict | None = None, target_grid_doc: dict | None = None, - progress: Callable[[str, int | None], None] = _default_progress, ) -> xr.Dataset: """Fetch LANDFIRE FCCS fuel model codes. diff --git a/services/griddle/tests/handlers/test_alignment_handlers.py b/services/griddle/tests/handlers/test_alignment_handlers.py index 011e82c8..6f7d89a3 100644 --- a/services/griddle/tests/handlers/test_alignment_handlers.py +++ b/services/griddle/tests/handlers/test_alignment_handlers.py @@ -81,9 +81,11 @@ class TestLandfireAlignmentDomain: def test_domain_target_passes_destination_kwargs(self, mock_cls): mock_cls.return_value = _mock_raster() roi = _domain_gdf() + progress = MagicMock() fetch_fbfm40( roi, + progress, version="2024", alignment={"target": "domain", "resolution": 2.0}, ) @@ -103,8 +105,9 @@ def test_domain_target_passes_destination_kwargs(self, mock_cls): def test_domain_target_default_uses_source_native_resolution(self, mock_cls): mock_cls.return_value = _mock_raster(source_resolution=30.0) roi = _domain_gdf() + progress = MagicMock() - fetch_fbfm40(roi, version="2024", alignment={"target": "domain"}) + fetch_fbfm40(roi, progress, version="2024", alignment={"target": "domain"}) kwargs = mock_cls.return_value.extract_window.call_args[1] transform = kwargs["destination_transform"] @@ -122,8 +125,9 @@ def test_domain_target_default_uses_source_native_in_roi_crs_units(self, mock_cl native_in_roi_resolution=30.0, ) roi = _domain_gdf() + progress = MagicMock() - fetch_fbfm40(roi, version="2024", alignment={"target": "domain"}) + fetch_fbfm40(roi, progress, version="2024", alignment={"target": "domain"}) kwargs = mock_cls.return_value.extract_window.call_args[1] transform = kwargs["destination_transform"] @@ -140,8 +144,9 @@ class TestLandfireAlignmentNative: def test_native_target_no_resolution_passes_no_destination(self, mock_cls): mock_cls.return_value = _mock_raster() roi = _domain_gdf() + progress = MagicMock() - fetch_fbfm40(roi, version="2024", alignment={"target": "native"}) + fetch_fbfm40(roi, progress, version="2024", alignment={"target": "native"}) kwargs = mock_cls.return_value.extract_window.call_args[1] assert "destination_transform" not in kwargs @@ -154,9 +159,11 @@ def test_native_target_no_resolution_passes_no_destination(self, mock_cls): def test_native_target_with_resolution_passes_resolution(self, mock_cls): mock_cls.return_value = _mock_raster() roi = _domain_gdf() + progress = MagicMock() fetch_fbfm40( roi, + progress, version="2024", alignment={"target": "native", "resolution": 5.0}, ) @@ -173,6 +180,7 @@ class TestLandfireAlignmentGrid: def test_grid_target_exact_match(self, mock_cls): mock_cls.return_value = _mock_raster() roi = _domain_gdf() + progress = MagicMock() target_grid_doc = { "georeference": { "crs": "EPSG:32611", @@ -183,6 +191,7 @@ def test_grid_target_exact_match(self, mock_cls): fetch_fbfm40( roi, + progress, version="2024", alignment={"target": "grid", "grid_id": "x"}, target_grid_doc=target_grid_doc, @@ -199,6 +208,7 @@ def test_grid_target_exact_match(self, mock_cls): def test_grid_target_with_resolution_recomputes_shape(self, mock_cls): mock_cls.return_value = _mock_raster() roi = _domain_gdf() + progress = MagicMock() # Target grid: 30m cells, 10x10, anchored lower-left at (720100, 5190200). target_grid_doc = { "georeference": { @@ -210,6 +220,7 @@ def test_grid_target_with_resolution_recomputes_shape(self, mock_cls): fetch_fbfm40( roi, + progress, version="2024", alignment={"target": "grid", "grid_id": "x", "resolution": 1.0}, target_grid_doc=target_grid_doc, diff --git a/services/griddle/tests/handlers/test_landfire.py b/services/griddle/tests/handlers/test_landfire.py index 6f565688..e243069a 100644 --- a/services/griddle/tests/handlers/test_landfire.py +++ b/services/griddle/tests/handlers/test_landfire.py @@ -205,32 +205,50 @@ class TestFetchFbfm13: def test_returns_dataset(self, roi): """fetch_fbfm13 returns a Dataset.""" - result = fetch_fbfm13(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm13( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert isinstance(result, xr.Dataset) def test_has_fbfm13_variable(self, roi): """Dataset contains an 'fbfm13' variable.""" - result = fetch_fbfm13(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm13( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert "fbfm13" in result.data_vars def test_fbfm13_shape(self, test_domain, roi): """The fbfm13 variable has the expected spatial shape.""" - result = fetch_fbfm13(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm13( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert result["fbfm13"].shape == test_domain.expected_shape def test_fbfm13_dtype(self, roi): """The fbfm13 variable is int16 (categorical codes).""" - result = fetch_fbfm13(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm13( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert result["fbfm13"].dtype == "int16" def test_crs_preserved(self, roi): """CRS is preserved via rioxarray.""" - result = fetch_fbfm13(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm13( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert result.rio.crs == roi.crs def test_fbfm13_values_in_valid_set(self, roi): """FBFM13 codes are limited to the Anderson 13 models plus non-burnable.""" - result = fetch_fbfm13(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm13( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) values = result["fbfm13"].values valid_codes = set(range(1, 14)) | {91, 92, 93, 98, 99} assert set(np.unique(values)).issubset(valid_codes) @@ -252,7 +270,7 @@ def test_lfps_available_version_routes_through_lfps( ) progress = MagicMock() - result = fetch_fbfm13(roi, version="2025", progress=progress) + result = fetch_fbfm13(roi, progress, version="2025") mock_fetch_lfps.assert_called_once_with( roi, "fbfm13", "2025", {"target": "domain"}, None, 0, progress @@ -267,28 +285,10 @@ def test_lfps_available_version_routes_through_lfps( ) assert "fbfm13" in result.data_vars - @patch("griddle.handlers.landfire._fetch_landfire_raster") - @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") - def test_lfps_available_version_without_progress_does_not_raise( - self, mock_fetch_lfps, mock_fetch_raster, roi - ): - """Omitting progress must not crash -- fetch_lfps calls it unconditionally.""" - mock_cm = MagicMock() - mock_cm.__enter__.return_value = "/tmp/lfps_xyz/result.tif" - mock_fetch_lfps.return_value = mock_cm - mock_fetch_raster.return_value = _make_canopy_raster( - np.zeros((4, 4), dtype=np.int16) - ) - - result = fetch_fbfm13(roi, version="2025") # no progress passed - - assert "fbfm13" in result.data_vars - # the substituted callback must actually be callable - mock_fetch_lfps.call_args[0][6]("test", 0) - @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") def test_staged_version_does_not_call_lfps(self, mock_fetch_lfps, roi): - fetch_fbfm13(roi, version="2024") + progress = MagicMock() + fetch_fbfm13(roi, progress, version="2024") mock_fetch_lfps.assert_not_called() @@ -351,32 +351,50 @@ def test_extent_buffer_cells_threaded_through(self, mock_raster_cls, roi, buffer def test_returns_dataset(self, roi): """fetch_fbfm40 returns a Dataset.""" - result = fetch_fbfm40(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm40( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert isinstance(result, xr.Dataset) def test_has_fbfm_variable(self, roi): """Dataset contains a 'fbfm' variable.""" - result = fetch_fbfm40(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm40( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert "fbfm" in result.data_vars def test_fbfm_shape(self, test_domain, roi): """The fbfm variable has the expected spatial shape.""" - result = fetch_fbfm40(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm40( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert result["fbfm"].shape == test_domain.expected_shape def test_fbfm_dtype(self, roi): """The fbfm variable is int16 (categorical codes).""" - result = fetch_fbfm40(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm40( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert result["fbfm"].dtype == "int16" def test_crs_preserved(self, roi): """CRS is preserved via rioxarray.""" - result = fetch_fbfm40(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm40( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert result.rio.crs == roi.crs def test_fbfm_values_in_range(self, roi): """FBFM40 codes should be <= 204.""" - result = fetch_fbfm40(roi=roi, version="2024", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fbfm40( + roi=roi, progress=progress, version="2024", extent_buffer_cells=8 + ) assert result["fbfm"].values.max() <= 204 @@ -395,7 +413,7 @@ def test_season_routes_through_lfps(self, mock_fetch_lfps, mock_fetch_raster, ro ) progress = MagicMock() - result = fetch_fbfm40(roi, version="2025", season="SP", progress=progress) + result = fetch_fbfm40(roi, progress, version="2025", season="SP") mock_fetch_lfps.assert_called_once_with( roi, "fbfm40", "2025", {"target": "domain"}, None, 0, progress, "SP" @@ -423,35 +441,17 @@ def test_lfps_available_version_without_season_routes_through_lfps( ) progress = MagicMock() - result = fetch_fbfm40(roi, version="2025", progress=progress) + result = fetch_fbfm40(roi, progress, version="2025") mock_fetch_lfps.assert_called_once_with( roi, "fbfm40", "2025", {"target": "domain"}, None, 0, progress, None ) assert "fbfm" in result.data_vars - @patch("griddle.handlers.landfire._fetch_landfire_raster") - @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") - def test_lfps_available_version_without_progress_does_not_raise( - self, mock_fetch_lfps, mock_fetch_raster, roi - ): - """Omitting progress must not crash -- fetch_lfps calls it unconditionally.""" - mock_cm = MagicMock() - mock_cm.__enter__.return_value = "/tmp/lfps_xyz/result.tif" - mock_fetch_lfps.return_value = mock_cm - mock_fetch_raster.return_value = _make_canopy_raster( - np.zeros((4, 4), dtype=np.int16) - ) - - result = fetch_fbfm40(roi, version="2025") # no progress passed - - assert "fbfm" in result.data_vars - # the substituted callback must actually be callable - mock_fetch_lfps.call_args[0][6]("test", 0) - @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") def test_no_season_does_not_call_lfps(self, mock_fetch_lfps, roi): - fetch_fbfm40(roi, version="2024") + progress = MagicMock() + fetch_fbfm40(roi, progress, version="2024") mock_fetch_lfps.assert_not_called() @@ -460,32 +460,50 @@ class TestFetchFccs: def test_returns_dataset(self, roi): """fetch_fccs returns a Dataset.""" - result = fetch_fccs(roi=roi, version="2023", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fccs( + roi=roi, progress=progress, version="2023", extent_buffer_cells=8 + ) assert isinstance(result, xr.Dataset) def test_has_fccs_variable(self, roi): """Dataset contains a 'fccs' variable.""" - result = fetch_fccs(roi=roi, version="2023", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fccs( + roi=roi, progress=progress, version="2023", extent_buffer_cells=8 + ) assert "fccs" in result.data_vars def test_fccs_shape(self, test_domain, roi): """The fccs variable has the expected spatial shape.""" - result = fetch_fccs(roi=roi, version="2023", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fccs( + roi=roi, progress=progress, version="2023", extent_buffer_cells=8 + ) assert result["fccs"].shape == test_domain.expected_shape def test_fccs_dtype(self, roi): """The fccs variable is int32 (codes up to 12990133 exceed int16 range).""" - result = fetch_fccs(roi=roi, version="2023", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fccs( + roi=roi, progress=progress, version="2023", extent_buffer_cells=8 + ) assert result["fccs"].dtype == "int32" def test_crs_preserved(self, roi): """CRS is preserved via rioxarray.""" - result = fetch_fccs(roi=roi, version="2023", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fccs( + roi=roi, progress=progress, version="2023", extent_buffer_cells=8 + ) assert result.rio.crs == roi.crs def test_fccs_valid_values_in_range(self, roi): """Mapped FCCS codes should be between 0 and 12990133.""" - result = fetch_fccs(roi=roi, version="2023", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fccs( + roi=roi, progress=progress, version="2023", extent_buffer_cells=8 + ) values = result["fccs"].values valid_mask = ~np.isin(values, [-1111, -9999]) assert values[valid_mask].min() >= 0 @@ -493,22 +511,29 @@ def test_fccs_valid_values_in_range(self, roi): def test_fccs_fill_values_are_expected(self, roi): """Any negative values are only the known fill values (-1111, -9999).""" - result = fetch_fccs(roi=roi, version="2023", extent_buffer_cells=8) + progress = MagicMock() + result = fetch_fccs( + roi=roi, progress=progress, version="2023", extent_buffer_cells=8 + ) values = result["fccs"].values negative_values = np.unique(values[values < 0]) assert set(negative_values).issubset({-1111, -9999}) def test_remove_bare_ground_removes_zeros(self, roi): """When remove_bare_ground=True, no bare ground cells (code 0) remain.""" - result = fetch_fccs(roi=roi, remove_bare_ground=True) + progress = MagicMock() + result = fetch_fccs(roi=roi, progress=progress, remove_bare_ground=True) values = result["fccs"].values valid_mask = ~np.isin(values, [-1111, -9999]) assert not np.any(values[valid_mask] == 0) def test_remove_bare_ground_false_by_default(self, roi): """remove_bare_ground defaults to False and does not alter the data.""" - default_result = fetch_fccs(roi=roi) - explicit_result = fetch_fccs(roi=roi, remove_bare_ground=False) + progress = MagicMock() + default_result = fetch_fccs(roi=roi, progress=progress) + explicit_result = fetch_fccs( + roi=roi, progress=progress, remove_bare_ground=False + ) np.testing.assert_array_equal( default_result["fccs"].values, explicit_result["fccs"].values, @@ -531,7 +556,7 @@ def test_lfps_available_version_routes_through_lfps( ) progress = MagicMock() - result = fetch_fccs(roi, version="2025", progress=progress) + result = fetch_fccs(roi, progress, version="2025") mock_fetch_lfps.assert_called_once_with( roi, "fccs", "2025", {"target": "domain"}, None, 0, progress @@ -546,28 +571,10 @@ def test_lfps_available_version_routes_through_lfps( ) assert "fccs" in result.data_vars - @patch("griddle.handlers.landfire._fetch_landfire_raster") - @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") - def test_lfps_available_version_without_progress_does_not_raise( - self, mock_fetch_lfps, mock_fetch_raster, roi - ): - """Omitting progress must not crash -- fetch_lfps calls it unconditionally.""" - mock_cm = MagicMock() - mock_cm.__enter__.return_value = "/tmp/lfps_xyz/result.tif" - mock_fetch_lfps.return_value = mock_cm - mock_fetch_raster.return_value = _make_canopy_raster( - np.zeros((4, 4), dtype=np.int16) - ) - - result = fetch_fccs(roi, version="2025") # no progress passed - - assert "fccs" in result.data_vars - # the substituted callback must actually be callable - mock_fetch_lfps.call_args[0][6]("test", 0) - @patch("griddle.handlers.landfire.landfire_lfps.fetch_lfps") def test_staged_version_does_not_call_lfps(self, mock_fetch_lfps, roi): - fetch_fccs(roi, version="2023") + progress = MagicMock() + fetch_fccs(roi, progress, version="2023") mock_fetch_lfps.assert_not_called() diff --git a/services/griddle/tests/test_dispatch.py b/services/griddle/tests/test_dispatch.py index 38c35da8..2bcb6794 100644 --- a/services/griddle/tests/test_dispatch.py +++ b/services/griddle/tests/test_dispatch.py @@ -53,12 +53,12 @@ def test_routes_fbfm13_to_handler(self, mock_fetch): mock_fetch.assert_called_once_with( mock_gdf, + progress, "2023", remove_non_burnable=None, extent_buffer_cells=0, alignment={"target": "domain"}, target_grid_doc=None, - progress=progress, ) assert result == mock_result @@ -73,7 +73,7 @@ def test_fbfm13_default_version(self, mock_fetch): handle_landfire(mock_gdf, source, progress) _, call_kwargs = mock_fetch.call_args - assert call_kwargs == {} or mock_fetch.call_args[0][1] == "2024" + assert call_kwargs == {} or mock_fetch.call_args[0][2] == "2024" @patch("griddle.dispatch.landfire.fetch_fbfm13") def test_fbfm13_calls_progress_callback(self, mock_fetch): @@ -104,13 +104,13 @@ def test_routes_fbfm40_to_handler(self, mock_fetch): mock_fetch.assert_called_once_with( mock_gdf, + progress, "2022", remove_non_burnable=None, extent_buffer_cells=0, alignment={"target": "domain"}, target_grid_doc=None, season=None, - progress=progress, ) assert result == mock_result @@ -124,7 +124,7 @@ def test_fbfm40_default_version(self, mock_fetch): handle_landfire(mock_gdf, source, progress) - assert mock_fetch.call_args[0][1] == "2024" + assert mock_fetch.call_args[0][2] == "2024" @patch("griddle.dispatch.landfire.fetch_fbfm40") def test_fbfm40_calls_progress_callback(self, mock_fetch): @@ -153,7 +153,7 @@ def test_fbfm40_threads_season_and_progress(self, mock_fetch): call_kwargs = mock_fetch.call_args[1] assert call_kwargs["season"] == "SP" - assert call_kwargs["progress"] is progress + assert mock_fetch.call_args[0][1] is progress @patch("griddle.dispatch.landfire.fetch_fccs") def test_routes_fccs_to_handler(self, mock_fetch): @@ -169,12 +169,12 @@ def test_routes_fccs_to_handler(self, mock_fetch): mock_fetch.assert_called_once_with( mock_gdf, + progress, "2023", remove_bare_ground=False, extent_buffer_cells=0, alignment={"target": "domain"}, target_grid_doc=None, - progress=progress, ) assert result == mock_result @@ -192,12 +192,12 @@ def test_routes_fccs_with_remove_bare_ground(self, mock_fetch): mock_fetch.assert_called_once_with( mock_gdf, + progress, "2023", remove_bare_ground=True, extent_buffer_cells=0, alignment={"target": "domain"}, target_grid_doc=None, - progress=progress, ) assert result == mock_result @@ -213,7 +213,7 @@ def test_fccs_default_version(self, mock_fetch): handle_landfire(mock_gdf, source, progress) call_args = mock_fetch.call_args[0] - assert call_args[1] == "2023" + assert call_args[2] == "2023" @patch("griddle.dispatch.landfire.fetch_fccs") def test_fccs_calls_progress(self, mock_fetch):