Skip to content

Commit ac11868

Browse files
committed
Issue #678/#682 further finetuning
- doc and typing tweaks - push more functionality to _get_geometry_argument - add support in load_stac as well
1 parent d83b3c8 commit ac11868

File tree

4 files changed

+64
-35
lines changed

4 files changed

+64
-35
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12-
- Argument `spatial_extent` in `load_collection` supports Shapely objects and loading GeoJSON from a local path.
1312
- Added `show_error_logs` argument to `cube.execute_batch()`/`job.start_and_wait()`/... to toggle the automatic printing of error logs on failure ([#505](https://github.com/Open-EO/openeo-python-client/issues/505))
1413
- Added `Connection.web_editor()` to build link to the openEO backend in the openEO Web Editor
1514
- Add support for `log_level` in `create_job()` and `execute_job()` ([#704](https://github.com/Open-EO/openeo-python-client/issues/704))
1615
- Add initial support for "geometry" dimension type in `CubeMetadata` ([#705](https://github.com/Open-EO/openeo-python-client/issues/705))
1716
- Add support for parameterized `bands` argument in `load_stac()`
17+
- Argument `spatial_extent` in `load_collection()`/`load_stac()`: add support for Shapely objects and loading GeoJSON from a local path. ([#678](https://github.com/Open-EO/openeo-python-client/issues/678))
1818

1919
### Changed
2020

openeo/rest/connection.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,7 +1256,7 @@ def datacube_from_json(self, src: Union[str, Path], parameters: Optional[dict] =
12561256
def load_collection(
12571257
self,
12581258
collection_id: Union[str, Parameter],
1259-
spatial_extent: Union[Dict[str, float], Parameter, shapely.geometry.base.BaseGeometry, None] = None,
1259+
spatial_extent: Union[dict, Parameter, shapely.geometry.base.BaseGeometry, None] = None,
12601260
temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None,
12611261
bands: Union[Iterable[str], Parameter, str, None] = None,
12621262
properties: Union[
@@ -1272,8 +1272,8 @@ def load_collection(
12721272
:param spatial_extent: limit data to specified bounding box or polygons. Can be provided in different ways:
12731273
- a bounding box dictionary
12741274
- a Shapely geometry object
1275-
- a GeoJSON-style dictionary,
1276-
- a path (:py:class:`str` or :py:class:`~pathlib.Path`) to a local, client-side GeoJSON file,
1275+
- a GeoJSON-style dictionary
1276+
- a path (as :py:class:`str` or :py:class:`~pathlib.Path`) to a local, client-side GeoJSON file,
12771277
which will be loaded automatically to get the geometries as GeoJSON construct.
12781278
- a :py:class:`~openeo.api.process.Parameter` instance.
12791279
:param temporal_extent: limit data to specified temporal interval.
@@ -1296,7 +1296,7 @@ def load_collection(
12961296
Add :py:func:`~openeo.rest.graph_building.collection_property` support to ``properties`` argument.
12971297
12981298
.. versionchanged:: 0.37.0
1299-
Add support for passing a Shapely geometry or a local path to a GeoJSON file to the ``spatial_extent`` argument.
1299+
Argument ``spatial_extent``: add support for passing a Shapely geometry or a local path to a GeoJSON file.
13001300
"""
13011301
return DataCube.load_collection(
13021302
collection_id=collection_id,
@@ -1562,7 +1562,7 @@ def load_geojson(
15621562
return VectorCube.load_geojson(connection=self, data=data, properties=properties)
15631563

15641564
@openeo_process
1565-
def load_url(self, url: str, format: str, options: Optional[dict] = None):
1565+
def load_url(self, url: str, format: str, options: Optional[dict] = None) -> VectorCube:
15661566
"""
15671567
Loads a file from a URL
15681568

openeo/rest/datacube.py

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ def load_collection(
143143
cls,
144144
collection_id: Union[str, Parameter],
145145
connection: Optional[Connection] = None,
146-
spatial_extent: Union[Dict[str, float], Parameter, shapely.geometry.base.BaseGeometry, None] = None,
146+
spatial_extent: Union[dict, Parameter, shapely.geometry.base.BaseGeometry, str, pathlib.Path, None] = None,
147147
temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None,
148148
bands: Union[Iterable[str], Parameter, str, None] = None,
149149
fetch_metadata: bool = True,
@@ -161,8 +161,8 @@ def load_collection(
161161
:param spatial_extent: limit data to specified bounding box or polygons. Can be provided in different ways:
162162
- a bounding box dictionary
163163
- a Shapely geometry object
164-
- a GeoJSON-style dictionary,
165-
- a path (:py:class:`str` or :py:class:`~pathlib.Path`) to a local, client-side GeoJSON file,
164+
- a GeoJSON-style dictionary
165+
- a path (as :py:class:`str` or :py:class:`~pathlib.Path`) to a local, client-side GeoJSON file,
166166
which will be loaded automatically to get the geometries as GeoJSON construct.
167167
- a :py:class:`~openeo.api.process.Parameter` instance.
168168
:param temporal_extent: limit data to specified temporal interval.
@@ -185,27 +185,20 @@ def load_collection(
185185
Add :py:func:`~openeo.rest.graph_building.collection_property` support to ``properties`` argument.
186186
187187
.. versionchanged:: 0.37.0
188-
Add support for passing a Shapely geometry or a local path to a GeoJSON file to the ``spatial_extent`` argument.
188+
Argument ``spatial_extent``: add support for passing a Shapely geometry or a local path to a GeoJSON file.
189189
"""
190190
if temporal_extent:
191191
temporal_extent = cls._get_temporal_extent(extent=temporal_extent)
192-
193-
if isinstance(spatial_extent, Parameter):
194-
if not schema_supports(spatial_extent.schema, type="object"):
195-
warnings.warn(
196-
"Unexpected parameterized `spatial_extent` in `load_collection`:"
197-
f" expected schema compatible with type 'object' but got {spatial_extent.schema!r}."
198-
)
199-
elif spatial_extent is None or (
200-
isinstance(spatial_extent, dict) and spatial_extent.keys() & {"west", "east", "north", "south"}
201-
):
202-
pass
203-
else:
204-
valid_geojson_types = [
205-
"Polygon", "MultiPolygon", "Feature", "FeatureCollection"
206-
]
207-
spatial_extent = _get_geometry_argument(argument=spatial_extent, valid_geojson_types=valid_geojson_types,
208-
connection=connection)
192+
spatial_extent = _get_geometry_argument(
193+
argument=spatial_extent,
194+
valid_geojson_types=["Polygon", "MultiPolygon", "Feature", "FeatureCollection"],
195+
connection=connection,
196+
allow_none=True,
197+
allow_parameter=True,
198+
allow_bounding_box=True,
199+
argument_name="spatial_extent",
200+
process_id="load_collection",
201+
)
209202

210203
arguments = {
211204
'id': collection_id,
@@ -392,9 +385,18 @@ def load_stac(
392385
393386
"""
394387
arguments = {"url": url}
395-
# TODO #425 more normalization/validation of extent/band parameters
396388
if spatial_extent:
397-
arguments["spatial_extent"] = spatial_extent
389+
arguments["spatial_extent"] = _get_geometry_argument(
390+
argument=spatial_extent,
391+
valid_geojson_types=["Polygon", "MultiPolygon", "Feature", "FeatureCollection"],
392+
connection=connection,
393+
allow_none=True,
394+
allow_parameter=True,
395+
allow_bounding_box=True,
396+
argument_name="spatial_extent",
397+
process_id="load_stac",
398+
)
399+
398400
if temporal_extent:
399401
arguments["temporal_extent"] = DataCube._get_temporal_extent(extent=temporal_extent)
400402
bands = cls._get_bands(bands, process_id="load_stac")
@@ -2892,23 +2894,47 @@ def _get_geometry_argument(
28922894
Parameter,
28932895
_FromNodeMixin,
28942896
],
2897+
*,
28952898
valid_geojson_types: List[str],
28962899
connection: Connection = None,
28972900
crs: Optional[str] = None,
2898-
) -> Union[dict, Parameter, PGNode]:
2901+
allow_parameter: bool = True,
2902+
allow_bounding_box: bool = False,
2903+
allow_none: bool = False,
2904+
argument_name: str = "n/a",
2905+
process_id: str = "n/a",
2906+
) -> Union[dict, Parameter, PGNode, _FromNodeMixin, None]:
28992907
"""
2900-
Convert input to a geometry as "geojson" subtype object or vectorcube.
2908+
Convert input to a geometry as "geojson" subtype object or vector cube.
29012909
29022910
:param crs: value that encodes a coordinate reference system.
29032911
See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument.
2912+
:param allow_parameter: allow argument to be a :py:class:`Parameter` instance, and pass-through as such
2913+
:param allow_none: allow argument to be ``None`` and pass-through as such
2914+
:param allow_bounding_box: allow argument to be a bounding box dictionary and pass-through as such
29042915
"""
2905-
if isinstance(argument, Parameter):
2916+
# Some quick exit shortcuts
2917+
if allow_parameter and isinstance(argument, Parameter):
2918+
if not schema_supports(argument.schema, type="object"):
2919+
warnings.warn(
2920+
f"Unexpected parameterized `{argument_name}` in `{process_id}`:"
2921+
f" expected schema compatible with type 'object' but got {argument.schema!r}."
2922+
)
29062923
return argument
29072924
elif isinstance(argument, _FromNodeMixin):
2925+
# Typical use case here: VectorCube instance
29082926
return argument.from_node()
2927+
elif allow_none and argument is None:
2928+
return argument
2929+
elif (
2930+
allow_bounding_box
2931+
and isinstance(argument, dict)
2932+
and all(k in argument for k in ["west", "south", "east", "north"])
2933+
):
2934+
return argument
29092935

2936+
# Support URL based geometry references (with `load_url` and best-effort format guess)
29102937
if isinstance(argument, str) and re.match(r"^https?://", argument, flags=re.I):
2911-
# Geometry provided as URL: load with `load_url` (with best-effort format guess)
29122938
url = urllib.parse.urlparse(argument)
29132939
suffix = pathlib.Path(url.path.lower()).suffix
29142940
format = {
@@ -2919,7 +2945,8 @@ def _get_geometry_argument(
29192945
".geoparquet": "Parquet",
29202946
}.get(suffix, suffix.split(".")[-1])
29212947
return connection.load_url(url=argument, format=format)
2922-
#
2948+
2949+
# Support loading GeoJSON from local files
29232950
if (
29242951
isinstance(argument, (str, pathlib.Path))
29252952
and pathlib.Path(argument).is_file()
@@ -2933,6 +2960,8 @@ def _get_geometry_argument(
29332960
else:
29342961
raise OpenEoClientException(f"Invalid geometry argument: {argument!r}")
29352962

2963+
# The assumption at this point is that we are working with a GeoJSON style dictionary
2964+
assert isinstance(geometry, dict)
29362965
if geometry.get("type") not in valid_geojson_types:
29372966
raise OpenEoClientException("Invalid geometry type {t!r}, must be one of {s}".format(
29382967
t=geometry.get("type"), s=valid_geojson_types

tests/rest/datacube/test_datacube.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def test_load_collection_connectionless_temporal_extent_shortcut(self):
138138
}
139139

140140
def test_load_collection_connectionless_shapely_spatial_extent(self):
141-
polygon = shapely.Polygon(((0.0,1.0),(2.0,1.0),(3.0,2.0),(1.5,0.0),(0.0,1.0)))
141+
polygon = shapely.geometry.Polygon(((0.0, 1.0), (2.0, 1.0), (3.0, 2.0), (1.5, 0.0), (0.0, 1.0)))
142142
cube = DataCube.load_collection("T3", spatial_extent=polygon)
143143
assert cube.flat_graph() == {
144144
"loadcollection1": {

0 commit comments

Comments
 (0)