Skip to content

Commit 9824cf4

Browse files
committed
fix: empty gauge_ids bug, add CorruptedDataError, re-extract 7 countries, update README
- Fix get_watersheds(country, []) returning all rows instead of empty (falsy empty list in _store._read_geo filter condition) - Add CorruptedDataError for ArrowInvalid parquet files - Re-extract australia, chile, germany, lithuania, norway, poland, south_africa through fixed hydra-shed with GDAL geometry repair - Add make_valid() safety net to convert_to_parquet.py - Fix south_africa RegionSpec key in convert_to_parquet.py - Update README gauge counts from live R2 (60,141 total across 16 countries) - 4 new unit tests (80 total)
1 parent 8c6c563 commit 9824cf4

8 files changed

Lines changed: 83 additions & 28 deletions

File tree

README.md

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ wr.available_countries()
6060

6161
# List gauge IDs for a country
6262
wr.available_gauges("portugal")
63-
# -> ['02G-02H', '02O-01H', ..., '16J-01H'] (73 gauges)
63+
# -> ['02G-02H', '02O-01H', ..., '16J-01H'] (~710 gauges)
6464
```
6565

6666
### Single Watershed
@@ -78,7 +78,7 @@ watershed, rivers = result
7878

7979
```python
8080
# All watersheds for a country
81-
gdf = wr.get_watersheds("portugal") # -> GeoDataFrame (73 rows)
81+
gdf = wr.get_watersheds("portugal") # -> GeoDataFrame (~710 rows)
8282

