Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- 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))

### Removed

### Fixed
Expand Down
2 changes: 2 additions & 0 deletions openeo/rest/datacube.py
Original file line number Diff line number Diff line change
Expand Up @@ -3043,6 +3043,8 @@ def _get_geometry_argument(
and isinstance(argument, dict)
and all(k in argument for k in ["west", "south", "east", "north"])
):
if "crs" in argument:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just use to_bbox_dict (or related) here instead of reinventing that wheel?

argument = dict(argument, crs=normalize_crs(argument["crs"], warn_on_change=True))
return argument

# Support URL based geometry references (with `load_url` and best-effort format guess)
Expand Down
14 changes: 10 additions & 4 deletions openeo/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from __future__ import annotations

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


def normalize_crs(crs: Any, *, use_pyproj: bool = True) -> Union[None, int, str]:
def normalize_crs(crs: Any, *, use_pyproj: bool = True, warn_on_change: bool = False) -> Union[None, int, str]:
"""
Normalize the given value (describing a CRS or Coordinate Reference System)
to an openEO compatible EPSG code (int) or WKT2 CRS string.
Expand Down Expand Up @@ -654,16 +655,18 @@ def normalize_crs(crs: Any, *, use_pyproj: bool = True) -> Union[None, int, str]
:param use_pyproj: whether ``pyproj`` should be leveraged at all
(mainly useful for testing the "no pyproj available" code path)

:param warn_on_change: whether to emit a warning when the normalization involves a change in value.

:return: EPSG code as int, or WKT2 string. Or None if input was empty.

:raises ValueError:
When the given CRS data can not be parsed/converted/normalized.

"""
orig_crs = copy.deepcopy(crs)
if crs in (None, "", {}):
return None

if pyproj and use_pyproj:
crs = None
elif pyproj and use_pyproj:
try:
# (if available:) let pyproj do the validation/parsing
crs_obj = pyproj.CRS.from_user_input(crs)
Expand All @@ -689,4 +692,7 @@ def normalize_crs(crs: Any, *, use_pyproj: bool = True) -> Union[None, int, str]
else:
raise ValueError(f"Can not normalize CRS data {type(crs)}")

if warn_on_change and orig_crs != crs:
logger.warning(f"Normalized CRS {orig_crs!r} to {crs!r}")

return crs
43 changes: 43 additions & 0 deletions tests/rest/test_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from contextlib import nullcontext
from pathlib import Path

import dirty_equals
import pytest
import requests
import requests_mock
Expand Down Expand Up @@ -2703,6 +2704,31 @@ def test_load_collection_spatial_extent_vector_cube(self, dummy_backend):
},
}

@pytest.mark.parametrize("crs", ["EPSG:4326", "epsg:4326", 4326, "4326"])
def test_load_collection_normalize_epsg_crs(self, dummy_backend, crs, caplog):
caplog.set_level(level=logging.WARNING)
spatial_extent = {"west": 1, "south": 2, "east": 3, "north": 4, "crs": crs}
temporal_extent = ["2019-01-01", "2019-01-22"]
cube = dummy_backend.connection.load_collection(
"S2", spatial_extent=spatial_extent, temporal_extent=temporal_extent, bands=["B2", "B3"]
)
cube.execute()
assert dummy_backend.get_sync_pg() == {
"loadcollection1": {
"process_id": "load_collection",
"arguments": {
"id": "S2",
"spatial_extent": {"east": 3, "north": 4, "south": 2, "west": 1, "crs": 4326},
"temporal_extent": ["2019-01-01", "2019-01-22"],
"bands": ["B2", "B3"],
},
"result": True,
}
}
if crs != 4326:
assert f"Normalized CRS {crs!r} to 4326" in caplog.text
else:
assert "Normalized CRS" not in caplog.text

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

@pytest.mark.parametrize("crs", ["EPSG:32632", "epsg:32632", 32632, "32632"])
def test_load_stac_spatial_extent_bbox_normalize_epsg_crs(self, dummy_backend, crs, caplog):
caplog.set_level(level=logging.WARNING)
spatial_extent = {"west": 1, "south": 2, "east": 3, "north": 4, "crs": crs}
# TODO #694 how to avoid request to dummy STAC URL (without mocking, which is overkill for this test)
cube = dummy_backend.connection.load_stac("https://stac.test/data", spatial_extent=spatial_extent)
cube.execute()
assert dummy_backend.get_sync_pg()["loadstac1"]["arguments"] == {
"url": "https://stac.test/data",
"spatial_extent": {"west": 1, "south": 2, "east": 3, "north": 4, "crs": 32632},
}

if crs != 32632:
assert f"Normalized CRS {crs!r} to 32632" in caplog.text
else:
assert "Normalized CRS" not in caplog.text

@pytest.mark.parametrize(
"spatial_extent",
[
Expand Down