Skip to content

Commit 289ca14

Browse files
committed
Tighten resample_spatial/resample_cube_spatial argument handling
related to #347 and Open-EO/openeo-python-client#690
1 parent df49864 commit 289ca14

File tree

5 files changed

+170
-16
lines changed

5 files changed

+170
-16
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and start a new "In Progress" section above it.
2121

2222
## In progress
2323

24+
- Better argument validation in `resample_spatial`/`resample_cube_spatial` (related to [Open-EO/openeo-python-client#690](https://github.com/Open-EO/openeo-python-client/issues/690))
2425

2526
## 0.123.0
2627

openeo_driver/ProcessGraphDeserializer.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
Processing,
4141
OpenEoBackendImplementation,
4242
)
43+
from openeo_driver.constants import RESAMPLE_SPATIAL_METHODS, RESAMPLE_SPATIAL_ALIGNS
4344
from openeo_driver.datacube import (
4445
DriverDataCube,
4546
DriverVectorCube,
@@ -1582,24 +1583,23 @@ def ndvi(args: dict, env: EvalEnv) -> DriverDataCube:
15821583
@process
15831584
def resample_spatial(args: ProcessArgs, env: EvalEnv) -> DriverDataCube:
15841585
cube: DriverDataCube = args.get_required("data", expected_type=DriverDataCube)
1585-
resolution = args.get_optional("resolution", 0)
1586-
projection = args.get_optional("projection", None)
1587-
method = args.get_optional("method", "near")
1588-
align = args.get_optional("align", "upper-left")
1586+
resolution = args.get_optional(
1587+
"resolution",
1588+
default=0,
1589+
validator=lambda v: isinstance(v, (int, float)) or (isinstance(v, (tuple, list)) and len(v) == 2),
1590+
)
1591+
projection = args.get_optional("projection", default=None)
1592+
method = args.get_enum("method", options=RESAMPLE_SPATIAL_METHODS, default="near")
1593+
align = args.get_enum("align", options=RESAMPLE_SPATIAL_ALIGNS, default="upper-left")
15891594
return cube.resample_spatial(resolution=resolution, projection=projection, method=method, align=align)
15901595

15911596

15921597
@process
1593-
def resample_cube_spatial(args: dict, env: EvalEnv) -> DriverDataCube:
1594-
image_collection = extract_arg(args, 'data')
1595-
target_image_collection = extract_arg(args, 'target')
1596-
method = args.get('method', 'near')
1597-
if not isinstance(image_collection, DriverDataCube):
1598-
raise ProcessParameterInvalidException(
1599-
parameter="data", process="resample_cube_spatial",
1600-
reason=f"Invalid data type {type(image_collection)!r} expected raster-cube."
1601-
)
1602-
return image_collection.resample_cube_spatial(target=target_image_collection, method=method)
1598+
def resample_cube_spatial(args: ProcessArgs, env: EvalEnv) -> DriverDataCube:
1599+
cube: DriverDataCube = args.get_required("data", expected_type=DriverDataCube)
1600+
target: DriverDataCube = args.get_required("target", expected_type=DriverDataCube)
1601+
method = args.get_enum("method", options=RESAMPLE_SPATIAL_METHODS, default="near")
1602+
return cube.resample_cube_spatial(target=target, method=method)
16031603

16041604

16051605
@process

openeo_driver/constants.py

+27
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,30 @@ class JOB_STATUS:
2222
CANCELED = "canceled"
2323
FINISHED = "finished"
2424
ERROR = "error"
25+
26+
27+
# Resample methods as used in official specs of `resample_spatial` and `resample_cube_spatial`
28+
RESAMPLE_SPATIAL_METHODS = [
29+
"average",
30+
"bilinear",
31+
"cubic",
32+
"cubicspline",
33+
"lanczos",
34+
"max",
35+
"med",
36+
"min",
37+
"mode",
38+
"near",
39+
"q1",
40+
"q3",
41+
"rms",
42+
"sum",
43+
]
44+
45+
# Align options as used in official spec of `resample_spatial`
46+
RESAMPLE_SPATIAL_ALIGNS = [
47+
"lower-left",
48+
"upper-left",
49+
"lower-right",
50+
"upper-right",
51+
]

tests/data/pg/1.0/resample_and_merge_cubes.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"target": {
2121
"from_node": "collection2"
2222
},
23-
"method": "cube"
23+
"method": "cubic"
2424
},
2525
"result": false
2626
},
@@ -52,4 +52,4 @@
5252
},
5353
"result": true
5454
}
55-
}
55+
}

tests/test_views_execute.py

+126
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,131 @@ def test_execute_merge_cubes(api):
514514
assert args[1:] == ('or',)
515515

516516

