Skip to content

Commit 064ae7e

Browse files
Fix copernicus storage client thumbnail URL resolver (#36)
1 parent dd1c32f commit 064ae7e

File tree

3 files changed

+53
-8
lines changed

3 files changed

+53
-8
lines changed

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: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,34 @@ def _thumbnail_relative_to_eodata_location(thumbnail_url: str, location: str) ->
9999
>>> )
100100
"preview/thumbnail.png"
101101
"""
102-
103102
url_path = thumbnail_url.rsplit("?path=", maxsplit=1)[-1]
104103
url_path = url_path.removeprefix("/")
105104
location = location.removeprefix("/eodata/")
106-
return str(ObjectPath(url_path).relative_to(location))
105+
try:
106+
return str(ObjectPath(url_path).relative_to(location))
107+
except ValueError:
108+
# in case the path couldn't be properly parsed, relative_to will fail. Fall back to the default value then
109+
return thumbnail_url
110+
111+
112+
def _is_copernicus_odata_url(url: str) -> bool:
113+
"""
114+
Checks whether a thumbnail path is an URL pointing to the Copernicus OData API
115+
116+
Those URLs don't encode the actual filename/location, so we cannot easily convert them to the S3 Paths.
117+
Therefore those thumbnails we'll always download via HTTP
118+
119+
Example:
120+
>>> _is_copernicus_odata_url("https://catalogue.dataspace.copernicus.eu/odata/v1/Assets(822e7592-0a66-41b1-b87d-27eec64c377b)/$value")
121+
True
122+
123+
Args:
124+
url: The granule thumbnail URL to check
125+
126+
Returns:
127+
bool: True if the URL is a Copernicus OData API URL, False otherwise
128+
"""
129+
return url.startswith("https://catalogue.dataspace.copernicus.eu/odata/v1/Assets") and url.endswith("/$value")
107130

108131

109132
@dataclass
@@ -133,11 +156,9 @@ def from_data(cls, dataset: "xr.Dataset | CopernicusStorageGranule") -> "Coperni
133156
if "thumbnail" in dataset:
134157
thumbnail_path = dataset.thumbnail.item().strip()
135158

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-
)
159+
thumbnail = thumbnail_path if isinstance(thumbnail_path, str) and len(thumbnail_path) > 0 else None
160+
if thumbnail is not None and not _is_copernicus_odata_url(thumbnail):
161+
thumbnail = _thumbnail_relative_to_eodata_location(thumbnail, location)
141162

142163
return cls(
143164
time,

0 commit comments

Comments
 (0)