Respect STAC cube:dimensions as source of truth in metadata_from_stac + align tests with dimension policy#867
Conversation
…legacy unit tests fail
soxofaan
left a comment
There was a problem hiding this comment.
just could give this a quick scan to make some notes
…se-correction for Dict to support python3.8 Co-authored-by: Stefaan Lippens <soxofaan@users.noreply.github.com>
|
ready for review @soxofaan |
|
Hi @soxofaan , please let us know if there is something we can do to move on with this PR! |
|
|
||
| # Dimension name resolution policy (STAC cube:dimensions vs openEO defaults) | ||
| @pytest.mark.parametrize( | ||
| ["stac_dict", "expected_dims"], |
There was a problem hiding this comment.
can a STAC 1.1 style test case be added which uses the new 'bands' common metadata and no datacube extension?
Collection metadata example:
https://github.com/radiantearth/stac-spec/blob/master/examples/collection-only/collection.json#L152
Bands spec:
https://github.com/radiantearth/stac-spec/blob/master/commons/common-metadata.md#band-object
There was a problem hiding this comment.
Added a STAC 1.1 bands common metadata test without the datacube extension in test_metadata_from_stac_stac_1_1_common_bands_without_datacube_extension. The test verifies that metadata_from_stac() still builds the fallback openEO dimensions and extracts the common bands metadata correctly.
jdries
left a comment
There was a problem hiding this comment.
FYI, Stefaan is on holiday
Added one comment myself.
Please also add a changelog entry.
Otherwise I'm inclined to say it looks good, assuming this change mostly preserves existing behaviour. (Biggest worry are collections with faulty datacube extension metadata that would suddenly have a change in behavior.)
| TemporalDimension(name="t", extent=self.infer_temporal_extent(stac_object)), | ||
| ] | ||
| if isinstance(stac_object, (pystac.Collection, pystac.Item)): | ||
| dimensions.append(BandDimension(name="bands", bands=list(bands))) |
There was a problem hiding this comment.
why conditionally add the band dimension here?
There was a problem hiding this comment.
Removed this conditional injection. When cube:dimensions is present, the declared dimensions are now treated as the source of truth and no fallback bands dimension is added.
| cube_dimensions = self.cube_dimensions_dict(stac_object) | ||
| return isinstance(cube_dimensions, dict) and len(cube_dimensions) > 0 | ||
|
|
||
| def cube_dimensions_dict(self, stac_object: pystac.STACObject) -> Dict[str, dict]: |
There was a problem hiding this comment.
I don't think this has to be in the public interface
There was a problem hiding this comment.
Agreed. I made these parser helpers private: _has_cube_dimensions(), _cube_dimensions_dict(), and _parse_declared_dimensions().
| return [Rfc3339(propagate_none=True).normalize(d) for d in interval] | ||
|
|
||
| if isinstance(stac_object, pystac.Item): | ||
| props = getattr(stac_object, "properties", {}) or {} |
There was a problem hiding this comment.
just wondering: why using getattr here? Isn't the presence of properties guaranteed?
There was a problem hiding this comment.
Changed this to use stac_obj.properties directly for items.
| norm = Rfc3339(propagate_none=True).normalize(dt_) | ||
| return [norm, norm] | ||
| start = props.get("start_datetime") | ||
| end = props.get("end_datetime") |
There was a problem hiding this comment.
isn't the "datetime" field a required field, so is this code even reachable?
Or to put differently: shouldn't "start_datetime"/"end_datetime" get higher precedence over "datetime"?
There was a problem hiding this comment.
updated the item temporal fallback logic so start_datetime / end_datetime take precedence over datetime and added test coverage for that precedence.
| dimensions = self.dimensions_from_stac_object(stac_object=stac_object, bands=bands) | ||
| return CubeMetadata(dimensions=dimensions) | ||
|
|
||
| def dimensions_from_stac_object(self, stac_object: pystac.STACObject, bands: _BandList) -> List[Dimension]: |
There was a problem hiding this comment.
Why having bands as argument here? Shouldn't it be the responsibility of this method to detect the bands? Note that this is a public method, so it's weird to have an argument for something you actually want to extract
There was a problem hiding this comment.
Agreed. dimensions_from_stac_object() now detects bands internally through bands_from_stac_object() instead of requiring a bands argument. I didn't notice that this bands_from stac_object() exists earlier, my bad.
| dimensions: List[Dimension] = [ | ||
| SpatialDimension(name="x", extent=[None, None]), | ||
| SpatialDimension(name="y", extent=[None, None]), | ||
| TemporalDimension(name="t", extent=self.infer_temporal_extent(stac_object)), |
There was a problem hiding this comment.
reuse get_temporal_dimension here instead ?
There was a problem hiding this comment.
removed the usage of this temporal extent helper, and reusing get_temporal_dimension here instead.
|
|
||
| return ext or [None, None] | ||
|
|
||
| def parse_declared_dimensions(self, stac_object: pystac.STACObject, bands: _BandList) -> List[Dimension]: |
There was a problem hiding this comment.
as noted above, it looks weird to have this required bands argument here without explanation
There was a problem hiding this comment.
Removed the required bands argument from the public-facing parser method. dimensions_from_stac_object() now detects bands internally through bands_from_stac_object(), no longer needed to pass extracted bands into the method. This behaviour is consistent across PR now!
|
@soxofaan @jdries this PR is ready to review when you can! This issue reminded me about this pending PR: https://forum.dataspace.copernicus.eu/t/load-stac-time-dimension-hidden/5312/3 |
| cube_dimensions = self._cube_dimensions_dict(stac_object) | ||
| return isinstance(cube_dimensions, dict) and len(cube_dimensions) > 0 | ||
|
|
||
| def _cube_dimensions_dict(self, stac_object: pystac.STACObject) -> Dict[str, dict]: |
There was a problem hiding this comment.
Can this be a static method?
You should probably check if there are other methods added by this PR that could also be made static as consequence of making this method static.
| PySTAC cube dimension wrapper may raise if 'extent' is missing. | ||
| Also, depending on serialization/version, extent might live in extra_fields. | ||
| """ | ||
| try: |
There was a problem hiding this comment.
Can this try-block be replaced by:
ext = getattr(dim, "extent", None)and the code will do the same thing?
| """ | ||
| try: | ||
| ext = dim.extent | ||
| except Exception: |
There was a problem hiding this comment.
If the getattr() I suggested in the previous comment isn't right, catching Exception here is too broad, the code should catch the exception it's expecting and let other exceptions through so the caller can deal with unexpected errors.
| _PYSTAC_1_9_EXTENSION_INTERFACE | ||
| and getattr(stac_object, "ext", None) is not None | ||
| and stac_object.ext.has("cube") | ||
| and hasattr(stac_object.ext, "cube") |
There was a problem hiding this comment.
Does this line do the same as the previous line?
| def _parse_cube_dimensions_from_raw_dict(self, stac_object: pystac.STACObject, bands: _BandList) -> List[Dimension]: | ||
| """ | ||
| Parse dimensions from raw cube:dimensions dict. | ||
| Supports 'spatial', 'temporal', and ('bands' or 'spectral' as an alias). |
There was a problem hiding this comment.
Parenthesis error? The sentence ends with an "and".
| return {} | ||
|
|
||
| @staticmethod | ||
| def _safe_extent_from_pystac_cube_dim(dim) -> list: |
There was a problem hiding this comment.
Can't use list in Python 3.8.
And since the old code is at least partially annotated, I think it's nice if you add type annotations for the new functions/methods in this PR.
Problem
metadata_from_stac()previously injected openEO default dimensions (x,y,t,bands) even when a STAC object already defined dimensions viacube:dimensions.This led to:
Fix
Introduce a clear dimension resolution policy:
If
cube:dimensionsis present→ treat it as the source of truth and preserve dimension names and order.
If
cube:dimensionsis absent→ fall back to openEO defaults (
x,y, optionalt).The metadata parsing logic was refactored accordingly and aligned with PySTAC datacube capabilities.
Testing
Updated existing tests to no longer assume an always-present band dimension.
Added checks verifying:
cube:dimensionsis missing,cube:dimensionsexists,Tests now explicitly enforce the new dimension policy to prevent regressions.
This change makes metadata parsing more STAC-compliant while keeping backward-compatible fallback behavior.
Refer:
https://github.com/destine-datalake-cube/dedl-openeo-coordination/issues/3
Also solves: #743