Skip to content

Commit

Permalink
Issue #592 DataCube.apply_polygon(): rename polygons argument to …
Browse files Browse the repository at this point in the history
…`geometries`
  • Loading branch information
soxofaan committed Sep 17, 2024
1 parent 33a1860 commit 21922f1
Show file tree
Hide file tree
Showing 3 changed files with 144 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- `MultiBackendJobManager`: changed job metadata storage API, to enable working with large databases
- `DataCube.apply_polygon()`: rename `polygons` argument to `geometries`, but keep support for legacy `polygons` for now ([#592](https://github.com/Open-EO/openeo-python-client/issues/592), [#511](https://github.com/Open-EO/openeo-processes/issues/511))

### Removed

Expand Down
45 changes: 37 additions & 8 deletions openeo/rest/datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,10 +1305,11 @@ def chunk_polygon(
@openeo_process
def apply_polygon(
self,
polygons: Union[shapely.geometry.base.BaseGeometry, dict, str, pathlib.Path, Parameter, VectorCube],
process: Union[str, PGNode, typing.Callable, UDF],
geometries: Union[shapely.geometry.base.BaseGeometry, dict, str, pathlib.Path, Parameter, VectorCube] = None,
process: Union[str, PGNode, typing.Callable, UDF] = None,
mask_value: Optional[float] = None,
context: Optional[dict] = None,
**kwargs,
) -> DataCube:
"""
Apply a process to segments of the data cube that are defined by the given polygons.
Expand All @@ -1318,22 +1319,50 @@ def apply_polygon(
the GeometriesOverlap exception is thrown.
Each sub data cube is passed individually to the given process.
.. warning:: experimental process: not generally supported, API subject to change.
:param polygons: Polygons, provided as a shapely geometry, a GeoJSON-style dictionary,
:param geometries: Polygons, provided as a shapely geometry, a GeoJSON-style dictionary,
a public GeoJSON URL, or a path (that is valid for the back-end) to a GeoJSON file.
:param process: "child callback" function, see :ref:`callbackfunctions`
:param mask_value: The value used for pixels outside the polygon.
:param context: Additional data to be passed to the process.
"""
.. warning:: experimental process: not generally supported, API subject to change.
.. versionchanged:: 0.32.0
Argument ``polygons`` was renamed to ``geometries``.
While deprecated, the old name ``polygons`` is still supported
as keyword argument for backwards compatibility.
"""
# TODO drop support for legacy `polygons` argument:
# remove `kwargs, remove default `None` value for `geometries` and `process`
# and the related backwards compatibility code
geometries_parameter = "geometries"
if geometries is None and "polygons" in kwargs:
geometries = kwargs.pop("polygons")
geometries_parameter = "polygons"
warnings.warn(
"In `apply_polygon` use argument `geometries` instead of deprecated 'polygons'.",
category=UserDeprecationWarning,
stacklevel=2,
)
if kwargs:
raise ValueError(f"Unexpected keyword arguments: {kwargs!r}")
if not geometries:
raise ValueError("No geometries provided.")

# Note: the `process` argument was given a default value `None` (with the `polygons`/`geometries` argument rename)
# to keep support for legacy `cube.apply_polygon(polygons=..., process=...)` usage:
# `geometries` had to be given a default value, and so did `process` as it comes after it.
# TODO: remove default value for `process` when dropping support for legacy `polygons` argument
assert process is not None

process = build_child_callback(process, parent_parameters=["data"], connection=self.connection)
valid_geojson_types = ["Polygon", "MultiPolygon", "Feature", "FeatureCollection"]
polygons = self._get_geometry_argument(polygons, valid_geojson_types=valid_geojson_types)
geometries = self._get_geometry_argument(geometries, valid_geojson_types=valid_geojson_types)
mask_value = float(mask_value) if mask_value is not None else None
return self.process(
process_id="apply_polygon",
data=THIS,
polygons=polygons,
**{geometries_parameter: geometries},
process=process,
arguments=dict_no_none(
mask_value=mask_value,
Expand Down
125 changes: 106 additions & 19 deletions tests/rest/datacube/test_datacube100.py
Original file line number Diff line number Diff line change
Expand Up @@ -1556,15 +1556,18 @@ def test_chunk_polygon_context(con100: Connection):
}


def test_apply_polygon_basic(con100: Connection):
def test_apply_polygon_basic_legacy(con100: Connection):
cube = con100.load_collection("S2")
polygons: shapely.geometry.Polygon = shapely.geometry.box(0, 0, 1, 1)
geometries = shapely.geometry.box(0, 0, 1, 1)
process = UDF(code="myfancycode", runtime="Python")
result = cube.apply_polygon(polygons=polygons, process=process)
result = cube.apply_polygon(polygons=geometries, process=process)
assert get_download_graph(result)["applypolygon1"] == {
"process_id": "apply_polygon",
"arguments": {
"data": {"from_node": "loadcollection1"},
# TODO #592: For now, stick to legacy "polygons" argument in this case.
# But eventually: change argument name to "geometries"
# when https://github.com/Open-EO/openeo-python-driver/commit/15b72a77 propagated to all backends
"polygons": {
"type": "Polygon",
"coordinates": [[[1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0], [1.0, 0.0]]],
Expand All @@ -1582,18 +1585,80 @@ def test_apply_polygon_basic(con100: Connection):
}


@pytest.mark.parametrize(["polygons", "expected_polygons"], basic_geometry_types)
def test_apply_polygon_types(con100: Connection, polygons, expected_polygons):
if isinstance(polygons, shapely.geometry.GeometryCollection):
def test_apply_polygon_basic(con100: Connection):
cube = con100.load_collection("S2")
geometries = shapely.geometry.box(0, 0, 1, 1)
process = UDF(code="myfancycode", runtime="Python")
result = cube.apply_polygon(geometries=geometries, process=process)
assert get_download_graph(result)["applypolygon1"] == {
"process_id": "apply_polygon",
"arguments": {
"data": {"from_node": "loadcollection1"},
"geometries": {
"type": "Polygon",
"coordinates": [[[1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0], [1.0, 0.0]]],
},
"process": {
"process_graph": {
"runudf1": {
"process_id": "run_udf",
"arguments": {"data": {"from_parameter": "data"}, "runtime": "Python", "udf": "myfancycode"},
"result": True,
}
}
},
},
}


def test_apply_polygon_basic_positional(con100: Connection):
"""DataCube.apply_polygon() with positional arguments."""
cube = con100.load_collection("S2")
geometries = shapely.geometry.box(0, 0, 1, 1)
process = UDF(code="myfancycode", runtime="Python")
result = cube.apply_polygon(geometries, process)
assert get_download_graph(result)["applypolygon1"] == {
"process_id": "apply_polygon",
"arguments": {
"data": {"from_node": "loadcollection1"},
"geometries": {
"type": "Polygon",
"coordinates": [[[1.0, 0.0], [1.0, 1.0], [0.0, 1.0], [0.0, 0.0], [1.0, 0.0]]],
},
"process": {
"process_graph": {
"runudf1": {
"process_id": "run_udf",
"arguments": {"data": {"from_parameter": "data"}, "runtime": "Python", "udf": "myfancycode"},
"result": True,
}
}
},
},
}


@pytest.mark.parametrize(
["geometries_argument", "geometries_parameter"],
[
("polygons", "polygons"), # TODO #592: *parameter* "polygons" for now, eventually change to "geometries"
("geometries", "geometries"),
],
)
@pytest.mark.parametrize(["geometries", "expected_polygons"], basic_geometry_types)
def test_apply_polygon_types(
con100: Connection, geometries, expected_polygons, geometries_argument, geometries_parameter
):
if isinstance(geometries, shapely.geometry.GeometryCollection):
pytest.skip("apply_polygon does not support GeometryCollection")
cube = con100.load_collection("S2")
process = UDF(code="myfancycode", runtime="Python")
result = cube.apply_polygon(polygons=polygons, process=process)
result = cube.apply_polygon(**{geometries_argument: geometries}, process=process)
assert get_download_graph(result)["applypolygon1"] == {
"process_id": "apply_polygon",
"arguments": {
"data": {"from_node": "loadcollection1"},
"polygons": expected_polygons,
geometries_parameter: expected_polygons,
"process": {
"process_graph": {
"runudf1": {
Expand All @@ -1607,16 +1672,23 @@ def test_apply_polygon_types(con100: Connection, polygons, expected_polygons):
}


def test_apply_polygon_parameter(con100: Connection):
@pytest.mark.parametrize(
["geometries_argument", "geometries_parameter"],
[
("polygons", "polygons"), # TODO #592: *parameter* "polygons" for now, eventually change to "geometries"
("geometries", "geometries"),
],
)
def test_apply_polygon_parameter(con100: Connection, geometries_argument, geometries_parameter):
cube = con100.load_collection("S2")
polygons = Parameter(name="shapes", schema="object")
geometries = Parameter(name="shapes", schema="object")
process = UDF(code="myfancycode", runtime="Python")
result = cube.apply_polygon(polygons=polygons, process=process)
result = cube.apply_polygon(**{geometries_argument: geometries}, process=process)
assert get_download_graph(result)["applypolygon1"] == {
"process_id": "apply_polygon",
"arguments": {
"data": {"from_node": "loadcollection1"},
"polygons": {"from_parameter": "shapes"},
geometries_parameter: {"from_parameter": "shapes"},
"process": {
"process_graph": {
"runudf1": {
Expand All @@ -1630,20 +1702,28 @@ def test_apply_polygon_parameter(con100: Connection):
}


def test_apply_polygon_path(con100: Connection):
@pytest.mark.parametrize(
["geometries_argument", "geometries_parameter"],
[
("polygons", "polygons"), # TODO #592: *parameter* "polygons" for now, eventually change to "geometries"
("geometries", "geometries"),
],
)
def test_apply_polygon_path(con100: Connection, geometries_argument, geometries_parameter):
cube = con100.load_collection("S2")
process = UDF(code="myfancycode", runtime="Python")
result = cube.apply_polygon(polygons="path/to/polygon.json", process=process)
result = cube.apply_polygon(**{geometries_argument: "path/to/polygon.json"}, process=process)
assert get_download_graph(result, drop_save_result=True, drop_load_collection=True) == {
"readvector1": {
# TODO #104 #457 get rid of non-standard read_vector
"process_id": "read_vector",
"arguments": {"filename": "path/to/polygon.json"},
},
"applypolygon1": {
"process_id": "apply_polygon",
"arguments": {
"data": {"from_node": "loadcollection1"},
"polygons": {"from_node": "readvector1"},
geometries_parameter: {"from_node": "readvector1"},
"process": {
"process_graph": {
"runudf1": {
Expand All @@ -1662,16 +1742,23 @@ def test_apply_polygon_path(con100: Connection):
}


def test_apply_polygon_context(con100: Connection):
@pytest.mark.parametrize(
["geometries_argument", "geometries_parameter"],
[
("polygons", "polygons"), # TODO #592: *parameter* "polygons" for now, eventually change to "geometries"
("geometries", "geometries"),
],
)
def test_apply_polygon_context(con100: Connection, geometries_argument, geometries_parameter):
cube = con100.load_collection("S2")
polygons = shapely.geometry.Polygon([(0, 0), (1, 0), (0, 1), (0, 0)])
geometries = shapely.geometry.Polygon([(0, 0), (1, 0), (0, 1), (0, 0)])
process = UDF(code="myfancycode", runtime="Python")
result = cube.apply_polygon(polygons=polygons, process=process, context={"foo": 4})
result = cube.apply_polygon(**{geometries_argument: geometries}, process=process, context={"foo": 4})
assert get_download_graph(result)["applypolygon1"] == {
"process_id": "apply_polygon",
"arguments": {
"data": {"from_node": "loadcollection1"},
"polygons": {"type": "Polygon", "coordinates": [[[0, 0], [1, 0], [0, 1], [0, 0]]]},
geometries_parameter: {"type": "Polygon", "coordinates": [[[0, 0], [1, 0], [0, 1], [0, 0]]]},
"process": {
"process_graph": {
"runudf1": {
Expand Down

0 comments on commit 21922f1

Please sign in to comment.