Skip to content

Commit d8ebc6a

Browse files
committed
#259 normalize "EPSG:123"-style CRS to integer
in bbox `spatial_extent` of `load_collection` and related
1 parent 5e17e18 commit d8ebc6a

File tree

4 files changed

+57
-4
lines changed

4 files changed

+57
-4
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
### Changed
1616

17+
- In bounding box `spatial_extent`s of `load_collection`, `load_stac`, ...: normalize non-standard "EPSG:123"-style CRS strings to proper integer value ([#259](https://github.com/Open-EO/openeo-python-client/issues/259), [Open-EO/openeo-processes#391](https://github.com/Open-EO/openeo-processes/issues/391))
18+
1719
### Removed
1820

1921
### Fixed

openeo/rest/datacube.py

+2
Original file line numberDiff line numberDiff line change
@@ -3043,6 +3043,8 @@ def _get_geometry_argument(
30433043
and isinstance(argument, dict)
30443044
and all(k in argument for k in ["west", "south", "east", "north"])
30453045
):
3046+
if "crs" in argument:
3047+
argument = dict(argument, crs=normalize_crs(argument["crs"], warn_on_change=True))
30463048
return argument
30473049

30483050
# Support URL based geometry references (with `load_url` and best-effort format guess)

openeo/util.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from __future__ import annotations
88

9+
import copy
910
import datetime as dt
1011
import functools
1112
import json
@@ -626,7 +627,7 @@ def get(self, fraction: float) -> str:
626627
return f"{self.left}{bar:{self.fill}<{width}s}{self.right}"
627628

628629

629-
def normalize_crs(crs: Any, *, use_pyproj: bool = True) -> Union[None, int, str]:
630+
def normalize_crs(crs: Any, *, use_pyproj: bool = True, warn_on_change: bool = False) -> Union[None, int, str]:
630631
"""
631632
Normalize the given value (describing a CRS or Coordinate Reference System)
632633
to an openEO compatible EPSG code (int) or WKT2 CRS string.
@@ -654,16 +655,18 @@ def normalize_crs(crs: Any, *, use_pyproj: bool = True) -> Union[None, int, str]
654655
:param use_pyproj: whether ``pyproj`` should be leveraged at all
655656
(mainly useful for testing the "no pyproj available" code path)
656657
658+
:param warn_on_change: whether to emit a warning when the normalization involves a change in value.
659+
657660
:return: EPSG code as int, or WKT2 string. Or None if input was empty.
658661
659662
:raises ValueError:
660663
When the given CRS data can not be parsed/converted/normalized.
661664
662665
"""
666+
orig_crs = copy.deepcopy(crs)
663667
if crs in (None, "", {}):
664-
return None
665-
666-
if pyproj and use_pyproj:
668+
crs = None
669+
elif pyproj and use_pyproj:
667670
try:
668671
# (if available:) let pyproj do the validation/parsing
669672
crs_obj = pyproj.CRS.from_user_input(crs)
@@ -689,4 +692,7 @@ def normalize_crs(crs: Any, *, use_pyproj: bool = True) -> Union[None, int, str]
689692
else:
690693
raise ValueError(f"Can not normalize CRS data {type(crs)}")
691694

695+
if warn_on_change and orig_crs != crs:
696+
logger.warning(f"Normalized CRS {orig_crs!r} to {crs!r}")
697+
692698
return crs

tests/rest/test_connection.py

+43
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from contextlib import nullcontext
1111
from pathlib import Path
1212

13+
import dirty_equals
1314
import pytest
1415
import requests
1516
import requests_mock
@@ -2703,6 +2704,31 @@ def test_load_collection_spatial_extent_vector_cube(self, dummy_backend):
27032704
},
27042705
}
27052706

2707+
@pytest.mark.parametrize("crs", ["EPSG:4326", "epsg:4326", 4326, "4326"])
2708+
def test_load_collection_normalize_epsg_crs(self, dummy_backend, crs, caplog):
2709+
caplog.set_level(level=logging.WARNING)
2710+
spatial_extent = {"west": 1, "south": 2, "east": 3, "north": 4, "crs": crs}
2711+
temporal_extent = ["2019-01-01", "2019-01-22"]
2712+
cube = dummy_backend.connection.load_collection(
2713+
"S2", spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=["B2", "B3"]
2714+
)
2715+
cube.execute()
2716+
assert dummy_backend.get_sync_pg() == {
2717+
"loadcollection1": {
2718+
"process_id": "load_collection",
2719+
"arguments": {
2720+
"id": "S2",
2721+
"spatial_extent": {"east": 3, "north": 4, "south": 2, "west": 1, "crs": 4326},
2722+
"temporal_extent": ["2019-01-01", "2019-01-22"],
2723+
"bands": ["B2", "B3"],
2724+
},
2725+
"result": True,
2726+
}
2727+
}
2728+
if crs != 4326:
2729+
assert f"Normalized CRS {crs!r} to 4326" in caplog.text
2730+
else:
2731+
assert "Normalized CRS" not in caplog.text
27062732

27072733
def test_load_result(requests_mock):
27082734
requests_mock.get(API_URL, json={"api_version": "1.0.0"})
@@ -3026,6 +3052,23 @@ def test_load_stac_spatial_extent_bbox(self, dummy_backend):
30263052
"spatial_extent": {"west": 1, "south": 2, "east": 3, "north": 4},
30273053
}
30283054

3055+
@pytest.mark.parametrize("crs", ["EPSG:32632", "epsg:32632", 32632, "32632"])
3056+
def test_load_stac_spatial_extent_bbox_normalize_epsg_crs(self, dummy_backend, crs, caplog):
3057+
caplog.set_level(level=logging.WARNING)
3058+
spatial_extent = {"west": 1, "south": 2, "east": 3, "north": 4, "crs": crs}
3059+
# TODO #694 how to avoid request to dummy STAC URL (without mocking, which is overkill for this test)
3060+
cube = dummy_backend.connection.load_stac("https://stac.test/data", spatial_extent=spatial_extent)
3061+
cube.execute()
3062+
assert dummy_backend.get_sync_pg()["loadstac1"]["arguments"] == {
3063+
"url": "https://stac.test/data",
3064+
"spatial_extent": {"west": 1, "south": 2, "east": 3, "north": 4, "crs": 32632},
3065+
}
3066+
3067+
if crs != 32632:
3068+
assert f"Normalized CRS {crs!r} to 32632" in caplog.text
3069+
else:
3070+
assert "Normalized CRS" not in caplog.text
3071+
30293072
@pytest.mark.parametrize(
30303073
"spatial_extent",
30313074
[

0 commit comments

Comments
 (0)