Skip to content

Commit f95728d

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 a72e3a5 commit f95728d

File tree

6 files changed

+81
-36
lines changed

6 files changed

+81
-36
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
- Add support for `log_level` in `create_job()` and `execute_job()` ([#704](https://github.com/Open-EO/openeo-python-client/issues/704))
1515
- Add initial support for "geometry" dimension type in `CubeMetadata` ([#705](https://github.com/Open-EO/openeo-python-client/issues/705))
1616
- Add support for parameterized `bands` argument in `load_stac()`
17-
- Argument `spatial_extent` in `load_collection` supports Shapely objects and loading GeoJSON from a local path.
17+
- Argument `spatial_extent` in `load_collection()`/`load_stac()`: add support for Shapely objects, loading GeoJSON from a local path and loading geometry from GeoJSON/GeoParquet URL. ([#678](https://github.com/Open-EO/openeo-python-client/issues/678))
1818

1919
### Changed
2020

openeo/api/process.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,9 @@ def schema_supports(schema: Union[dict, List[dict]], type: str, subtype: Optiona
467467
elif isinstance(actual_type, list):
468468
if type not in actual_type:
469469
return False
470+
elif actual_type is None:
471+
# Without explicit "type", anything is accepted
472+
return True
470473
else:
471474
raise ValueError(actual_type)
472475
if subtype:

openeo/rest/connection.py

Lines changed: 9 additions & 6 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, str, Path, 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,
@@ -1355,7 +1355,7 @@ def load_result(
13551355
def load_stac(
13561356
self,
13571357
url: str,
1358-
spatial_extent: Union[Dict[str, float], Parameter, None] = None,
1358+
spatial_extent: Union[dict, Parameter, shapely.geometry.base.BaseGeometry, str, Path, None] = None,
13591359
temporal_extent: Union[Sequence[InputDate], Parameter, str, None] = None,
13601360
bands: Union[Iterable[str], Parameter, str, None] = None,
13611361
properties: Optional[Dict[str, Union[str, PGNode, Callable]]] = None,
@@ -1457,6 +1457,9 @@ def load_stac(
14571457
.. versionchanged:: 0.23.0
14581458
Argument ``temporal_extent``: add support for year/month shorthand notation
14591459
as discussed at :ref:`date-shorthand-handling`.
1460+
1461+
.. versionchanged:: 0.37.0
1462+
Argument ``spatial_extent``: add support for passing a Shapely geometry or a local path to a GeoJSON file.
14601463
"""
14611464
return DataCube.load_stac(
14621465
url=url,
@@ -1562,7 +1565,7 @@ def load_geojson(
15621565
return VectorCube.load_geojson(connection=self, data=data, properties=properties)
15631566

15641567
@openeo_process
1565-
def load_url(self, url: str, format: str, options: Optional[dict] = None):
1568+
def load_url(self, url: str, format: str, options: Optional[dict] = None) -> VectorCube:
15661569
"""
15671570
Loads a file from a URL
15681571

openeo/rest/datacube.py

Lines changed: 59 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,
@@ -390,11 +383,22 @@ def load_stac(
390383
391384
.. versionadded:: 0.33.0
392385
386+
.. versionchanged:: 0.37.0
387+
Argument ``spatial_extent``: add support for passing a Shapely geometry or a local path to a GeoJSON file.
393388
"""
394389
arguments = {"url": url}
395-
# TODO #425 more normalization/validation of extent/band parameters
396390
if spatial_extent:
397-
arguments["spatial_extent"] = spatial_extent
391+
arguments["spatial_extent"] = _get_geometry_argument(
392+
argument=spatial_extent,
393+
valid_geojson_types=["Polygon", "MultiPolygon", "Feature", "FeatureCollection"],
394+
connection=connection,
395+
allow_none=True,
396+
allow_parameter=True,
397+
allow_bounding_box=True,
398+
argument_name="spatial_extent",
399+
process_id="load_stac",
400+
)
401+
398402
if temporal_extent:
399403
arguments["temporal_extent"] = DataCube._get_temporal_extent(extent=temporal_extent)
400404
bands = cls._get_bands(bands, process_id="load_stac")
@@ -2892,23 +2896,47 @@ def _get_geometry_argument(
28922896
Parameter,
28932897
_FromNodeMixin,
28942898
],
2899+
*,
28952900
valid_geojson_types: List[str],
28962901
connection: Connection = None,
28972902
crs: Optional[str] = None,
2898-
) -> Union[dict, Parameter, PGNode]:
2903+
allow_parameter: bool = True,
2904+
allow_bounding_box: bool = False,
2905+
allow_none: bool = False,
2906+
argument_name: str = "n/a",
2907+
process_id: str = "n/a",
2908+
) -> Union[dict, Parameter, PGNode, _FromNodeMixin, None]:
28992909
"""
2900-
Convert input to a geometry as "geojson" subtype object or vectorcube.
2910+
Convert input to a geometry as "geojson" subtype object or vector cube.
29012911
29022912
:param crs: value that encodes a coordinate reference system.
29032913
See :py:func:`openeo.util.normalize_crs` for more details about additional normalization that is applied to this argument.
2914+
:param allow_parameter: allow argument to be a :py:class:`Parameter` instance, and pass-through as such
2915+
:param allow_none: allow argument to be ``None`` and pass-through as such
2916+
:param allow_bounding_box: allow argument to be a bounding box dictionary and pass-through as such
29042917
"""
2905-
if isinstance(argument, Parameter):
2918+
# Some quick exit shortcuts
2919+
if allow_parameter and isinstance(argument, Parameter):
2920+
if not schema_supports(argument.schema, type="object"):
2921+
warnings.warn(
2922+
f"Unexpected parameterized `{argument_name}` in `{process_id}`:"
2923+
f" expected schema compatible with type 'object' but got {argument.schema!r}."
2924+
)
29062925
return argument
29072926
elif isinstance(argument, _FromNodeMixin):
2927+
# Typical use case here: VectorCube instance
29082928
return argument.from_node()
2929+
elif allow_none and argument is None:
2930+
return argument
2931+
elif (
2932+
allow_bounding_box
2933+
and isinstance(argument, dict)
2934+
and all(k in argument for k in ["west", "south", "east", "north"])
2935+
):
2936+
return argument
29092937

2938+
# Support URL based geometry references (with `load_url` and best-effort format guess)
29102939
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)
29122940
url = urllib.parse.urlparse(argument)
29132941
suffix = pathlib.Path(url.path.lower()).suffix
29142942
format = {
@@ -2919,7 +2947,8 @@ def _get_geometry_argument(
29192947
".geoparquet": "Parquet",
29202948
}.get(suffix, suffix.split(".")[-1])
29212949
return connection.load_url(url=argument, format=format)
2922-
#
2950+
2951+
# Support loading GeoJSON from local files
29232952
if (
29242953
isinstance(argument, (str, pathlib.Path))
29252954
and pathlib.Path(argument).is_file()
@@ -2933,6 +2962,8 @@ def _get_geometry_argument(
29332962
else:
29342963
raise OpenEoClientException(f"Invalid geometry argument: {argument!r}")
29352964

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

tests/api/test_process.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,11 @@ def test_schema_supports_list():
291291
assert schema_supports(schema, type="object") is True
292292
assert schema_supports(schema, type="object", subtype="datacube") is True
293293
assert schema_supports(schema, type="object", subtype="geojson") is False
294+
295+
296+
def test_default_parameter_supports_anything():
297+
parameter = Parameter(name="foo")
298+
assert schema_supports(parameter.schema, type="string") is True
299+
assert schema_supports(parameter.schema, type="number") is True
300+
assert schema_supports(parameter.schema, type="object") is True
301+
assert schema_supports(parameter.schema, type="object", subtype="datacube") is True

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)