Skip to content

Commit 68bd072

Browse files
committed
Merge branch 'issue717-drop-python38'
2 parents a0db65d + 8f0e6d4 commit 68bd072

12 files changed

Lines changed: 49 additions & 140 deletions

File tree

‎.github/workflows/unittests.yml‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ jobs:
2020
# Basic configurations to run on
2121
os: ["ubuntu-latest"]
2222
python-version:
23-
- "3.8"
2423
- "3.9"
2524
- "3.10"
2625
- "3.11"

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616

1717
### Removed
1818

19+
- Require at least Python 3.9 (drop support for Python 3.8, [#717](https://github.com/Open-EO/openeo-python-client/issues/717))
20+
1921
### Fixed
2022

2123
- `metadata_from_stac()` now keeps declared STAC `cube:dimensions` as the dimension source of truth and handles STAC 1.1 common `bands` metadata without requiring the datacube extension ([#743](https://github.com/Open-EO/openeo-python-client/issues/743), [#867](https://github.com/Open-EO/openeo-python-client/pull/867)).

‎Jenkinsfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
pythonPipeline {
1313
package_name = 'openeo'
1414
wipeout_workspace = true
15-
python_version = ["3.8"]
15+
python_version = ["3.11"]
1616
extras_require = 'tests'
1717
upload_dev_wheels = false
1818
wheel_repo = 'python-openeo'

‎openeo/extra/spectral_indices/spectral_indices.py‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import functools
2+
import importlib.resources
23
import json
34
import re
45
from typing import Dict, List, Optional, Set
@@ -8,11 +9,6 @@
89
from openeo.processes import ProcessBuilder, array_create, array_modify
910
from openeo.rest.datacube import DataCube
1011

11-
try:
12-
import importlib_resources
13-
except ImportError:
14-
import importlib.resources as importlib_resources
15-
1612

1713
@functools.lru_cache(maxsize=1)
1814
def load_indices() -> Dict[str, dict]:
@@ -26,7 +22,7 @@ def load_indices() -> Dict[str, dict]:
2622
# and provide an alternative mechanism to work with custom indices
2723
"resources/extra-indices-dict.json",
2824
]:
29-
resource = importlib_resources.files("openeo.extra.spectral_indices") / path
25+
resource = importlib.resources.files("openeo.extra.spectral_indices") / path
3026
data = json.loads(resource.read_text(encoding="utf8"))
3127
overwrites = set(specs.keys()).intersection(data["SpectralIndices"].keys())
3228
if overwrites:
@@ -41,7 +37,7 @@ def load_constants() -> Dict[str, float]:
4137
"""Load constants defined by Awesome Spectral Indices."""
4238
# TODO: encapsulate all this json loading in a single Awesome Spectral Indices registry class?
4339
resource = (
44-
importlib_resources.files("openeo.extra.spectral_indices") / "resources/awesome-spectral-indices/constants.json"
40+
importlib.resources.files("openeo.extra.spectral_indices") / "resources/awesome-spectral-indices/constants.json"
4541
)
4642
data = json.loads(resource.read_text(encoding="utf8"))
4743

@@ -53,7 +49,7 @@ def _load_bands() -> Dict[str, dict]:
5349
"""Load band name mapping defined by Awesome Spectral Indices."""
5450
# TODO: encapsulate all this json loading in a single Awesome Spectral Indices registry class?
5551
resource = (
56-
importlib_resources.files("openeo.extra.spectral_indices") / "resources/awesome-spectral-indices/bands.json"
52+
importlib.resources.files("openeo.extra.spectral_indices") / "resources/awesome-spectral-indices/bands.json"
5753
)
5854
data = json.loads(resource.read_text(encoding="utf8"))
5955
return data

‎openeo/metadata.py‎

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -693,9 +693,6 @@ def metadata_from_stac(url: str) -> CubeMetadata:
693693
parser = _StacMetadataParser()
694694
return parser.metadata_from_stac_object(stac_object)
695695

696-
# Sniff for PySTAC extension API since version 1.9.0 (which is not available below Python 3.9)
697-
# TODO: remove this once support for Python 3.7 and 3.8 is dropped
698-
_PYSTAC_1_9_EXTENSION_INTERFACE = hasattr(pystac.Item, "ext")
699696

700697
# Sniff for PySTAC support for Collection.item_assets (in STAC core since 1.1)
701698
# (supported since PySTAC 1.12.0, which requires Python>=3.10)
@@ -845,8 +842,7 @@ def _parse_declared_dimensions(self, stac_object: pystac.STACObject, bands: _Ban
845842
Parse dimensions declared through cube:dimensions.
846843
"""
847844
if (
848-
_PYSTAC_1_9_EXTENSION_INTERFACE
849-
and getattr(stac_object, "ext", None) is not None
845+
getattr(stac_object, "ext", None) is not None
850846
and stac_object.ext.has("cube")
851847
and hasattr(stac_object.ext, "cube")
852848
):
@@ -960,7 +956,7 @@ def bands_from_stac_catalog(self, catalog: pystac.Catalog, *, on_empty: str = _O
960956
summaries = catalog.extra_fields.get("summaries", {})
961957
self._warn(f"bands_from_stac_catalog with {summaries.keys()=} (which is non-standard)")
962958
if "eo:bands" in summaries:
963-
if _PYSTAC_1_9_EXTENSION_INTERFACE and not catalog.ext.has("eo"):
959+
if not catalog.ext.has("eo"):
964960
self._warn_undeclared_metadata(field="eo:bands", ext="eo")
965961
return _BandList(self._band_from_eo_bands_metadata(b) for b in summaries["eo:bands"])
966962
elif "bands" in summaries:
@@ -985,7 +981,7 @@ def bands_from_stac_collection(
985981
self._log(f"bands_from_stac_collection with {collection.summaries.lists.keys()=}")
986982
# Look for band metadata in collection summaries
987983
if "eo:bands" in collection.summaries.lists:
988-
if _PYSTAC_1_9_EXTENSION_INTERFACE and not collection.ext.has("eo"):
984+
if not collection.ext.has("eo"):
989985
self._warn_undeclared_metadata(field="eo:bands", ext="eo")
990986
return _BandList(self._band_from_eo_bands_metadata(b) for b in collection.summaries.lists["eo:bands"])
991987
elif "bands" in collection.summaries.lists:
@@ -998,8 +994,7 @@ def bands_from_stac_collection(
998994
elif _PYSTAC_1_12_ITEM_ASSETS and collection.item_assets:
999995
return self._bands_from_item_assets(collection.item_assets)
1000996
elif (
1001-
_PYSTAC_1_9_EXTENSION_INTERFACE
1002-
and collection.ext.has("item_assets")
997+
collection.ext.has("item_assets")
1003998
and collection.extra_fields.get("item-assets")
1004999
and collection.ext.item_assets
10051000
):
@@ -1068,10 +1063,10 @@ def bands_from_stac_asset(self, asset: pystac.Asset, *, on_empty: str = _ON_EMPT
10681063
"""
10691064
# TODO: "eo:bands" vs "bands" priority based on STAC and EO extension version information
10701065
# TODO: filter on asset roles?
1071-
if _PYSTAC_1_9_EXTENSION_INTERFACE and asset.owner and asset.ext.has("eo") and asset.ext.eo.bands is not None:
1066+
if asset.owner and asset.ext.has("eo") and asset.ext.eo.bands is not None:
10721067
return _BandList(self._band_from_eo_bands_metadata(b) for b in asset.ext.eo.bands)
10731068
elif "eo:bands" in asset.extra_fields:
1074-
if _PYSTAC_1_9_EXTENSION_INTERFACE and asset.owner and not asset.ext.has("eo"):
1069+
if asset.owner and not asset.ext.has("eo"):
10751070
self._warn_undeclared_metadata(field="eo:bands", ext="eo")
10761071
return _BandList(self._band_from_eo_bands_metadata(b) for b in asset.extra_fields["eo:bands"])
10771072
elif "bands" in asset.extra_fields:
@@ -1095,7 +1090,7 @@ def _bands_from_item_asset_definition(
10951090
"""
10961091
if isinstance(asset, pystac.extensions.item_assets.AssetDefinition):
10971092
if "eo:bands" in asset.properties:
1098-
if _PYSTAC_1_9_EXTENSION_INTERFACE and asset.owner and not asset.ext.has("eo"):
1093+
if asset.owner and not asset.ext.has("eo"):
10991094
self._warn_undeclared_metadata(field="eo:bands", ext="eo")
11001095
return _BandList(self._band_from_eo_bands_metadata(b) for b in asset.properties["eo:bands"])
11011096
elif "bands" in asset.properties:
@@ -1104,7 +1099,7 @@ def _bands_from_item_asset_definition(
11041099
if "bands" in asset.properties:
11051100
return _BandList(self._band_from_common_bands_metadata(b) for b in asset.properties["bands"])
11061101
elif "eo:bands" in asset.properties:
1107-
if _PYSTAC_1_9_EXTENSION_INTERFACE and asset.owner and not asset.ext.has("eo"):
1102+
if asset.owner and not asset.ext.has("eo"):
11081103
self._warn_undeclared_metadata(field="eo:bands", ext="eo")
11091104
return _BandList(self._band_from_eo_bands_metadata(b) for b in asset.properties["eo:bands"])
11101105
else:

‎openeo/udf/debug.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,4 @@ def inspect(data=None, message: str = "", code: str = "User", level: str = "info
2626
.. seealso:: :ref:`udf_logging_with_inspect`
2727
"""
2828
extra = {"data": data, "code": code}
29-
kwargs = {"stacklevel": 2} if sys.version_info >= (3, 8) else {}
30-
_user_log.log(level=logging.getLevelName(level.upper()), msg=message, extra=extra, **kwargs)
29+
_user_log.log(level=logging.getLevelName(level.upper()), msg=message, extra=extra, stacklevel=2)

‎pyproject.toml‎

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
[build-system]
2-
# Setuptools 75 is the latest version that works with Python 3.8.
32
requires = ["setuptools>=75"]
43
build-backend = "setuptools.build_meta"
54

@@ -9,14 +8,13 @@ dynamic = ["version"]
98
description = "Client API for openEO"
109
readme = "README.md"
1110
license = { text = "Apache-2.0" }
12-
requires-python = ">=3.8"
11+
requires-python = ">=3.9"
1312
authors = [
1413
{ name = "Jeroen Dries", email = "jeroen.dries@vito.be" },
1514
]
1615
classifiers = [
1716
"Development Status :: 5 - Production/Stable",
1817
"Operating System :: OS Independent",
19-
"Programming Language :: Python :: 3.8",
2018
"Programming Language :: Python :: 3.9",
2119
"Programming Language :: Python :: 3.10",
2220
"Programming Language :: Python :: 3.11",
@@ -26,16 +24,11 @@ classifiers = [
2624
]
2725
dependencies = [
2826
"deprecated>=1.2.12",
29-
# TODO #717 Simplify geopandas constraints when Python 3.8 support is dropped
30-
"geopandas", # Best-effort geopandas dependency for Python 3.8
31-
"geopandas>=0.14; python_version>='3.9'",
32-
"importlib_resources; python_version<'3.9'",
27+
"geopandas>=0.14",
3328
"numpy>=1.17.0",
3429
"oschmod>=0.3.12; sys_platform == \"win32\"",
3530
"pandas>0.20.0,<3.0.0", # TODO pandas 3 compatibility https://github.com/Open-EO/openeo-python-client/issues/856
36-
# TODO #578: pystac 1.5.0 is highest version available for lowest Python
37-
# version we still support (3.7).
38-
"pystac>=1.5.0",
31+
"pystac>=1.9.0",
3932
"requests>=2.26.0",
4033
"shapely>=1.6.4",
4134
"urllib3>=1.9.0",
@@ -49,20 +42,17 @@ artifacts = [
4942
]
5043
dev = [
5144
"boto3",
52-
"boto3~=1.37.38; python_version<'3.9'", # Pin to speed up slow dependency resolution on Python 3.8.
5345
"botocore",
54-
"botocore~=1.37.38; python_version<'3.9'", # Pin to speed up slow dependency resolution on Python 3.8.
5546
"dirty_equals>=0.8.0",
5647
"flake8>=5.0.0",
5748
"httpretty>=1.1.4",
5849
"mock",
5950
"moto>=5.0.0",
60-
"moto~=5.0.28; python_version<'3.9'", # Pin to speed up slow dependency resolution on Python 3.8.
6151
"myst-parser",
6252
"netCDF4>=1.7.0",
6353
"pyarrow>=10.0.1",
6454
"pydata_sphinx_theme",
65-
"pyproj>=3.2.0", # Pyproj is an optional, best-effort runtime dependency
55+
"pyproj>=3.3.0", # Pyproj is an optional, best-effort runtime dependency
6656
"pystac-client>=0.7.5",
6757
"pytest>=4.5.0",
6858
"python-dateutil>=2.7.0",
@@ -100,19 +90,15 @@ oschmod = [
10090
]
10191
tests = [
10292
"boto3",
103-
"boto3~=1.37.38; python_version<'3.9'", # Pin to speed up slow dependency resolution on Python 3.8.
10493
"botocore",
105-
"botocore~=1.37.38; python_version<'3.9'", # Pin to speed up slow dependency resolution on Python 3.8.
10694
"dirty_equals>=0.8.0",
10795
"flake8>=5.0.0",
10896
"httpretty>=1.1.4",
10997
"mock",
11098
"moto>=5.0.0",
111-
# Some pins to speed up slow dependency resolution in Python 3.8 venvs
112-
"moto~=5.0.28; python_version<'3.9'", # Pin to speed up slow dependency resolution on Python 3.8.
11399
"netCDF4>=1.7.0",
114100
"pyarrow>=10.0.1", # For Parquet read/write support in pandas
115-
"pyproj>=3.2.0",
101+
"pyproj>=3.3.0",
116102
"pystac-client>=0.7.5",
117103
"pytest>=4.5.0",
118104
"python-dateutil>=2.7.0",
@@ -140,7 +126,7 @@ version = {attr = "openeo._version.__version__"}
140126
include = ["openeo*"]
141127

142128
[tool.uv]
143-
# Version constraints for optionals aren't solvable with older Python versions.
129+
# Version constraints for some optional dependencies aren't solvable with older Python versions.
144130
environments = ["python_version >= '3.10'"]
145131

146132
[tool.black]

‎tests/extra/job_management/test_job_db.py‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,6 @@ def test_initialize_from_df_on_exists_skip(self, tmp_path):
268268
)
269269
assert set(db.read()["some_number"]) == {1, 2, 3}
270270

271-
@pytest.mark.skipif(
272-
ComparableVersion(geopandas.__version__) < "0.14",
273-
reason="This issue has no workaround with geopandas < 0.14 (highest available version on Python 3.8 is 0.13.2)",
274-
)
275271
def test_read_with_crs_column(self, tmp_path):
276272
"""
277273
Having a column named "crs" can cause obscure error messages when creating a GeoPandas dataframe

‎tests/rest/datacube/test_datacube100.py‎

Lines changed: 24 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -193,17 +193,6 @@
193193
}
194194

195195

196-
def _get_normalizable_crs_inputs():
197-
"""
198-
Dynamic (proj version based) generation of supported CRS inputs (to normalize).
199-
:return:
200-
"""
201-
yield "EPSG:32631"
202-
yield 32631
203-
yield "32631"
204-
yield "+proj=utm +zone=31 +datum=WGS84 +units=m +no_defs" # is also EPSG:32631, in proj format
205-
yield WKT2_FOR_EPSG23631
206-
207196

208197
def _get_leaf_node(cube: DataCube) -> dict:
209198
"""Get leaf node (node with result=True), supporting old and new style of graph building."""
@@ -561,7 +550,18 @@ def test_aggregate_spatial_types(con100: Connection, polygon, expected_geometrie
561550
}
562551

563552

564-
@pytest.mark.parametrize("crs", _get_normalizable_crs_inputs())
553+
@pytest.mark.parametrize(
554+
"crs",
555+
[
556+
"EPSG:32631",
557+
32631,
558+
"32631",
559+
"+proj=utm +zone=31 +datum=WGS84 +units=m +no_defs", # is also EPSG:32631, in proj format
560+
WKT2_FOR_EPSG23631,
561+
PROJJSON_FOR_EPSG23631,
562+
json.dumps(PROJJSON_FOR_EPSG23631),
563+
],
564+
)
565565
def test_aggregate_spatial_with_crs(con100: Connection, recwarn, crs: str):
566566
img = con100.load_collection("S2")
567567
polygon = shapely.geometry.box(0, 0, 1, 1)
@@ -588,36 +588,6 @@ def test_aggregate_spatial_with_crs(con100: Connection, recwarn, crs: str):
588588
}
589589

590590

591-
@pytest.mark.skipif(
592-
pyproj.__version__ < ComparableVersion("3.3.0"),
593-
reason="PROJJSON format support requires pyproj 3.3.0 or higher",
594-
)
595-
@pytest.mark.parametrize("crs", [PROJJSON_FOR_EPSG23631, json.dumps(PROJJSON_FOR_EPSG23631)])
596-
def test_aggregate_spatial_with_crs_as_projjson(con100: Connection, recwarn, crs):
597-
"""Separate test coverage for PROJJSON, so we can skip it for Python versions below 3.8"""
598-
img = con100.load_collection("S2")
599-
polygon = shapely.geometry.box(0, 0, 1, 1)
600-
masked = img.aggregate_spatial(geometries=polygon, reducer="mean", crs=crs)
601-
warnings = [str(w.message) for w in recwarn]
602-
assert f"Geometry with non-Lon-Lat CRS {crs!r} is only supported by specific back-ends." in warnings
603-
assert sorted(masked.flat_graph().keys()) == ["aggregatespatial1", "loadcollection1"]
604-
assert masked.flat_graph()["aggregatespatial1"] == {
605-
"process_id": "aggregate_spatial",
606-
"arguments": {
607-
"data": {"from_node": "loadcollection1"},
608-
"geometries": {
609-
"type": "Polygon",
610-
"coordinates": (((1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0), (1.0, 0.0)),),
611-
"crs": {"properties": {"name": "EPSG:32631"}, "type": "name"},
612-
},
613-
"reducer": {
614-
"process_graph": {
615-
"mean1": {"process_id": "mean", "arguments": {"data": {"from_parameter": "data"}}, "result": True}
616-
}
617-
},
618-
},
619-
"result": True,
620-
}
621591

622592

623593
@pytest.mark.parametrize(
@@ -906,7 +876,18 @@ def test_mask_polygon_types(con100: Connection, polygon, expected_mask):
906876
}
907877

908878

909-
@pytest.mark.parametrize("crs", _get_normalizable_crs_inputs())
879+
@pytest.mark.parametrize(
880+
"crs",
881+
[
882+
"EPSG:32631",
883+
32631,
884+
"32631",
885+
"+proj=utm +zone=31 +datum=WGS84 +units=m +no_defs", # is also EPSG:32631, in proj format
886+
WKT2_FOR_EPSG23631,
887+
PROJJSON_FOR_EPSG23631,
888+
json.dumps(PROJJSON_FOR_EPSG23631),
889+
],
890+
)
910891
def test_mask_polygon_with_crs(con100: Connection, recwarn, crs: str):
911892
img = con100.load_collection("S2")
912893
polygon = shapely.geometry.box(0, 0, 1, 1)
@@ -929,33 +910,6 @@ def test_mask_polygon_with_crs(con100: Connection, recwarn, crs: str):
929910
}
930911

931912

932-
@pytest.mark.skipif(
933-
pyproj.__version__ < ComparableVersion("3.3.0"),
934-
reason="PROJJSON format support requires pyproj 3.3.0 or higher",
935-
)
936-
@pytest.mark.parametrize("crs", [PROJJSON_FOR_EPSG23631, json.dumps(PROJJSON_FOR_EPSG23631)])
937-
def test_mask_polygon_with_crs_as_projjson(con100: Connection, recwarn, crs):
938-
"""Separate test coverage for PROJJSON, so we can skip it for Python versions below 3.8"""
939-
img = con100.load_collection("S2")
940-
polygon = shapely.geometry.box(0, 0, 1, 1)
941-
masked = img.mask_polygon(mask=polygon, srs=crs)
942-
warnings = [str(w.message) for w in recwarn]
943-
assert f"Geometry with non-Lon-Lat CRS {crs!r} is only supported by specific back-ends." in warnings
944-
assert sorted(masked.flat_graph().keys()) == ["loadcollection1", "maskpolygon1"]
945-
assert masked.flat_graph()["maskpolygon1"] == {
946-
"process_id": "mask_polygon",
947-
"arguments": {
948-
"data": {"from_node": "loadcollection1"},
949-
"mask": {
950-
"type": "Polygon",
951-
"coordinates": (((1.0, 0.0), (1.0, 1.0), (0.0, 1.0), (0.0, 0.0), (1.0, 0.0)),),
952-
# All listed test inputs for crs should be converted to "EPSG:32631"
953-
"crs": {"type": "name", "properties": {"name": "EPSG:32631"}},
954-
},
955-
},
956-
"result": True,
957-
}
958-
959913

960914
def test_mask_polygon_parameter(con100: Connection):
961915
img = con100.load_collection("S2")

0 commit comments

Comments
 (0)