Skip to content

Commit 221873b

Browse files
committed
Issue #298: start producing "bands" metadata on assets
in addition to existing "eo:bands" to align better with STAC 1.1
1 parent 9136699 commit 221873b

File tree

6 files changed

+108
-17
lines changed

6 files changed

+108
-17
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ and start a new "In Progress" section above it.
2828
- Preserve original non-spatial dimensions in `resample_cube_spatial` dry run ([#397](https://github.com/Open-EO/openeo-python-driver/issues/397))
2929
- Fix compatibility with Shapely2 ([#158](https://github.com/Open-EO/openeo-python-driver/issues/158))
3030
- Allow `overlap` in `apply_neighborhood` to be not specified ([#401](https://github.com/Open-EO/openeo-python-driver/issues/401))
31+
- Start including STAC-1.1.0-style "bands" metadata in assets in batch job results ([#298](https://github.com/Open-EO/openeo-python-driver/issues/298))
3132

3233

3334
## 0.133.0

openeo_driver/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
class STAC_EXTENSION:
22
PROCESSING = "https://stac-extensions.github.io/processing/v1.1.0/schema.json"
33
EO = "https://stac-extensions.github.io/eo/v1.1.0/schema.json"
4+
EO_V110 = "https://stac-extensions.github.io/eo/v1.1.0/schema.json"
5+
EO_V200 = "https://stac-extensions.github.io/eo/v2.0.0/schema.json"
46
FILEINFO = "https://stac-extensions.github.io/file/v2.1.0/schema.json"
57
PROJECTION = "https://stac-extensions.github.io/projection/v1.1.0/schema.json"
68
DATACUBE = "https://stac-extensions.github.io/datacube/v2.2.0/schema.json"

openeo_driver/util/stac.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""
2+
Generic helpers to handle/consume/produce STAC items, collections, metadata constructs.
3+
"""
4+
from typing import Any
5+
6+
import collections.abc
7+
8+
9+
def sniff_stac_extension_prefix(data: Any, prefix: str) -> bool:
10+
"""
11+
Recursively walk through a data structure to
12+
find a particular STAC extension prefix
13+
in object keys (e.g. "eo:" in a "eo:bands" field of an asset).
14+
15+
:param data: data structure to scan
16+
:param prefix: STAC extension prefix to look for,
17+
e.g. "eo:", "raster:", "proj:", ...
18+
"""
19+
if isinstance(data, dict):
20+
if any(isinstance(k, str) and k.startswith(prefix) for k in data.keys()):
21+
return True
22+
return sniff_stac_extension_prefix(data=list(data.values()), prefix=prefix)
23+
elif isinstance(data, collections.abc.Iterable) and not isinstance(data, (str, bytes)):
24+
return any(sniff_stac_extension_prefix(data=x, prefix=prefix) for x in data)
25+
return False

openeo_driver/views.py

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
from openeo_driver.users.auth import HttpAuthHandler
7373
from openeo_driver.util.geometry import BoundingBox, reproject_geometry
7474
from openeo_driver.util.logging import ExtraLoggingFilter, FlaskRequestCorrelationIdLogging
75+
from openeo_driver.util.stac import sniff_stac_extension_prefix
7576
from openeo_driver.utils import EvalEnv, filter_supported_kwargs, smart_bool
7677

7778
_log = logging.getLogger(__name__)
@@ -1166,7 +1167,9 @@ def job_results_canonical_url() -> str:
11661167
)
11671168

11681169
assets = {
1169-
filename: _asset_object(job_id, user_id, filename, asset_metadata, job_info)
1170+
filename: _asset_object(
1171+
job_id=job_id, user_id=user_id, filename=filename, asset_metadata=asset_metadata, job_info=job_info
1172+
)
11701173
for filename, asset_metadata in result_assets.items()
11711174
if asset_metadata.get("asset", True)
11721175
}
@@ -1211,7 +1214,7 @@ def job_result_item_url(item_id) -> str:
12111214
"type": "Collection",
12121215
"stac_version": "1.0.0",
12131216
"stac_extensions": [
1214-
STAC_EXTENSION.EO,
1217+
STAC_EXTENSION.EO_V110,
12151218
STAC_EXTENSION.FILEINFO,
12161219
STAC_EXTENSION.PROCESSING,
12171220
STAC_EXTENSION.PROJECTION,
@@ -1274,8 +1277,8 @@ def job_result_item_url(item_id) -> str:
12741277
STAC_EXTENSION.FILEINFO,
12751278
]
12761279

1277-
if any("eo:bands" in asset_object for asset_object in result["assets"].values()):
1278-
result["stac_extensions"].append(STAC_EXTENSION.EO)
1280+
if sniff_stac_extension_prefix(result["assets"].values(), prefix="eo:"):
1281+
result["stac_extensions"].append(STAC_EXTENSION.EO_V110)
12791282

12801283
if any(key.startswith("proj:") for key in result["properties"]) or any(
12811284
key.startswith("proj:") for key in result["assets"]
@@ -1439,9 +1442,9 @@ def _get_job_result_item(job_id, item_id, user_id):
14391442
"type": "Feature",
14401443
"stac_version": "1.0.0",
14411444
"stac_extensions": [
1442-
STAC_EXTENSION.EO,
1445+
STAC_EXTENSION.EO_V110,
14431446
STAC_EXTENSION.FILEINFO,
1444-
STAC_EXTENSION.PROJECTION
1447+
STAC_EXTENSION.PROJECTION,
14451448
],
14461449
"id": item_id,
14471450
"geometry": geometry,
@@ -1524,20 +1527,31 @@ def _asset_object(job_id, user_id, filename: str, asset_metadata: dict, job_info
15241527
return result_dict
15251528
bands = asset_metadata.get("bands")
15261529

1530+
if bands:
1531+
# TODO: eliminate this legacy "eo:bands" construct at some point?
1532+
result_dict["eo:bands"] = [
1533+
dict_no_none(
1534+
{
1535+
"name": band.name,
1536+
"center_wavelength": band.wavelength_um,
1537+
}
1538+
)
1539+
for band in bands
1540+
]
1541+
# TODO: "bands" is a STAC>=1.1 feature, but here we don't know what version we are in.
1542+
result_dict["bands"] = [
1543+
dict_no_none(
1544+
{
1545+
"name": band.name,
1546+
"eo:center_wavelength": band.wavelength_um,
1547+
}
1548+
)
1549+
for band in bands
1550+
]
1551+
15271552
result_dict.update(
15281553
dict_no_none(
15291554
**{
1530-
"eo:bands": [
1531-
dict_no_none(
1532-
**{
1533-
"name": band.name,
1534-
"center_wavelength": band.wavelength_um,
1535-
}
1536-
)
1537-
for band in bands
1538-
]
1539-
if bands
1540-
else None,
15411555
"proj:bbox": asset_metadata.get("proj:bbox", job_info.proj_bbox),
15421556
"proj:epsg": asset_metadata.get("proj:epsg", job_info.epsg),
15431557
"proj:shape": asset_metadata.get("proj:shape", job_info.proj_shape),

tests/test_views.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1775,6 +1775,7 @@ def test_get_job_results_100(self, api100):
17751775
"href": "http://oeo.net/openeo/1.0.0/jobs/07024ee9-7847-4b8a-b260-6c879a2b3cdc/results/assets/output.tiff",
17761776
"type": "image/tiff; application=geotiff",
17771777
"eo:bands": [{"name": "NDVI", "center_wavelength": 1.23}],
1778+
"bands": [{"name": "NDVI", "eo:center_wavelength": 1.23}],
17781779
},
17791780
"output.nc": {
17801781
"href": "http://oeo.net/openeo/1.0.0/jobs/07024ee9-7847-4b8a-b260-6c879a2b3cdc/results/assets/output.nc",
@@ -1847,6 +1848,7 @@ def test_get_job_results_100(self, api100):
18471848
"proj:epsg": 4326,
18481849
"proj:shape": [300, 600],
18491850
"eo:bands": [{"name": "NDVI", "center_wavelength": 1.23}],
1851+
"bands": [{"name": "NDVI", "eo:center_wavelength": 1.23}],
18501852
},
18511853
"output.nc": {
18521854
"href": "http://oeo.net/openeo/1.0.0/jobs/53c71345-09b4-46b4-b6b0-03fd6fe1f199/results/assets/output.nc",
@@ -1945,6 +1947,7 @@ def test_get_job_results_110(self, api110):
19451947
"href": "http://oeo.net/openeo/1.1.0/jobs/07024ee9-7847-4b8a-b260-6c879a2b3cdc/results/assets/output.tiff",
19461948
"type": "image/tiff; application=geotiff",
19471949
"eo:bands": [{"name": "NDVI", "center_wavelength": 1.23}],
1950+
"bands": [{"name": "NDVI", "eo:center_wavelength": 1.23}],
19481951
},
19491952
"output.nc": {
19501953
"href": "http://oeo.net/openeo/1.1.0/jobs/07024ee9-7847-4b8a-b260-6c879a2b3cdc/results/assets/output.nc",
@@ -2029,6 +2032,7 @@ def test_get_job_results_110(self, api110):
20292032
"proj:epsg": 4326,
20302033
"proj:shape": [300, 600],
20312034
"eo:bands": [{"name": "NDVI", "center_wavelength": 1.23}],
2035+
"bands": [{"name": "NDVI", "eo:center_wavelength": 1.23}],
20322036
},
20332037
"output.nc": {
20342038
"href": "http://oeo.net/openeo/1.1.0/jobs/53c71345-09b4-46b4-b6b0-03fd6fe1f199/results/assets/output.nc",
@@ -2132,6 +2136,7 @@ def test_get_job_results_110(self, api110):
21322136
{"name": "S2-L2A-EVI_t1"},
21332137
{"name": "S2-L2A-EVI_t2"},
21342138
],
2139+
"bands": [{"name": "S2-L2A-EVI_t0"}, {"name": "S2-L2A-EVI_t1"}, {"name": "S2-L2A-EVI_t2"}],
21352140
},
21362141
"timeseries.parquet": {
21372142
"href": "http://oeo.net/openeo/1.1.0/jobs/j-2406047c20fc4966ab637d387502728f/results/assets/timeseries.parquet",
@@ -2143,6 +2148,7 @@ def test_get_job_results_110(self, api110):
21432148
{"name": "S2-L2A-EVI_t1"},
21442149
{"name": "S2-L2A-EVI_t2"},
21452150
],
2151+
"bands": [{"name": "S2-L2A-EVI_t0"}, {"name": "S2-L2A-EVI_t1"}, {"name": "S2-L2A-EVI_t2"}],
21462152
},
21472153
},
21482154
"links": [
@@ -2214,6 +2220,7 @@ def test_get_job_results_signed_100(self, api100, flask_app, backend_config_over
22142220
"href": "http://oeo.net/openeo/1.0.0/jobs/07024ee9-7847-4b8a-b260-6c879a2b3cdc/results/assets/TXIuVGVzdA==/50afb0cad129e61d415278c4ffcd8a83/output.tiff",
22152221
"type": "image/tiff; application=geotiff",
22162222
"eo:bands": [{"name": "NDVI", "center_wavelength": 1.23}],
2223+
"bands": [{"name": "NDVI", "eo:center_wavelength": 1.23}],
22172224
},
22182225
"output.nc": {
22192226
"href": "http://oeo.net/openeo/1.0.0/jobs/07024ee9-7847-4b8a-b260-6c879a2b3cdc/results/assets/TXIuVGVzdA==/e28f17365e698783574dd313de0d64cd/output.nc",
@@ -2355,6 +2362,7 @@ def test_get_job_results_signed_110(self, api110, flask_app, backend_config_over
23552362
"href": "http://oeo.net/openeo/1.1.0/jobs/07024ee9-7847-4b8a-b260-6c879a2b3cdc/results/assets/TXIuVGVzdA==/50afb0cad129e61d415278c4ffcd8a83/output.tiff",
23562363
"type": "image/tiff; application=geotiff",
23572364
"eo:bands": [{"name": "NDVI", "center_wavelength": 1.23}],
2365+
"bands": [{"name": "NDVI", "eo:center_wavelength": 1.23}],
23582366
},
23592367
"output.nc": {
23602368
"href": "http://oeo.net/openeo/1.1.0/jobs/07024ee9-7847-4b8a-b260-6c879a2b3cdc/results/assets/TXIuVGVzdA==/e28f17365e698783574dd313de0d64cd/output.nc",
@@ -2498,6 +2506,7 @@ def test_get_job_results_signed_with_expiration_100(self, api100, flask_app, bac
24982506
"href": "http://oeo.net/openeo/1.0.0/jobs/07024ee9-7847-4b8a-b260-6c879a2b3cdc/results/assets/TXIuVGVzdA==/fd0ca65e29c6d223da05b2e73a875683/output.tiff?expires=2234",
24992507
"type": "image/tiff; application=geotiff",
25002508
"eo:bands": [{"name": "NDVI", "center_wavelength": 1.23}],
2509+
"bands": [{"name": "NDVI", "eo:center_wavelength": 1.23}],
25012510
},
25022511
"output.nc": {
25032512
"href": "http://oeo.net/openeo/1.0.0/jobs/07024ee9-7847-4b8a-b260-6c879a2b3cdc/results/assets/TXIuVGVzdA==/3ed7b944c4f9cc88d2c3ef7534a27596/output.nc?expires=2234",
@@ -2685,6 +2694,7 @@ def test_get_job_results_signed_with_expiration_110(self, api110, flask_app, bac
26852694
"proj:epsg": 4326,
26862695
"proj:shape": [300, 600],
26872696
"eo:bands": [{"center_wavelength": 1.23, "name": "NDVI"}],
2697+
"bands": [{"eo:center_wavelength": 1.23, "name": "NDVI"}],
26882698
"roles": ["data"],
26892699
},
26902700
"output.nc": {
@@ -3068,6 +3078,7 @@ def test_get_job_result_item(self, flask_app, api110, backend_config_overrides):
30683078
"proj:epsg": 4326,
30693079
"proj:shape": [300, 600],
30703080
"eo:bands": [{"center_wavelength": 1.23, "name": "NDVI"}],
3081+
"bands": [{"eo:center_wavelength": 1.23, "name": "NDVI"}],
30713082
"roles": ["data"],
30723083
}
30733084
},
@@ -3166,6 +3177,7 @@ def test_get_vector_cube_job_result_item(
31663177
"roles": ["data"],
31673178
"title": vector_asset_filename,
31683179
"eo:bands": [{"name": "S2-L2A-EVI_t0"}, {"name": "S2-L2A-EVI_t1"}, {"name": "S2-L2A-EVI_t2"}],
3180+
"bands": [{"name": "S2-L2A-EVI_t0"}, {"name": "S2-L2A-EVI_t1"}, {"name": "S2-L2A-EVI_t2"}],
31693181
}
31703182
},
31713183
}
@@ -3231,6 +3243,7 @@ def test_get_job_result_item_with_temporal_extent_on_asset(self, flask_app, api1
32313243
"roles": ["data"],
32323244
"title": "timeseries.parquet",
32333245
"eo:bands": [{"name": "S1-SIGMA0-VV"}, {"name": "S1-SIGMA0-VH"}, {"name": "S2-L2A-B01"}],
3246+
"bands": [{"name": "S1-SIGMA0-VV"}, {"name": "S1-SIGMA0-VH"}, {"name": "S2-L2A-B01"}],
32343247
}
32353248
},
32363249
}

tests/util/test_stac.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
from openeo_driver.util.stac import sniff_stac_extension_prefix
2+
3+
4+
import pytest
5+
6+
7+
@pytest.mark.parametrize(
8+
["data", "prefix", "expected"],
9+
[
10+
({}, "eo:", False),
11+
([], "eo:", False),
12+
(123, "eo:", False),
13+
("foobar", "eo:", False),
14+
("eo:foobar", "eo:", False),
15+
([123, "eo:foobar"], "eo:", False),
16+
({123: "eo:foobar"}, "eo:", False),
17+
({"bands": [{"name": "red"}]}, "eo:", False),
18+
({"eo:bands": [{"name": "red"}]}, "eo:", True),
19+
({"bands": [{"name": "red", "eo:center_wavelength": 123}]}, "eo:", True),
20+
([{"eo:bands": [{"name": "red"}]}, {"eo:bands": [{"name": "green"}]}], "eo:", True),
21+
(({"eo:bands": [{"name": "red"}]}, {"eo:bands": [{"name": "green"}]}), "eo:", True),
22+
(
23+
{1: {"eo:bands": [{"name": "R"}]}, 2: {"eo:bands": [{"name": "G"}]}},
24+
"eo:",
25+
True,
26+
),
27+
(
28+
# Support passing a `dict.values()` view too
29+
{1: {"eo:bands": [{"name": "R"}]}, 2: {"eo:bands": [{"name": "G"}]}}.values(),
30+
"eo:",
31+
True,
32+
),
33+
],
34+
)
35+
def test_sniff_extension_prefix(data, prefix, expected):
36+
assert sniff_stac_extension_prefix(data, prefix=prefix) == expected

0 commit comments

Comments
 (0)