diff --git a/src/abovepy/_mosaic.py b/src/abovepy/_mosaic.py index 2a451ed..e0c9bd2 100644 --- a/src/abovepy/_mosaic.py +++ b/src/abovepy/_mosaic.py @@ -103,7 +103,17 @@ def _build_vrt( Path Path to the created VRT file. """ - from osgeo import gdal + try: + from osgeo import gdal + except ImportError as exc: + from abovepy._exceptions import MosaicError + + raise MosaicError( + "VRT construction requires the GDAL Python bindings, which are not " + "installed by default. Install with `conda install -c conda-forge gdal` " + "(recommended) or `pip install gdal` (requires matching system GDAL headers). " + "Alternatively, pass an output path ending in `.tif` to merge via rasterio." + ) from exc gdal.UseExceptions() diff --git a/tests/test_integration.py b/tests/test_integration.py index b75271f..9bb4e4e 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -36,8 +36,8 @@ def test_search_dem_phase3_frankfort(self, frankfort_bbox): """Search returns DEM tiles for the Frankfort area.""" tiles = abovepy.search(bbox=frankfort_bbox, product="dem_phase3", max_items=10) assert len(tiles) > 0 - assert "tile_id" in tiles.columns - assert "asset_url" in tiles.columns + assert "tile_id" in tiles.tiles.columns + assert "asset_url" in tiles.tiles.columns def test_search_by_county(self): """County-based search returns results.""" @@ -59,7 +59,7 @@ def test_asset_urls_are_accessible(self, frankfort_bbox): import httpx tiles = abovepy.search(bbox=frankfort_bbox, product="dem_phase3", max_items=1) - url = tiles.iloc[0]["asset_url"] + url = tiles.tiles.iloc[0]["asset_url"] resp = httpx.head(url, follow_redirects=True, timeout=30) assert resp.status_code == 200 @@ -90,7 +90,7 @@ def test_dem_products(self, frankfort_bbox, product): """DEM products return tiles for Frankfort area.""" tiles = abovepy.search(bbox=frankfort_bbox, product=product, max_items=3) assert len(tiles) > 0 - assert tiles.iloc[0]["product"] == product + assert tiles.tiles.iloc[0]["product"] == product @pytest.mark.parametrize( "product", @@ -113,9 +113,11 @@ def test_ortho_products(self, frankfort_bbox, product): "laz_phase3", ], ) - def test_laz_products(self, frankfort_bbox, product): - """LiDAR products return tiles for Frankfort area.""" - tiles = abovepy.search(bbox=frankfort_bbox, product=product, max_items=3) + def test_laz_products(self, pike_county_bbox, product): + """LiDAR products return tiles. Uses Pike County because Phase 1 LAZ + does not cover Franklin County (Frankfort); Phase 1 was flown + county-by-county 2010-2017 and Franklin was not in that batch.""" + tiles = abovepy.search(bbox=pike_county_bbox, product=product, max_items=3) assert len(tiles) > 0 @@ -198,7 +200,7 @@ class TestLiveRead: def test_read_cog_windowed(self, frankfort_bbox): """Read a real tile with a windowed bbox.""" tiles = abovepy.search(bbox=frankfort_bbox, product="dem_phase3", max_items=1) - url = tiles.iloc[0]["asset_url"] + url = tiles.tiles.iloc[0]["asset_url"] data, profile = abovepy.read(url, bbox=frankfort_bbox) assert data.shape[0] >= 1 assert profile["crs"] is not None @@ -207,7 +209,7 @@ def test_read_cog_windowed(self, frankfort_bbox): def test_read_full_tile(self, frankfort_bbox): """Read a full tile without bbox clipping.""" tiles = abovepy.search(bbox=frankfort_bbox, product="dem_phase3", max_items=1) - url = tiles.iloc[0]["asset_url"] + url = tiles.tiles.iloc[0]["asset_url"] data, profile = abovepy.read(url) assert data.shape[1] > 0 assert data.shape[2] > 0 @@ -216,7 +218,7 @@ def test_read_full_tile(self, frankfort_bbox): def test_read_returns_epsg3089(self, frankfort_bbox): """Read tile CRS should be EPSG:3089.""" tiles = abovepy.search(bbox=frankfort_bbox, product="dem_phase3", max_items=1) - url = tiles.iloc[0]["asset_url"] + url = tiles.tiles.iloc[0]["asset_url"] _, profile = abovepy.read(url) crs_str = str(profile["crs"]) assert "3089" in crs_str @@ -253,6 +255,10 @@ def test_download_skip_existing(self, frankfort_bbox): @pytest.mark.slow def test_mosaic_vrt(self, frankfort_bbox): """Download 2 tiles, mosaic to VRT, verify it's readable.""" + pytest.importorskip( + "osgeo", + reason="VRT construction requires GDAL Python bindings (not a default runtime dep).", + ) tiles = abovepy.search(bbox=frankfort_bbox, product="dem_phase3", max_items=2) if len(tiles) < 2: pytest.skip("Need at least 2 tiles for mosaic test") @@ -308,6 +314,6 @@ def test_laz_tile_url_accessible(self, frankfort_bbox): tiles = abovepy.search(bbox=frankfort_bbox, product="laz_phase2", max_items=1) if tiles.empty: pytest.skip("No COPC tiles found in Frankfort area") - url = tiles.iloc[0]["asset_url"] + url = tiles.tiles.iloc[0]["asset_url"] resp = httpx.head(url, follow_redirects=True, timeout=30) assert resp.status_code == 200