Skip to content

Commit 737c90d

Browse files
committed
Support collection_property helper in load_stac #246
1 parent 3f1cec5 commit 737c90d

File tree

5 files changed

+161
-44
lines changed

5 files changed

+161
-44
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Support `collection_property` based property filtering in `load_stac` ([#246](https://github.com/Open-EO/openeo-python-client/issues/246))
13+
1214
### Changed
1315

1416
- Eliminate deprecated `utcnow` usage patterns. Introduce `Rfc3339.now_utc()` method (as replacement for deprecated `utcnow()` method) to simplify finding deprecated `utcnow` usage in user code. ([#760](https://github.com/Open-EO/openeo-python-client/issues/760))

openeo/rest/connection.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -1097,7 +1097,7 @@ def load_collection(
10971097
temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None,
10981098
bands: Union[Iterable[str], Parameter, str, None] = None,
10991099
properties: Union[
1100-
None, Dict[str, Union[str, PGNode, Callable]], List[CollectionProperty], CollectionProperty
1100+
Dict[str, Union[PGNode, Callable]], List[CollectionProperty], CollectionProperty, None
11011101
] = None,
11021102
max_cloud_cover: Optional[float] = None,
11031103
fetch_metadata: bool = True,
@@ -1198,7 +1198,9 @@ def load_stac(
11981198
spatial_extent: Union[dict, Parameter, shapely.geometry.base.BaseGeometry, str, Path, None] = None,
11991199
temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None,
12001200
bands: Union[Iterable[str], Parameter, str, None] = None,
1201-
properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None,
1201+
properties: Union[
1202+
Dict[str, Union[PGNode, Callable]], List[CollectionProperty], CollectionProperty, None
1203+
] = None,
12021204
) -> DataCube:
12031205
"""
12041206
Loads data from a static STAC catalog or a STAC API Collection and returns the data as a processable :py:class:`DataCube`.
@@ -1292,6 +1294,8 @@ def load_stac(
12921294
The value must be a condition (user-defined process) to be evaluated against a STAC API.
12931295
This parameter is not supported for static STAC.
12941296
1297+
See :py:func:`~openeo.rest.graph_building.collection_property` for easy construction of property filters.
1298+
12951299
.. versionadded:: 0.17.0
12961300
12971301
.. versionchanged:: 0.23.0
@@ -1300,6 +1304,9 @@ def load_stac(
13001304
13011305
.. versionchanged:: 0.37.0
13021306
Argument ``spatial_extent``: add support for passing a Shapely geometry or a local path to a GeoJSON file.
1307+
1308+
.. versionchanged:: 0.41.0
1309+
Add support for :py:func:`~openeo.rest.graph_building.collection_property` based property filters
13031310
"""
13041311
return DataCube.load_stac(
13051312
url=url,

openeo/rest/datacube.py

+66-28
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ def load_collection(
175175
bands: Union[Iterable[str], Parameter, str, None] = None,
176176
fetch_metadata: bool = True,
177177
properties: Union[
178-
None, Dict[str, Union[str, PGNode, typing.Callable]], List[CollectionProperty], CollectionProperty
178+
Dict[str, Union[PGNode, typing.Callable]], List[CollectionProperty], CollectionProperty, None
179179
] = None,
180180
max_cloud_cover: Optional[float] = None,
181181
) -> DataCube:
@@ -250,27 +250,13 @@ def load_collection(
250250
metadata = metadata.filter_bands(bands)
251251
arguments['bands'] = bands
252252

253-
if isinstance(properties, list):
254-
# TODO: warn about items that are not CollectionProperty objects instead of silently dropping them.
255-
properties = {p.name: p.from_node() for p in properties if isinstance(p, CollectionProperty)}
256-
if isinstance(properties, CollectionProperty):
257-
properties = {properties.name: properties.from_node()}
258-
elif properties is None:
259-
properties = {}
260-
if max_cloud_cover:
261-
properties["eo:cloud_cover"] = lambda v: v <= max_cloud_cover
262-
if properties:
263-
summaries = metadata and metadata.get("summaries") or {}
264-
undefined_properties = set(properties.keys()).difference(summaries.keys())
265-
if undefined_properties:
266-
warnings.warn(
267-
f"{collection_id} property filtering with properties that are undefined "
268-
f"in the collection metadata (summaries): {', '.join(undefined_properties)}.",
269-
stacklevel=2,
270-
)
271-
arguments["properties"] = {
272-
prop: build_child_callback(pred, parent_parameters=["value"]) for prop, pred in properties.items()
273-
}
253+
properties = cls._build_load_properties_argument(
254+
properties=properties,
255+
supported_properties=(metadata.get("summaries", default={}).keys() if metadata else None),
256+
max_cloud_cover=max_cloud_cover,
257+
)
258+
if properties is not None:
259+
arguments["properties"] = properties
274260

275261
pg = PGNode(
276262
process_id='load_collection',
@@ -282,6 +268,50 @@ def load_collection(
282268
load_collection, name="create_collection", since="0.4.6"
283269
)
284270

271+
@classmethod
272+
def _build_load_properties_argument(
273+
cls,
274+
properties: Union[
275+
Dict[str, Union[PGNode, typing.Callable]],
276+
List[CollectionProperty],
277+
CollectionProperty,
278+
None,
279+
],
280+
*,
281+
supported_properties: Optional[typing.Collection[str]] = None,
282+
max_cloud_cover: Optional[float] = None,
283+
) -> Union[Dict[str, PGNode], None]:
284+
"""
285+
Helper to convert/build the ``properties`` argument
286+
for ``load_collection`` and ``load_stac`` from user input
287+
"""
288+
if isinstance(properties, CollectionProperty):
289+
properties = [properties]
290+
if isinstance(properties, list):
291+
if not all(isinstance(p, CollectionProperty) for p in properties):
292+
raise ValueError(
293+
f"When providing load properties as a list, all items must be CollectionProperty objects, but got {properties}"
294+
)
295+
properties = {p.name: p.from_node() for p in properties}
296+
297+
if max_cloud_cover is not None:
298+
properties = properties or {}
299+
properties["eo:cloud_cover"] = lambda v: v <= max_cloud_cover
300+
301+
if isinstance(properties, dict):
302+
if supported_properties:
303+
unsupported_properties = set(properties.keys()).difference(supported_properties)
304+
if unsupported_properties:
305+
warnings.warn(
306+
f"Property filtering with unsupported properties according to collection/STAC metadata: {unsupported_properties} (supported: {supported_properties}).",
307+
stacklevel=3,
308+
)
309+
properties = {
310+
prop: build_child_callback(pred, parent_parameters=["value"]) for prop, pred in properties.items()
311+
}
312+
313+
return properties
314+
285315
@classmethod
286316
@deprecated(reason="Depends on non-standard process, replace with :py:meth:`openeo.rest.connection.Connection.load_stac` where possible.",version="0.25.0")
287317
def load_disk_collection(cls, connection: Connection, file_format: str, glob_pattern: str, **options) -> DataCube:
@@ -314,7 +344,9 @@ def load_stac(
314344
spatial_extent: Union[dict, Parameter, shapely.geometry.base.BaseGeometry, str, pathlib.Path, None] = None,
315345
temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None,
316346
bands: Union[Iterable[str], Parameter, str, None] = None,
317-
properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None,
347+
properties: Union[
348+
Dict[str, Union[PGNode, typing.Callable]], List[CollectionProperty], CollectionProperty, None
349+
] = None,
318350
connection: Optional[Connection] = None,
319351
) -> DataCube:
320352
"""
@@ -409,14 +441,19 @@ def load_stac(
409441
The value must be a condition (user-defined process) to be evaluated against a STAC API.
410442
This parameter is not supported for static STAC.
411443
444+
See :py:func:`~openeo.rest.graph_building.collection_property` for easy construction of property filters.
445+
412446
:param connection: The connection to use to connect with the backend.
413447
414448
.. versionadded:: 0.33.0
415449
416450
.. versionchanged:: 0.37.0
417451
Argument ``spatial_extent``: add support for passing a Shapely geometry or a local path to a GeoJSON file.
452+
453+
.. versionchanged:: 0.41.0
454+
Add support for :py:func:`~openeo.rest.graph_building.collection_property` based property filters
418455
"""
419-
arguments = {"url": url}
456+
arguments: dict = {"url": url}
420457
if spatial_extent:
421458
arguments["spatial_extent"] = _get_geometry_argument(
422459
argument=spatial_extent,
@@ -434,10 +471,11 @@ def load_stac(
434471
bands = cls._get_bands(bands, process_id="load_stac")
435472
if bands is not None:
436473
arguments["bands"] = bands
437-
if properties:
438-
arguments["properties"] = {
439-
prop: build_child_callback(pred, parent_parameters=["value"]) for prop, pred in properties.items()
440-
}
474+
475+
properties = cls._build_load_properties_argument(properties=properties)
476+
if properties is not None:
477+
arguments["properties"] = properties
478+
441479
graph = PGNode("load_stac", arguments=arguments)
442480
try:
443481
metadata = metadata_from_stac(url)

tests/rest/datacube/test_datacube100.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2129,7 +2129,7 @@ def test_load_collection_max_cloud_cover_summaries_warning(
21292129
if expect_warning:
21302130
assert len(recwarn.list) == 1
21312131
assert re.search(
2132-
"property filtering.*undefined.*collection metadata.*eo:cloud_cover",
2132+
"Property filtering.*unsupported.*collection.*metadata.*eo:cloud_cover",
21332133
str(recwarn.pop(UserWarning).message),
21342134
)
21352135
else:

tests/rest/test_connection.py

+83-13
Original file line numberDiff line numberDiff line change
@@ -2788,24 +2788,94 @@ def test_extents(self, con120):
27882788
}
27892789
}
27902790

2791-
def test_properties(self, con120):
2792-
cube = con120.load_stac("https://provider.test/dataset", properties={"platform": lambda p: p == "S2A"})
2791+
@pytest.mark.parametrize(
2792+
["properties", "expected"],
2793+
[
2794+
(
2795+
# dict with callable
2796+
{"platform": lambda p: p == "S2A"},
2797+
{
2798+
"platform": {
2799+
"process_graph": {
2800+
"eq1": {
2801+
"process_id": "eq",
2802+
"arguments": {"x": {"from_parameter": "value"}, "y": "S2A"},
2803+
"result": True,
2804+
}
2805+
}
2806+
}
2807+
},
2808+
),
2809+
(
2810+
# Single collection_property
2811+
(openeo.collection_property("platform") == "S2A"),
2812+
{
2813+
"platform": {
2814+
"process_graph": {
2815+
"eq1": {
2816+
"process_id": "eq",
2817+
"arguments": {"x": {"from_parameter": "value"}, "y": "S2A"},
2818+
"result": True,
2819+
}
2820+
}
2821+
}
2822+
},
2823+
),
2824+
(
2825+
# List of collection_properties
2826+
[
2827+
openeo.collection_property("platform") == "S2A",
2828+
openeo.collection_property("flavor") == "salty",
2829+
],
2830+
{
2831+
"platform": {
2832+
"process_graph": {
2833+
"eq1": {
2834+
"process_id": "eq",
2835+
"arguments": {"x": {"from_parameter": "value"}, "y": "S2A"},
2836+
"result": True,
2837+
}
2838+
}
2839+
},
2840+
"flavor": {
2841+
"process_graph": {
2842+
"eq2": {
2843+
"process_id": "eq",
2844+
"arguments": {"x": {"from_parameter": "value"}, "y": "salty"},
2845+
"result": True,
2846+
}
2847+
}
2848+
},
2849+
},
2850+
),
2851+
(
2852+
# Advanced PGNode usage
2853+
{"platform": PGNode(process_id="custom_foo", arguments={"mode": "bar"})},
2854+
{
2855+
"platform": {
2856+
"process_graph": {
2857+
"customfoo1": {
2858+
"process_id": "custom_foo",
2859+
"arguments": {"mode": "bar"},
2860+
"result": True,
2861+
}
2862+
}
2863+
},
2864+
},
2865+
),
2866+
],
2867+
)
2868+
def test_property_filtering(self, con120, properties, expected):
2869+
cube = con120.load_stac(
2870+
"https://provider.test/dataset",
2871+
properties=properties,
2872+
)
27932873
assert cube.flat_graph() == {
27942874
"loadstac1": {
27952875
"process_id": "load_stac",
27962876
"arguments": {
27972877
"url": "https://provider.test/dataset",
2798-
"properties": {
2799-
"platform": {
2800-
"process_graph": {
2801-
"eq1": {
2802-
"arguments": {"x": {"from_parameter": "value"}, "y": "S2A"},
2803-
"process_id": "eq",
2804-
"result": True,
2805-
}
2806-
}
2807-
}
2808-
},
2878+
"properties": expected,
28092879
},
28102880
"result": True,
28112881
}

0 commit comments

Comments
 (0)