From 696f6e0675d1520f2fc46cf2f4baa815f3bddc78 Mon Sep 17 00:00:00 2001 From: thodson-usgs Date: Sat, 30 May 2026 14:21:40 -0400 Subject: [PATCH] fix(nldi): return an empty GeoDataFrame instead of crashing on empty results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NLDI can legitimately return no features (e.g. a feature with nothing upstream), and _query_nldi returns {} when a 200 response carries no JSON body (per its own comment). The getters then called gpd.GeoDataFrame.from_features(..., crs=_CRS), which raises "Assigning CRS to a GeoDataFrame without a geometry column is not supported" — an opaque crash on a valid empty result. (as_json=True already tolerated empties; the default GeoDataFrame path did not.) Added a shared _features_to_gdf helper, used by all four getters, that returns an empty GeoDataFrame with the correct CRS when there are no features. Co-Authored-By: Claude Opus 4.8 (1M context) --- dataretrieval/nldi.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/dataretrieval/nldi.py b/dataretrieval/nldi.py index a483dc9e..8d61fcc2 100644 --- a/dataretrieval/nldi.py +++ b/dataretrieval/nldi.py @@ -32,6 +32,21 @@ def _query_nldi(url, query_params, error_message): return response_data +def _features_to_gdf(feature_collection: dict) -> gpd.GeoDataFrame: + """Build a GeoDataFrame from an NLDI FeatureCollection, tolerating empties. + + NLDI can legitimately return no features (e.g. a feature with nothing + upstream), and :func:`_query_nldi` returns ``{}`` when a 200 response + carries no JSON body. ``GeoDataFrame.from_features`` raises on those + (there's no geometry column to attach the CRS to), so return an empty + GeoDataFrame with the correct CRS instead of crashing. + """ + features = feature_collection.get("features") if feature_collection else None + if not features: + return gpd.GeoDataFrame(geometry=[], crs=_CRS) + return gpd.GeoDataFrame.from_features(feature_collection, crs=_CRS) + + def get_flowlines( navigation_mode: str, distance: int = 5, @@ -104,7 +119,7 @@ def get_flowlines( feature_collection = _query_nldi(url, query_params, err_msg) if as_json: return feature_collection - gdf = gpd.GeoDataFrame.from_features(feature_collection, crs=_CRS) + gdf = _features_to_gdf(feature_collection) return gdf @@ -157,7 +172,7 @@ def get_basin( feature_collection = _query_nldi(url, query_params, err_msg) if as_json: return feature_collection - gdf = gpd.GeoDataFrame.from_features(feature_collection, crs=_CRS) + gdf = _features_to_gdf(feature_collection) return gdf @@ -273,7 +288,7 @@ def get_features( feature_collection = _query_nldi(url, query_params, err_msg) if as_json: return feature_collection - gdf = gpd.GeoDataFrame.from_features(feature_collection, crs=_CRS) + gdf = _features_to_gdf(feature_collection) return gdf @@ -307,7 +322,7 @@ def get_features_by_data_source(data_source: str) -> gpd.GeoDataFrame: url = f"{NLDI_API_BASE_URL}/{data_source}" err_msg = f"Error getting features for data source '{data_source}'" feature_collection = _query_nldi(url, {}, err_msg) - gdf = gpd.GeoDataFrame.from_features(feature_collection, crs=_CRS) + gdf = _features_to_gdf(feature_collection) return gdf