8383
# Subset by gauge IDs
8484
gdf = wr.get_watersheds("portugal", ["04K/04A", "05G/01A"])
@@ -117,24 +117,24 @@ from watershed_retrieve import (
117117

118118
| Country | Gauges | Status |
119119
|---------|--------|--------|
120-
| Australia | ~5,200 | Available |
121-
| Brazil | ~4,300 | Available |
122-
| Canada | ~5,800 | Available |
123-
| Chile | ~500 | Available |
124-
| Czech Republic | ~400 | Available |
125-
| France | ~5,400 | Available |
126-
| Germany | ~500 | Available |
127-
| Japan | ~1,200 | Available |
128-
| Lithuania | ~70 | Available |
129-
| Norway | ~600 | Available |
130-
| Poland | ~600 | Available |
131-
| Portugal | 73 | Available |
132-
| Slovenia | ~200 | Available |
133-
| South Africa | ~900 | Available |
134-
| Spain | ~600 | Available |
135-
| UK (EA) | ~1,500 | Pending — MERIT-Hydro coverage gap |
136-
| UK (NRFA) | ~1,500 | Pending — MERIT-Hydro coverage gap |
137-
| USA | ~32,000 | Available |
120+
| Australia | ~6,200 | Available |
121+
| Brazil | ~4,600 | Available |
122+
| Canada | ~7,600 | Available |
123+
| Chile | ~540 | Available |
124+
| Czech Republic | ~830 | Available |
125+
| France | ~5,300 | Available |
126+
| Germany | ~190 | Available |
127+
| Japan | ~820 | Available |
128+
| Lithuania | ~100 | Available |
129+
| Norway | ~4,500 | Available |
130+
| Poland | ~1,300 | Available |
131+
| Portugal | ~710 | Available |
132+
| Slovenia | ~710 | Available |
133+
| South Africa | ~1,290 | Available |
134+
| Spain | ~1,500 | Available |
135+
| UK (EA) | | Pending — MERIT-Hydro coverage gap |
136+
| UK (NRFA) | | Pending — MERIT-Hydro coverage gap |
137+
| USA | ~23,900 | Available |
138138

139139
## Development
140140

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "watershed-retrieve"
3-
version = "1.0.3"
3+
version = "1.0.4"
44
description = "Pre-delineated MERIT-Hydro watershed boundaries for ~60,000 gauging stations across 16 countries"
55
readme = "README.md"
66
authors = [{ name = "Nicolas Lazaro", email = "nlazaro@ethz.ch" }]
@@ -88,7 +88,7 @@ markers = [
8888
# Bump My Version
8989
# -----------------------
9090
[tool.bumpversion]
91-
current_version = "1.0.3"
91+
current_version = "1.0.4"
9292
commit = false
9393
tag = false
9494
allow_dirty = true

scripts/convert_to_parquet.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class RegionSpec(NamedTuple):
3737
RegionSpec("poland", "poland"),
3838
RegionSpec("portugal", "portugal"),
3939
RegionSpec("slovenia", "slovenia"),
40-
RegionSpec("south", "south_africa"),
40+
RegionSpec("south_africa", "south_africa"),
4141
RegionSpec("spain", "spain"),
4242
RegionSpec("uk_ea", "uk_ea"),
4343
RegionSpec("uk_nrfa", "uk_nrfa"),
@@ -82,6 +82,11 @@ def convert(input_dir: Path, output_dir: Path) -> None:
8282
t0 = time.time()
8383
print(f"Reading {region.key}.gpkg layer={layer} ...", flush=True)
8484
gdf = gpd.read_file(path, layer=layer)
85+
invalid_mask = ~gdf.geometry.is_valid
86+
n_invalid = invalid_mask.sum()
87+
if n_invalid > 0:
88+
print(f" Repairing {n_invalid} invalid geometries ...", flush=True)
89+
gdf.geometry = gdf.make_valid()
8590
nrows = len(gdf)
8691

8792
if region.key != region.output_name and "country" in gdf.columns:

src/watershed_retrieve/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = "1.0.3"
1+
__version__ = "1.0.4"
22

33
from ._api import (
44
available_countries,
@@ -10,6 +10,7 @@
1010
get_watersheds_with_rivers,
1111
)
1212
from ._errors import (
13+
CorruptedDataError,
1314
CountryNotFoundError,
1415
DataNotFoundError,
1516
DataUnavailableError,
@@ -29,6 +30,7 @@
2930
"get_watersheds",
3031
"get_watersheds_with_rivers",
3132
"Backend",
33+
"CorruptedDataError",
3234
"CountryNotFoundError",
3335
"DataNotFoundError",
3436
"DataUnavailableError",

src/watershed_retrieve/_errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ class DataUnavailableError(WatershedRetrieveError):
2323

2424
class R2ConnectionError(WatershedRetrieveError):
2525
pass
26+
27+
28+
class CorruptedDataError(WatershedRetrieveError):
29+
pass

src/watershed_retrieve/_store.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
import geopandas as gpd
88
import pandas as pd
99
from pyarrow.fs import FSSpecHandler, PyFileSystem
10+
from pyarrow.lib import ArrowInvalid
1011

11-
from ._errors import DataNotFoundError, R2ConnectionError
12+
from ._errors import CorruptedDataError, DataNotFoundError, R2ConnectionError
1213
from ._registry import CountryInfo
1314
from ._types import CompositeGaugeId
1415

@@ -51,9 +52,19 @@ def _read_geo(
5152
path: Path,
5253
gauge_ids: list[CompositeGaugeId] | None,
5354
) -> gpd.GeoDataFrame:
55+
if gauge_ids is not None and len(gauge_ids) == 0:
56+
return gpd.GeoDataFrame()
5457
self._ensure_exists(path)
55-
filters: list[tuple[str, str, list[str]]] | None = [("gauge_id", "in", gauge_ids)] if gauge_ids else None
56-
return gpd.read_parquet(path, filters=filters)
58+
filters: list[tuple[str, str, list[str]]] | None = (
59+
[("gauge_id", "in", gauge_ids)] if gauge_ids is not None else None
60+
)
61+
try:
62+
gdf = gpd.read_parquet(path, filters=filters)
63+
except ArrowInvalid as exc:
64+
raise CorruptedDataError(
65+
f"Data file appears corrupted: {path}. Try re-downloading or report this issue. Detail: {exc}"
66+
) from exc
67+
return gdf
5768

5869
def read_watersheds(
5970
self,
@@ -93,11 +104,19 @@ def _remote_path(self, country: CountryInfo, suffix: str) -> str:
93104
return f"{_R2_BASE_URL}/{country.file_stem}_{suffix}.parquet"
94105

95106
def _read_geo(self, url: str, gauge_ids: list[CompositeGaugeId] | None) -> gpd.GeoDataFrame:
96-
filters: list[tuple[str, str, list[str]]] | None = [("gauge_id", "in", gauge_ids)] if gauge_ids else None
107+
if gauge_ids is not None and len(gauge_ids) == 0:
108+
return gpd.GeoDataFrame()
109+
filters: list[tuple[str, str, list[str]]] | None = (
110+
[("gauge_id", "in", gauge_ids)] if gauge_ids is not None else None
111+
)
97112
try:
98113
return gpd.read_parquet(url, filesystem=self._fs, filters=filters)
99114
except FileNotFoundError:
100115
raise DataNotFoundError(f"Remote data file not found: {url}") from None
116+
except ArrowInvalid as exc:
117+
raise CorruptedDataError(
118+
f"Remote data file appears corrupted: {url}. This is a known issue — please report it. Detail: {exc}"
119+
) from exc
101120
except (OSError, ConnectionError) as exc:
102121
raise R2ConnectionError(f"Failed to fetch data from R2: {exc}") from exc
103122

tests/test_api_unit.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,8 @@ def test_missing_gauge_in_list_raises(self, fake_store: FakeWatershedStore) -> N
9494
def test_returns_geodataframe(self, fake_store: FakeWatershedStore) -> None:
9595
api_mod._store = fake_store
9696
assert isinstance(wr.get_watersheds("portugal"), gpd.GeoDataFrame)
97+
98+
def test_empty_gauge_ids_returns_empty(self, fake_store: FakeWatershedStore) -> None:
99+
api_mod._store = fake_store
100+
result = wr.get_watersheds("portugal", [])
101+
assert len(result) == 0

tests/test_local_store.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import geopandas as gpd
44
import pytest
55

6+
from watershed_retrieve import CorruptedDataError
67
from watershed_retrieve._errors import DataNotFoundError
78
from watershed_retrieve._registry import CountryInfo
89
from watershed_retrieve._store import LocalParquetStore
@@ -38,6 +39,11 @@ def test_missing_file_raises(self, tmp_path: Path) -> None:
3839
with pytest.raises(DataNotFoundError, match="Data file not found"):
3940
store.read_watersheds(PORTUGAL)
4041

42+
def test_empty_gauge_ids_returns_empty(self, synthetic_parquet_dir: Path) -> None:
43+
store = LocalParquetStore(synthetic_parquet_dir)
44+
result = store.read_watersheds(PORTUGAL, [])
45+
assert len(result) == 0
46+
4147

4248
class TestLocalParquetStoreReadRivers:
4349
def test_reads_all_rows(self, synthetic_parquet_dir: Path) -> None:
@@ -55,6 +61,11 @@ def test_filters_by_gauge_ids(self, synthetic_parquet_dir: Path) -> None:
5561
gdf = store.read_rivers(PORTUGAL, [CompositeGaugeId("portugal_G001")])
5662
assert len(gdf) == 1
5763

64+
def test_empty_gauge_ids_returns_empty(self, synthetic_parquet_dir: Path) -> None:
65+
store = LocalParquetStore(synthetic_parquet_dir)
66+
result = store.read_rivers(PORTUGAL, [])
67+
assert len(result) == 0
68+
5869

5970
class TestLocalParquetStoreReadGaugeIds:
6071
def test_returns_all_gauge_ids(self, synthetic_parquet_dir: Path) -> None:
@@ -71,3 +82,12 @@ def test_missing_file_raises(self, tmp_path: Path) -> None:
7182
store = LocalParquetStore(tmp_path)
7283
with pytest.raises(DataNotFoundError):
7384
store.read_gauge_ids(PORTUGAL)
85+
86+
87+
class TestCorruptedParquet:
88+
def test_corrupted_file_raises_corrupted_data_error(self, tmp_path: Path) -> None:
89+
store = LocalParquetStore(tmp_path)
90+
corrupt_path = tmp_path / "portugal_watersheds.parquet"
91+
corrupt_path.write_bytes(b"not a parquet file")
92+
with pytest.raises(CorruptedDataError, match="corrupted"):
93+
store.read_watersheds(PORTUGAL)

0 commit comments

Comments
 (0)