Skip to content

Commit 179a852

Browse files
committed
Issue #676 fix usage of Parameter.spatial_extent with load_collection
and filter_bbox
1 parent 3fd041a commit 179a852

File tree

6 files changed

+156
-24
lines changed

6 files changed

+156
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2424
### Fixed
2525

2626
- `load_stac`: use fallback temporal dimension when no "cube:dimensions" in STAC Collection ([#666](https://github.com/Open-EO/openeo-python-client/issues/666))
27+
- Fix usage of `Parameter.spatial_extent()` with `load_collection` and `filter_bbox` ([#676](https://github.com/Open-EO/openeo-python-client/issues/676))
2728

2829
## [0.35.0] - 2024-11-19
2930

openeo/api/process.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ def geojson(cls, name: str, description: str = "Geometries specified as GeoJSON
410410
@classmethod
411411
def temporal_interval(
412412
cls,
413-
name: str,
413+
name: str = "temporal_extent",
414414
description: str = "Temporal extent specified as two-element array with start and end date/date-time.",
415415
**kwargs,
416416
) -> Parameter:
@@ -441,3 +441,26 @@ def temporal_interval(
441441
},
442442
}
443443
return cls(name=name, description=description, schema=schema, **kwargs)
444+
445+
446+
def schema_supports(schema: Union[dict, List[dict]], type: str, subtype: Optional[str] = None) -> bool:
447+
"""Helper to check if parameter schema supports given type/subtype"""
448+
# TODO: support checking item type in arrays
449+
if isinstance(schema, dict):
450+
actual_type = schema.get("type")
451+
if isinstance(actual_type, str):
452+
if actual_type != type:
453+
return False
454+
elif isinstance(actual_type, list):
455+
if type not in actual_type:
456+
return False
457+
else:
458+
raise ValueError(actual_type)
459+
if subtype:
460+
if schema.get("subtype") != subtype:
461+
return False
462+
return True
463+
elif isinstance(schema, list):
464+
return any(schema_supports(s, type=type, subtype=subtype) for s in schema)
465+
else:
466+
raise ValueError(schema)

openeo/rest/datacube.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
import shapely.geometry.base
2626
from shapely.geometry import MultiPolygon, Polygon, mapping
2727

28-
from openeo.api.process import Parameter
28+
from openeo.api.process import Parameter, schema_supports
2929
from openeo.dates import get_temporal_extent
3030
from openeo.internal.documentation import openeo_process
3131
from openeo.internal.graph_building import PGNode, ReduceNode, _FromNodeMixin
@@ -182,10 +182,10 @@ def load_collection(
182182
temporal_extent = cls._get_temporal_extent(extent=temporal_extent)
183183

184184
if isinstance(spatial_extent, Parameter):
185-
if spatial_extent.schema.get("type") != "object":
185+
if not schema_supports(spatial_extent.schema, type="object"):
186186
warnings.warn(
187187
"Unexpected parameterized `spatial_extent` in `load_collection`:"
188-
f" expected schema with type 'object' but got {spatial_extent.schema!r}."
188+
f" expected schema compatible with type 'object' but got {spatial_extent.schema!r}."
189189
)
190190
arguments = {
191191
'id': collection_id,
@@ -481,7 +481,7 @@ def filter_bbox(
481481
crs: Optional[Union[int, str]] = None,
482482
base: Optional[float] = None,
483483
height: Optional[float] = None,
484-
bbox: Optional[Sequence[float]] = None,
484+
bbox: Union[Sequence[float], Parameter, None] = None,
485485
) -> DataCube:
486486
"""
487487
Limits the data cube to the specified bounding box.
@@ -555,10 +555,10 @@ def filter_bbox(
555555
raise ValueError(args)
556556

557557
if isinstance(bbox, Parameter):
558-
if bbox.schema.get("type") != "object":
558+
if not schema_supports(bbox.schema, type="object"):
559559
warnings.warn(
560560
"Unexpected parameterized `extent` in `filter_bbox`:"
561-
f" expected schema with type 'object' but got {bbox.schema!r}."
561+
f" expected schema compatible with type 'object' but got {bbox.schema!r}."
562562
)
563563
extent = bbox
564564
else:

tests/api/test_process.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import pytest
22

3-
from openeo.api.process import Parameter
3+
from openeo.api.process import Parameter, schema_supports
44

55

66
def test_parameter_defaults():
@@ -261,3 +261,35 @@ def test_parameter_spatial_extent():
261261
},
262262
],
263263
}
264+
265+
266+
def test_schema_supports_type_basic():
267+
schema = {"type": "string"}
268+
assert schema_supports(schema, type="string") is True
269+
assert schema_supports(schema, type="number") is False
270+
271+
272+
def test_schema_supports_type_list():
273+
schema = {"type": ["string", "number"]}
274+
assert schema_supports(schema, type="string") is True
275+
assert schema_supports(schema, type="number") is True
276+
assert schema_supports(schema, type="object") is False
277+
278+
279+
def test_schema_supports_subtype():
280+
schema = {"type": "object", "subtype": "datacube"}
281+
assert schema_supports(schema, type="object") is True
282+
assert schema_supports(schema, type="object", subtype="datacube") is True
283+
assert schema_supports(schema, type="object", subtype="geojson") is False
284+
285+
286+
def test_schema_supports_list():
287+
schema = [
288+
{"type": "string"},
289+
{"type": "object", "subtype": "datacube"},
290+
]
291+
assert schema_supports(schema, type="string") is True
292+
assert schema_supports(schema, type="number") is False
293+
assert schema_supports(schema, type="object") is True
294+
assert schema_supports(schema, type="object", subtype="datacube") is True
295+
assert schema_supports(schema, type="object", subtype="geojson") is False

tests/rest/datacube/test_datacube100.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,15 @@ def test_filter_bbox_kwargs(con100: Connection, kwargs, expected):
243243
assert node["arguments"]["extent"] == expected
244244

245245

246-
def test_filter_bbox_parameter(con100: Connection):
246+
@pytest.mark.parametrize(
247+
"bbox_param",
248+
[
249+
Parameter(name="my_bbox", schema={"type": "object"}),
250+
Parameter.spatial_extent(name="my_bbox"),
251+
Parameter.bounding_box(name="my_bbox"),
252+
],
253+
)
254+
def test_filter_bbox_parameter(con100: Connection, bbox_param):
247255
expected = {
248256
"process_id": "filter_bbox",
249257
"arguments": {
@@ -252,7 +260,6 @@ def test_filter_bbox_parameter(con100: Connection):
252260
},
253261
"result": True,
254262
}
255-
bbox_param = Parameter(name="my_bbox", schema={"type": "object"})
256263

257264
cube = con100.load_collection("S2").filter_bbox(bbox_param)
258265
assert _get_leaf_node(cube) == expected
@@ -274,14 +281,14 @@ def test_filter_bbox_parameter_invalid_schema(con100: Connection):
274281

275282
with pytest.warns(
276283
UserWarning,
277-
match="Unexpected parameterized `extent` in `filter_bbox`: expected schema with type 'object' but got {'type': 'string'}.",
284+
match="Unexpected parameterized `extent` in `filter_bbox`: expected schema compatible with type 'object' but got {'type': 'string'}.",
278285
):
279286
cube = con100.load_collection("S2").filter_bbox(bbox_param)
280287
assert _get_leaf_node(cube) == expected
281288

282289
with pytest.warns(
283290
UserWarning,
284-
match="Unexpected parameterized `extent` in `filter_bbox`: expected schema with type 'object' but got {'type': 'string'}.",
291+
match="Unexpected parameterized `extent` in `filter_bbox`: expected schema compatible with type 'object' but got {'type': 'string'}.",
285292
):
286293
cube = con100.load_collection("S2").filter_bbox(bbox=bbox_param)
287294
assert _get_leaf_node(cube) == expected
@@ -2301,6 +2308,43 @@ def test_load_collection_parameterized_bands(con100):
23012308
}
23022309

23032310

2311+
@pytest.mark.parametrize(
2312+
["spatial_extent", "temporal_extent", "spatial_name", "temporal_name"],
2313+
[
2314+
(
2315+
Parameter.spatial_extent(),
2316+
Parameter.temporal_interval(),
2317+
"spatial_extent",
2318+
"temporal_extent",
2319+
),
2320+
(
2321+
Parameter.bounding_box(name="spatial_extent"),
2322+
Parameter.temporal_interval(),
2323+
"spatial_extent",
2324+
"temporal_extent",
2325+
),
2326+
(
2327+
Parameter(name="my_bbox", schema={"type": "object"}),
2328+
Parameter(name="dates", schema={"type": "array"}),
2329+
"my_bbox",
2330+
"dates",
2331+
),
2332+
],
2333+
)
2334+
def test_load_collection_parameterized_extents(con100, spatial_extent, temporal_extent, spatial_name, temporal_name):
2335+
cube = con100.load_collection("S2", spatial_extent=spatial_extent, temporal_extent=temporal_extent)
2336+
assert get_download_graph(cube, drop_save_result=True) == {
2337+
"loadcollection1": {
2338+
"arguments": {
2339+
"id": "S2",
2340+
"spatial_extent": {"from_parameter": spatial_name},
2341+
"temporal_extent": {"from_parameter": temporal_name},
2342+
},
2343+
"process_id": "load_collection",
2344+
},
2345+
}
2346+
2347+
23042348
def test_apply_dimension_temporal_cumsum_with_target(con100, test_data):
23052349
cumsum = con100.load_collection("S2").apply_dimension('cumsum', dimension="t", target_dimension="MyNewTime")
23062350
actual_graph = cumsum.flat_graph()

tests/rest/test_udp.py

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,28 @@ def test_delete(con100, requests_mock):
279279
assert adapter.called
280280

281281

282-
def test_build_parameterized_cube_basic(con100, recwarn):
283-
layer = Parameter.string("layer", description="Collection Id")
284-
dates = Parameter.string("dates", description="Temporal extent")
285-
bbox = Parameter("bbox", schema="object", description="bbox")
282+
@pytest.mark.parametrize(
283+
["layer", "dates", "bbox"],
284+
[
285+
(
286+
Parameter.string("layer", description="Collection Id"),
287+
Parameter.string("dates", description="Temporal extent"),
288+
Parameter("bbox", schema="object", description="bbox"),
289+
),
290+
(
291+
Parameter.string("layer", description="Collection Id"),
292+
Parameter.temporal_interval(name="dates"),
293+
Parameter.spatial_extent(name="bbox"),
294+
),
295+
(
296+
Parameter.string("layer", description="Collection Id"),
297+
Parameter.temporal_interval(name="dates"),
298+
Parameter.bounding_box(name="bbox"),
299+
),
300+
],
301+
)
302+
def test_build_parameterized_cube_filters(con100, layer, dates, bbox, recwarn):
286303
cube = con100.load_collection(layer).filter_temporal(dates).filter_bbox(bbox)
287-
288304
assert cube.flat_graph() == {
289305
"loadcollection1": {
290306
"process_id": "load_collection",
@@ -353,12 +369,28 @@ def test_build_parameterized_cube_start_date(con100, recwarn):
353369
assert recwarn.list == []
354370

355371

356-
def test_build_parameterized_cube_load_collection(con100, recwarn):
357-
layer = Parameter.string("layer", description="Collection id")
358-
dates = Parameter.string("dates", description="temporal extent")
359-
bbox = Parameter("bbox", schema="object", description="bbox")
372+
@pytest.mark.parametrize(
373+
["layer", "dates", "bbox"],
374+
[
375+
(
376+
Parameter.string("layer", description="Collection Id"),
377+
Parameter.string("dates", description="Temporal extent"),
378+
Parameter("bbox", schema="object", description="bbox"),
379+
),
380+
(
381+
Parameter.string("layer", description="Collection Id"),
382+
Parameter.temporal_interval(name="dates"),
383+
Parameter.spatial_extent(name="bbox"),
384+
),
385+
(
386+
Parameter.string("layer", description="Collection Id"),
387+
Parameter.temporal_interval(name="dates"),
388+
Parameter.bounding_box(name="bbox"),
389+
),
390+
],
391+
)
392+
def test_build_parameterized_cube_load_collection(con100, recwarn, layer, dates, bbox):
360393
cube = con100.load_collection(layer, spatial_extent=bbox, temporal_extent=dates)
361-
362394
assert cube.flat_graph() == {
363395
"loadcollection1": {
364396
"process_id": "load_collection",
@@ -379,7 +411,7 @@ def test_build_parameterized_cube_load_collection_invalid_bbox_schema(con100):
379411
bbox = Parameter.string("bbox", description="Spatial extent")
380412
with pytest.warns(
381413
UserWarning,
382-
match="Unexpected parameterized `spatial_extent` in `load_collection`: expected schema with type 'object' but got {'type': 'string'}.",
414+
match="Unexpected parameterized `spatial_extent` in `load_collection`: expected schema compatible with type 'object' but got {'type': 'string'}.",
383415
):
384416
cube = con100.load_collection(layer, spatial_extent=bbox, temporal_extent=dates)
385417

@@ -401,7 +433,7 @@ def test_build_parameterized_cube_filter_bbox_invalid_schema(con100):
401433
bbox = Parameter.string("bbox", description="Spatial extent")
402434
with pytest.warns(
403435
UserWarning,
404-
match="Unexpected parameterized `extent` in `filter_bbox`: expected schema with type 'object' but got {'type': 'string'}.",
436+
match="Unexpected parameterized `extent` in `filter_bbox`: expected schema compatible with type 'object' but got {'type': 'string'}.",
405437
):
406438
cube = con100.load_collection(layer).filter_bbox(bbox)
407439

0 commit comments

Comments
 (0)