Skip to content

Commit 2f2f878

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

3 files changed

Lines changed: 56 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: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,37 @@ def _thumbnail_relative_to_eodata_location(thumbnail_url: str, location: str) ->
100100
"preview/thumbnail.png"
101101
"""
102102

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

108135

109136
@dataclass
@@ -133,11 +160,9 @@ def from_data(cls, dataset: "xr.Dataset | CopernicusStorageGranule") -> "Coperni
133160
if "thumbnail" in dataset:
134161
thumbnail_path = dataset.thumbnail.item().strip()
135162

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-
)
163+
thumbnail = thumbnail_path if isinstance(thumbnail_path, str) and len(thumbnail_path) > 0 else None
164+
if thumbnail is not None and not _is_copernicus_odata_url(thumbnail):
165+
thumbnail = _thumbnail_relative_to_eodata_location(thumbnail, location)
141166

142167
return cls(
143168
time,

0 commit comments

Comments
 (0)