Skip to content

Commit d22c2bf

Browse files
Refactor storage clients to use obstore
1 parent 447323c commit d22c2bf

13 files changed

Lines changed: 944 additions & 804 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ repos:
66
- id: end-of-file-fixer
77
- repo: https://github.com/charliermarsh/ruff-pre-commit
88
# keep the version here in sync with the version in uv.lock
9-
rev: "v0.12.7"
9+
rev: "v0.12.9"
1010
hooks:
1111
- id: ruff-check
1212
args: [--fix, --exit-non-zero-on-fix]

tilebox-storage/pyproject.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ dependencies = [
2626
"aiofile>=3.8",
2727
"folium>=0.15",
2828
"shapely>=2",
29-
"boto3>=1.33",
30-
"boto3-stubs[essential]>=1.33",
29+
"obstore>=0.8.0",
3130
]
3231

3332
[dependency-groups]
@@ -37,7 +36,6 @@ dev = [
3736
"pytest-asyncio>=0.24.0",
3837
"pytest-cov>=5.0.0",
3938
"pytest>=8.3.2",
40-
"moto>=5",
4139
]
4240

4341
[project.urls]

tilebox-storage/tests/storage_data.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@
1111

1212
from hypothesis.strategies import DrawFn, booleans, composite, datetimes, integers, just, one_of, text, uuids
1313

14-
from tilebox.storage.granule import ASFStorageGranule, CopernicusStorageGranule, UmbraStorageGranule
14+
from tilebox.storage.granule import (
15+
ASFStorageGranule,
16+
CopernicusStorageGranule,
17+
UmbraStorageGranule,
18+
USGSLandsatStorageGranule,
19+
)
1520
from tilebox.storage.providers import _ASF_URL, StorageURLs
1621

1722

@@ -82,3 +87,23 @@ def s5p_granules(draw: DrawFn) -> CopernicusStorageGranule:
8287

8388
file_size = draw(integers(min_value=10_000, max_value=999_999_999))
8489
return CopernicusStorageGranule(start, granule_name, location, file_size)
90+
91+
92+
@composite
93+
def landsat_granules(draw: DrawFn) -> USGSLandsatStorageGranule:
94+
"""Generate a realistic-looking random USGS Landsat granule."""
95+
time = draw(datetimes(min_value=datetime(1990, 1, 1), max_value=datetime(2025, 1, 1), timezones=just(None)))
96+
landsat_mission = draw(integers(min_value=1, max_value=9))
97+
98+
path = draw(integers(min_value=1, max_value=999))
99+
row = draw(integers(min_value=1, max_value=999))
100+
101+
granule_name = f"LC{landsat_mission:02d}_L1GT_{path:03d}{row:03d}_{time:%Y%m%d}_{time:%Y%m%d}_02_T1"
102+
location = f"s3://usgs-landsat/collection02/level-1/standard/oli-tirs/{time:%Y}/{path:03d}/{row:03d}/{granule_name}"
103+
thumbnail = draw(one_of(just(f"{granule_name}_thumb_small.jpeg"), just(None)))
104+
return USGSLandsatStorageGranule(
105+
time,
106+
granule_name,
107+
location,
108+
thumbnail,
109+
)

tilebox-storage/tests/test_granule.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@
44
from hypothesis import given
55
from hypothesis.strategies import lists
66

7-
from tests.storage_data import ers_granules, s5p_granules, umbra_granules
8-
from tilebox.storage.granule import ASFStorageGranule, CopernicusStorageGranule, UmbraStorageGranule, _asf_download_urls
7+
from tests.storage_data import ers_granules, landsat_granules, s5p_granules, umbra_granules
8+
from tilebox.storage.granule import (
9+
ASFStorageGranule,
10+
CopernicusStorageGranule,
11+
UmbraStorageGranule,
12+
USGSLandsatStorageGranule,
13+
_asf_download_urls,
14+
)
915

1016

1117
def _asf_granule_to_datapoint(granule: ASFStorageGranule) -> xr.Dataset:
@@ -101,3 +107,31 @@ def test_granule_from_copernicus_datapoints(granules: list[CopernicusStorageGran
101107

102108
for i in range(len(granules)): # converting a dataset with a time dimension of 1 should still work though
103109
assert CopernicusStorageGranule.from_data(dataset.isel(time=i)) == granules[i]
110+
111+
112+
def _landsat_granule_to_datapoint(granule: USGSLandsatStorageGranule) -> xr.Dataset:
113+
datapoint = xr.Dataset()
114+
datapoint.coords["time"] = np.array(granule.time).astype("datetime64[ns]")
115+
datapoint["granule_name"] = granule.granule_name
116+
datapoint["location"] = granule.location
117+
if granule.thumbnail is not None:
118+
datapoint["thumbnail"] = f"{granule.location}/{granule.thumbnail}"
119+
return datapoint
120+
121+
122+
@given(landsat_granules())
123+
def test_granule_from_landsat_datapoint(granule: USGSLandsatStorageGranule) -> None:
124+
datapoint = _landsat_granule_to_datapoint(granule)
125+
assert USGSLandsatStorageGranule.from_data(datapoint) == granule
126+
assert USGSLandsatStorageGranule.from_data(USGSLandsatStorageGranule.from_data(datapoint)) == granule
127+
128+
129+
@given(lists(landsat_granules(), min_size=2, max_size=5))
130+
def test_granule_from_landsat_datapoints(granules: list[USGSLandsatStorageGranule]) -> None:
131+
datapoints = [_landsat_granule_to_datapoint(granule) for granule in granules]
132+
dataset = xr.concat(datapoints, dim="time")
133+
with pytest.raises(ValueError, match=".*more than one granule.*"):
134+
USGSLandsatStorageGranule.from_data(dataset)
135+
136+
for i in range(len(granules)): # converting a dataset with a time dimension of 1 should still work though
137+
assert USGSLandsatStorageGranule.from_data(dataset.isel(time=i)) == granules[i]

tilebox-storage/tests/test_providers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from tilebox.storage.providers import _asf_login
66

77

8-
@pytest.mark.anyio
8+
@pytest.mark.asyncio
99
async def test_asf_login(httpx_mock: HTTPXMock) -> None:
1010
httpx_mock.add_response(headers={"Set-Cookie": "logged_in=yes"})
1111

@@ -15,8 +15,10 @@ async def test_asf_login(httpx_mock: HTTPXMock) -> None:
1515
assert isinstance(client.auth, BasicAuth)
1616
assert client.cookies["logged_in"] == "yes"
1717

18+
await client.aclose()
1819

19-
@pytest.mark.anyio
20+
21+
@pytest.mark.asyncio
2022
async def test_asf_login_invalid_auth(httpx_mock: HTTPXMock) -> None:
2123
httpx_mock.add_response(401)
2224
with pytest.raises(ValueError, match="Invalid username or password."):

0 commit comments

Comments
 (0)