Skip to content

Commit e42fa0b

Browse files
Fix copernicus storage client thumbnail URL resolver
1 parent dd1c32f commit e42fa0b

3 files changed

Lines changed: 58 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.51.0] - 2026-04-07
11+
1012
### Changed
1113

1214
- `tilebox-storage`: Replaced `httpx` with `niquests` for ASF HTTP downloads.
1315

16+
### Fixed
17+
18+
- `tilebox-storage`: Fixed an issue with the Copernicus storage client that prevented downloading granules pointing to the Copernicus OData thumbnail endpoint. (All granules ingested from March 2026 onwards).
19+
1420
## [0.50.1] - 2026-04-01
1521

1622
### Added
@@ -351,7 +357,8 @@ the first client that does not cache data (since it's already on the local file
351357
- Released under the [MIT](https://opensource.org/license/mit) license.
352358
- Released packages: `tilebox-datasets`, `tilebox-workflows`, `tilebox-storage`, `tilebox-grpc`
353359

354-
[Unreleased]: https://github.com/tilebox/tilebox-python/compare/v0.50.0...HEAD
360+
[Unreleased]: https://github.com/tilebox/tilebox-python/compare/v0.51.0...HEAD
361+
[0.51.0]: https://github.com/tilebox/tilebox-python/compare/v0.50.1...v0.51.0
355362
[0.50.1]: https://github.com/tilebox/tilebox-python/compare/v0.50.0...v0.50.1
356363
[0.50.0]: https://github.com/tilebox/tilebox-python/compare/v0.49.0...v0.50.0
357364
[0.49.0]: https://github.com/tilebox/tilebox-python/compare/v0.48.0...v0.49.0

tilebox-storage/tilebox/storage/aio.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
LocationStorageGranule,
2828
UmbraStorageGranule,
2929
USGSLandsatStorageGranule,
30+
_is_copernicus_odata_url,
3031
)
3132
from tilebox.storage.providers import login
3233

@@ -750,6 +751,22 @@ async def _download_quicklook(self, datapoint: xr.Dataset | CopernicusStorageGra
750751
else Path.cwd() / self._STORAGE_PROVIDER
751752
)
752753

754+
if _is_copernicus_odata_url(granule.thumbnail):
755+
# the thumbnail is not stored in the S3 bucket, but is accessible via a public URL. So download it
756+
# directly.
757+
response = await niquests.aget(
758+
granule.thumbnail, allow_redirects=True
759+
) # to check if the thumbnail is accessible, raises if not
760+
response.raise_for_status()
761+
content = response.content
762+
if content is None:
763+
raise ValueError("Received empty content when downloading quicklook.")
764+
765+
download_location = (output_folder / granule.granule_name).with_suffix(".jpg")
766+
download_location.parent.mkdir(parents=True, exist_ok=True)
767+
download_location.write_bytes(content)
768+
return download_location
769+
753770
await download_objects(self._store, prefix, [granule.thumbnail], output_folder, show_progress=False)
754771
return output_folder / granule.thumbnail
755772

tilebox-storage/tilebox/storage/granule.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,39 @@ def _thumbnail_relative_to_eodata_location(thumbnail_url: str, location: str) ->
100100
"preview/thumbnail.png"
101101
"""
102102

103+
# OData API thumbnail URLs have no hint of the actual location/path, so we cannot easily convert them to S3 Paths
104+
if thumbnail_url.startswith("https:/catalogue.dataspace.copernicus.eu/odata/v1/Assets") and thumbnail_url.endswith(
105+
"/$value"
106+
):
107+
return thumbnail_url
108+
103109
url_path = thumbnail_url.rsplit("?path=", maxsplit=1)[-1]
104110
url_path = url_path.removeprefix("/")
105111
location = location.removeprefix("/eodata/")
106-
return str(ObjectPath(url_path).relative_to(location))
112+
try:
113+
return str(ObjectPath(url_path).relative_to(location))
114+
except ValueError:
115+
return thumbnail_url
116+
117+
118+
def _is_copernicus_odata_url(url: str) -> bool:
119+
"""
120+
Checks whether a thumbnail path is an URL pointing to the Copernicus OData API
121+
122+
Those URLs don't encode the actual filename/location, so we cannot easily convert them to the S3 Paths.
123+
Therefore those thumbnails we'll always download via HTTP
124+
125+
Example:
126+
>>> _is_copernicus_odata_url("https://catalogue.dataspace.copernicus.eu/odata/v1/Assets(822e7592-0a66-41b1-b87d-27eec64c377b)/$value")
127+
True
128+
129+
Args:
130+
url: The granule thumbnail URL to check
131+
132+
Returns:
133+
bool: True if the URL is a Copernicus OData API URL, False otherwise
134+
"""
135+
return url.startswith("https://catalogue.dataspace.copernicus.eu/odata/v1/Assets") and url.endswith("/$value")
107136

108137

109138
@dataclass
@@ -133,11 +162,9 @@ def from_data(cls, dataset: "xr.Dataset | CopernicusStorageGranule") -> "Coperni
133162
if "thumbnail" in dataset:
134163
thumbnail_path = dataset.thumbnail.item().strip()
135164

136-
thumbnail = (
137-
_thumbnail_relative_to_eodata_location(thumbnail_path, location)
138-
if isinstance(thumbnail_path, str) and len(thumbnail_path) > 0
139-
else None
140-
)
165+
thumbnail = thumbnail_path if isinstance(thumbnail_path, str) and len(thumbnail_path) > 0 else None
166+
if thumbnail is not None and not _is_copernicus_odata_url(thumbnail):
167+
thumbnail = _thumbnail_relative_to_eodata_location(thumbnail, location)
141168

142169
return cls(
143170
time,

0 commit comments

Comments
 (0)