517+
def test_execute_resample_spatial_defaults(api):
518+
api.check_result(
519+
{
520+
"lc": {"process_id": "load_collection", "arguments": {"id": "S2_FOOBAR"}},
521+
"resample": {
522+
"process_id": "resample_spatial",
523+
"arguments": {"data": {"from_node": "lc"}},
524+
"result": True,
525+
},
526+
}
527+
)
528+
dummy = dummy_backend.get_collection("S2_FOOBAR")
529+
assert dummy.resample_spatial.call_count == 1
530+
assert dummy.resample_spatial.call_args.kwargs == {
531+
"resolution": 0,
532+
"projection": None,
533+
"method": "near",
534+
"align": "upper-left",
535+
}
536+
537+
538+
def test_execute_resample_spatial_custom(api):
539+
api.check_result(
540+
{
541+
"lc": {"process_id": "load_collection", "arguments": {"id": "S2_FOOBAR"}},
542+
"resample": {
543+
"process_id": "resample_spatial",
544+
"arguments": {
545+
"data": {"from_node": "lc"},
546+
"resolution": [11, 123],
547+
"projection": 3857,
548+
"method": "cubic",
549+
"align": "lower-right",
550+
},
551+
"result": True,
552+
},
553+
}
554+
)
555+
dummy = dummy_backend.get_collection("S2_FOOBAR")
556+
assert dummy.resample_spatial.call_count == 1
557+
assert dummy.resample_spatial.call_args.kwargs == {
558+
"resolution": [11, 123],
559+
"projection": 3857,
560+
"method": "cubic",
561+
"align": "lower-right",
562+
}
563+
564+
565+
@pytest.mark.parametrize(
566+
"kwargs",
567+
[
568+
{"resolution": [1, 2, 3, 4, 5]},
569+
{"method": "glossy"},
570+
{"align": "justified"},
571+
],
572+
)
573+
def test_execute_resample_spatial_invalid(api, kwargs):
574+
res = api.result(
575+
{
576+
"lc": {"process_id": "load_collection", "arguments": {"id": "S2_FOOBAR"}},
577+
"resample": {
578+
"process_id": "resample_spatial",
579+
"arguments": {"data": {"from_node": "lc"}, **kwargs},
580+
"result": True,
581+
},
582+
}
583+
)
584+
res.assert_error(status_code=400, error_code="ProcessParameterInvalid")
585+
586+
587+
def test_execute_resample_cube_spatial_defaults(api):
588+
api.check_result(
589+
{
590+
"lc1": {"process_id": "load_collection", "arguments": {"id": "S2_FOOBAR"}},
591+
"lc2": {"process_id": "load_collection", "arguments": {"id": "SENTINEL1_GRD"}},
592+
"resample": {
593+
"process_id": "resample_cube_spatial",
594+
"arguments": {"data": {"from_node": "lc1"}, "target": {"from_node": "lc2"}},
595+
"result": True,
596+
},
597+
}
598+
)
599+
cube1 = dummy_backend.get_collection("S2_FOOBAR")
600+
cube2 = dummy_backend.get_collection("SENTINEL1_GRD")
601+
assert cube1.resample_cube_spatial.call_count == 1
602+
assert cube1.resample_cube_spatial.call_args.kwargs == {"target": cube2, "method": "near"}
603+
604+
605+
def test_execute_resample_cube_spatial_custom(api):
606+
api.check_result(
607+
{
608+
"lc1": {"process_id": "load_collection", "arguments": {"id": "S2_FOOBAR"}},
609+
"lc2": {"process_id": "load_collection", "arguments": {"id": "SENTINEL1_GRD"}},
610+
"resample": {
611+
"process_id": "resample_cube_spatial",
612+
"arguments": {"data": {"from_node": "lc1"}, "target": {"from_node": "lc2"}, "method": "lanczos"},
613+
"result": True,
614+
},
615+
}
616+
)
617+
cube1 = dummy_backend.get_collection("S2_FOOBAR")
618+
cube2 = dummy_backend.get_collection("SENTINEL1_GRD")
619+
assert cube1.resample_cube_spatial.call_count == 1
620+
assert cube1.resample_cube_spatial.call_args.kwargs == {"target": cube2, "method": "lanczos"}
621+
622+
623+
def test_execute_resample_cube_spatial_invalid(api):
624+
res = api.result(
625+
{
626+
"lc1": {"process_id": "load_collection", "arguments": {"id": "S2_FOOBAR"}},
627+
"lc2": {"process_id": "load_collection", "arguments": {"id": "SENTINEL1_GRD"}},
628+
"resample": {
629+
"process_id": "resample_cube_spatial",
630+
"arguments": {"data": {"from_node": "lc1"}, "target": {"from_node": "lc2"}, "method": "du chef"},
631+
"result": True,
632+
},
633+
}
634+
)
635+
res.assert_error(
636+
status_code=400,
637+
error_code="ProcessParameterInvalid",
638+
message=re.compile(r"Invalid enum value 'du chef'\. Expected one of.*cubic.*near"),
639+
)
640+
641+
517642
def test_execute_resample_and_merge_cubes(api):
518643
api.check_result("resample_and_merge_cubes.json")
519644
dummy = dummy_backend.get_collection("S2_FAPAR_CLOUDCOVER")
@@ -522,6 +647,7 @@ def test_execute_resample_and_merge_cubes(api):
522647
assert last_load_collection_call.target_resolution == [10, 10]
523648
assert dummy.merge_cubes.call_count == 1
524649
assert dummy.resample_cube_spatial.call_count == 1
650+
assert dummy.resample_cube_spatial.call_args.kwargs["method"] == "cubic"
525651
args, kwargs = dummy.merge_cubes.call_args
526652
assert args[1:] == ('or',)
527653

0 commit comments

Comments
 (0)