Skip to content

Commit 3a215b5

Browse files
committed
Merge remote-tracking branch 'origin/master' into issue816_threadeddownload
2 parents c999d1f + e0393e7 commit 3a215b5

File tree

13 files changed

+210
-12
lines changed

13 files changed

+210
-12
lines changed

.github/workflows/unittests.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727
- "3.11"
2828
- "3.12"
2929
- "3.13"
30+
- "3.14"
3031
# Additional special cases (see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#example-adding-configurations)
3132
include:
3233
- os: "windows-latest"

CHANGELOG.md

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

1010
### Added
1111

12+
- Official support for Python 3.14 (include Python 3.14 in unit test matrix on GitHub Actions) ([#801](https://github.com/Open-EO/openeo-python-client/issues/801))
13+
- Add `on_response_headers_sync` option to `openeo.connect`/`Connection` to set default `on_response_headers` handler for sync requests at connection level.
14+
1215
### Changed
1316

1417
### Removed
1518

1619
### Fixed
1720

21+
- Guard STAC metadata parsing against invalid "item_assets" usage ([#853](https://github.com/Open-EO/openeo-python-client/issues/853))
22+
1823

1924
## [0.47.0] - 2025-12-17
2025

openeo/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.48.0a1"
1+
__version__ = "0.48.0a3"

openeo/metadata.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,7 @@ def _parse_dimensions(cls, spec: dict, complain: Callable[[str], None] = _log.wa
603603
deep_get(spec, "summaries", "eo:bands", default=None)
604604
or deep_get(spec, "summaries", "bands", default=None)
605605
or deep_get(spec, "summaries", "raster:bands", default=None)
606+
# TODO: drop this 0.4-style "properties/eo:bands"
606607
or deep_get(spec, "properties", "eo:bands", default=None)
607608
)
608609
if summaries_bands:
@@ -878,7 +879,12 @@ def bands_from_stac_collection(
878879
# Check item assets if available
879880
elif _PYSTAC_1_12_ITEM_ASSETS and collection.item_assets:
880881
return self._bands_from_item_assets(collection.item_assets)
881-
elif _PYSTAC_1_9_EXTENSION_INTERFACE and collection.ext.has("item_assets") and collection.ext.item_assets:
882+
elif (
883+
_PYSTAC_1_9_EXTENSION_INTERFACE
884+
and collection.ext.has("item_assets")
885+
and collection.extra_fields.get("item-assets")
886+
and collection.ext.item_assets
887+
):
882888
return self._bands_from_item_assets(collection.ext.item_assets)
883889
elif collection.extra_fields.get("item_assets"):
884890
# Workaround for lack of support for STAC 1.1 core item_assets with pystac < 1.12

openeo/rest/connection.py

Lines changed: 61 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@
107107
DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE = 30 * 60
108108

109109

110+
ResponseHeadersHandler = Callable[[Mapping], None]
111+
112+
110113
class Connection(RestApiConnection):
111114
"""
112115
Connection to an openEO backend.
@@ -134,8 +137,14 @@ class Connection(RestApiConnection):
134137
- ``None`` (default) to use default openEO-oriented retry settings
135138
- ``False`` to disable retrying requests
136139
140+
:param on_response_headers_sync: (optional) callback to handle (e.g. :py:func:`print`)
141+
the response headers of synchronous processing requests.
142+
137143
.. versionchanged:: 0.41.0
138144
Added ``retry`` argument.
145+
146+
.. versionchanged:: 0.48
147+
Added argument ``on_response_headers_sync``.
139148
"""
140149

141150
_MINIMUM_API_VERSION = ComparableVersion("1.0.0")
@@ -153,6 +162,7 @@ def __init__(
153162
oidc_auth_renewer: Optional[OidcAuthenticator] = None,
154163
auth: Optional[AuthBase] = None,
155164
retry: Union[urllib3.util.Retry, dict, bool, None] = None,
165+
on_response_headers_sync: Optional[ResponseHeadersHandler] = None,
156166
):
157167
if "://" not in url:
158168
url = "https://" + url
@@ -172,6 +182,7 @@ def __init__(
172182
self._refresh_token_store = refresh_token_store
173183
self._oidc_auth_renewer = oidc_auth_renewer
174184
self._auto_validate = auto_validate
185+
self._on_response_headers_sync = on_response_headers_sync
175186

176187
@classmethod
177188
def version_discovery(
@@ -216,6 +227,37 @@ def _get_refresh_token_store(self) -> RefreshTokenStore:
216227
self._refresh_token_store = RefreshTokenStore()
217228
return self._refresh_token_store
218229

230+
def list_auth_providers(self) -> List[dict]:
231+
providers = []
232+
cap = self.capabilities()
233+
234+
# Add OIDC providers
235+
oidc_path = "/credentials/oidc"
236+
if cap.supports_endpoint(oidc_path, method="GET"):
237+
try:
238+
data = self.get(oidc_path, expected_status=200).json()
239+
if isinstance(data, dict):
240+
for provider in data.get("providers", []):
241+
provider["type"] = "oidc"
242+
providers.append(provider)
243+
except OpenEoApiError as e:
244+
_log.warning(f"Unable to load the OpenID Connect provider list: {e.message}")
245+
246+
# Add Basic provider
247+
basic_path = "/credentials/basic"
248+
if cap.supports_endpoint(basic_path, method="GET"):
249+
providers.append(
250+
{
251+
"id": basic_path,
252+
"issuer": self.build_url(basic_path),
253+
"type": "basic",
254+
"title": "Internal",
255+
"description": "The HTTP Basic authentication method is mostly used for development and testing purposes.",
256+
}
257+
)
258+
259+
return providers
260+
219261
def authenticate_basic(self, username: Optional[str] = None, password: Optional[str] = None) -> Connection:
220262
"""
221263
Authenticate a user to the backend using basic username and password.
@@ -1681,7 +1723,7 @@ def download(
16811723
chunk_size: int = DEFAULT_DOWNLOAD_CHUNK_SIZE,
16821724
additional: Optional[dict] = None,
16831725
job_options: Optional[dict] = None,
1684-
on_response_headers: Optional[Callable[[Mapping], None]] = None,
1726+
on_response_headers: Optional[ResponseHeadersHandler] = None,
16851727
) -> Union[None, bytes]:
16861728
"""
16871729
Send the underlying process graph to the backend
@@ -1700,7 +1742,7 @@ def download(
17001742
:param additional: (optional) additional (top-level) properties to set in the request body
17011743
:param job_options: (optional) dictionary of job options to pass to the backend
17021744
(under top-level property "job_options")
1703-
:param on_response_headers: (optional) callback to handle/show the response headers
1745+
:param on_response_headers: (optional) callback to handle (e.g. :py:func:`print`) the response headers.
17041746
17051747
:return: if ``outputfile`` was not specified:
17061748
a :py:class:`bytes` object containing the raw data.
@@ -1723,7 +1765,7 @@ def download(
17231765
stream=True,
17241766
timeout=timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE,
17251767
)
1726-
if on_response_headers:
1768+
if on_response_headers := (on_response_headers or self._on_response_headers_sync):
17271769
on_response_headers(response.headers)
17281770

17291771
if outputfile is not None:
@@ -1745,6 +1787,7 @@ def execute(
17451787
auto_decode: bool = True,
17461788
additional: Optional[dict] = None,
17471789
job_options: Optional[dict] = None,
1790+
on_response_headers: Optional[ResponseHeadersHandler] = None,
17481791
) -> Union[dict, requests.Response]:
17491792
"""
17501793
Execute a process graph synchronously and return the result. If the result is a JSON object, it will be parsed.
@@ -1757,11 +1800,15 @@ def execute(
17571800
:param additional: additional (top-level) properties to set in the request body
17581801
:param job_options: dictionary of job options to pass to the backend
17591802
(under top-level property "job_options")
1803+
:param on_response_headers: (optional) callback to handle (e.g. :py:func:`print`) the response headers.
17601804
17611805
:return: parsed JSON response as a dict if auto_decode is True, otherwise response object
17621806
17631807
.. versionadded:: 0.36.0
17641808
Added arguments ``additional`` and ``job_options``.
1809+
1810+
.. versionchanged:: 0.48
1811+
Added argument ``on_response_headers``.
17651812
"""
17661813
pg_with_metadata = self._build_request_with_process_graph(
17671814
process_graph=process_graph, additional=additional, job_options=job_options
@@ -1773,6 +1820,9 @@ def execute(
17731820
expected_status=200,
17741821
timeout=timeout or DEFAULT_TIMEOUT_SYNCHRONOUS_EXECUTE,
17751822
)
1823+
if on_response_headers := (on_response_headers or self._on_response_headers_sync):
1824+
on_response_headers(response.headers)
1825+
17761826
if auto_decode:
17771827
try:
17781828
return response.json()
@@ -1962,6 +2012,7 @@ def connect(
19622012
default_timeout: Optional[int] = None,
19632013
auto_validate: bool = True,
19642014
retry: Union[urllib3.util.Retry, dict, bool, None] = None,
2015+
on_response_headers_sync: Optional[ResponseHeadersHandler] = None,
19652016
) -> Connection:
19662017
"""
19672018
This method is the entry point to OpenEO.
@@ -1989,11 +2040,17 @@ def connect(
19892040
- ``None`` (default) to use default openEO-oriented retry settings
19902041
- ``False`` to disable retrying requests
19912042
2043+
:param on_response_headers_sync: (optional) callback to handle (e.g. :py:func:`print`)
2044+
the response headers of synchronous processing requests.
2045+
19922046
.. versionchanged:: 0.24.0
19932047
Added ``auto_validate`` argument
19942048
19952049
.. versionchanged:: 0.41.0
19962050
Added ``retry`` argument.
2051+
2052+
.. versionchanged:: 0.48
2053+
Added argument ``on_response_headers_sync``.
19972054
"""
19982055

19992056
def _config_log(message):
@@ -2024,6 +2081,7 @@ def _config_log(message):
20242081
default_timeout=default_timeout,
20252082
auto_validate=auto_validate,
20262083
retry=retry,
2084+
on_response_headers_sync=on_response_headers_sync,
20272085
)
20282086

20292087
auth_type = auth_type.lower() if isinstance(auth_type, str) else auth_type

openeo/rest/datacube.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ def _build_load_properties_argument(
300300
unsupported_properties = set(properties.keys()).difference(supported_properties)
301301
if unsupported_properties:
302302
warnings.warn(
303-
f"Property filtering with unsupported properties according to collection/STAC metadata: {unsupported_properties} (supported: {supported_properties}).",
303+
f"Property filtering with properties not listed in collection/STAC metadata: {list(unsupported_properties)} (supported: {list(supported_properties)}).",
304304
stacklevel=3,
305305
)
306306
properties = {
@@ -2456,7 +2456,7 @@ def download(
24562456
:param additional: (optional) additional (top-level) properties to set in the request body
24572457
:param job_options: (optional) dictionary of job options to pass to the backend
24582458
(under top-level property "job_options")
2459-
:param on_response_headers: (optional) callback to handle/show the response headers
2459+
:param on_response_headers: (optional) callback to handle (e.g. :py:func:`print`) the response headers.
24602460
24612461
:return: if ``outputfile`` was not specified:
24622462
a :py:class:`bytes` object containing the raw data.
@@ -3072,6 +3072,34 @@ def unflatten_dimension(self, dimension: str, target_dimensions: List[str], labe
30723072
),
30733073
)
30743074

3075+
@openeo_process
3076+
def aspect(self) -> DataCube:
3077+
"""
3078+
Converts every band in the datacube to a band with suffix "_aspect" containing the aspect.
3079+
3080+
:return: A data cube with the same dimensions but containing the computed aspect values.
3081+
"""
3082+
return self.process(
3083+
process_id="aspect",
3084+
arguments=dict_no_none(
3085+
data=THIS
3086+
),
3087+
)
3088+
3089+
@openeo_process
3090+
def slope(self) -> DataCube:
3091+
"""
3092+
Converts every band in the datacube to a band with suffix "_slope" containing the slope.
3093+
3094+
:return: A data cube with the same dimensions but containing the computed slope values.
3095+
"""
3096+
return self.process(
3097+
process_id="slope",
3098+
arguments=dict_no_none(
3099+
data=THIS
3100+
),
3101+
)
3102+
30753103
@openeo_process
30763104
def convert_data_type(
30773105
self,

openeo/rest/stac_resource.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def download(
102102
:param additional: (optional) additional (top-level) properties to set in the request body
103103
:param job_options: (optional) dictionary of job options to pass to the backend
104104
(under top-level property "job_options")
105-
:param on_response_headers: (optional) callback to handle/show the response headers
105+
:param on_response_headers: (optional) callback to handle (e.g. :py:func:`print`) the response headers.
106106
107107
:return: if ``outputfile`` was not specified:
108108
a :py:class:`bytes` object containing the raw data.

openeo/rest/vectorcube.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ def download(
268268
:param additional: (optional) additional (top-level) properties to set in the request body
269269
:param job_options: (optional) dictionary of job options to pass to the backend
270270
(under top-level property "job_options")
271-
:param on_response_headers: (optional) callback to handle/show the response headers
271+
:param on_response_headers: (optional) callback to handle (e.g. :py:func:`print`) the response headers.
272272
273273
:return: if ``outputfile`` was not specified:
274274
a :py:class:`bytes` object containing the raw data.
@@ -292,7 +292,12 @@ def download(
292292
else:
293293
res = self
294294
return self._connection.download(
295-
res.flat_graph(), outputfile=outputfile, validate=validate, additional=additional, job_options=job_options
295+
res.flat_graph(),
296+
outputfile=outputfile,
297+
validate=validate,
298+
additional=additional,
299+
job_options=job_options,
300+
on_response_headers=on_response_headers,
296301
)
297302

298303
def execute_batch(

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
"shapely>=1.6.4",
8181
"numpy>=1.17.0",
8282
"xarray>=0.12.3,<2025.01.2", # TODO #721 xarray non-nanosecond support
83-
"pandas>0.20.0",
83+
"pandas>0.20.0,<3.0.0", # TODO pandas 3 compatibility https://github.com/Open-EO/openeo-python-client/issues/856
8484
# TODO #578: pystac 1.5.0 is highest version available for lowest Python version we still support (3.7).
8585
"pystac>=1.5.0",
8686
"deprecated>=1.2.12",
@@ -108,6 +108,7 @@
108108
"Programming Language :: Python :: 3.11",
109109
"Programming Language :: Python :: 3.12",
110110
"Programming Language :: Python :: 3.13",
111+
"Programming Language :: Python :: 3.14",
111112
"License :: OSI Approved :: Apache Software License",
112113
"Development Status :: 5 - Production/Stable",
113114
"Operating System :: OS Independent",

tests/rest/datacube/test_datacube100.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2129,7 +2129,7 @@ def test_load_collection_max_cloud_cover_summaries_warning(
21292129
if expect_warning:
21302130
assert len(recwarn.list) == 1
21312131
assert re.search(
2132-
"Property filtering.*unsupported.*collection.*metadata.*eo:cloud_cover",
2132+
"Property filtering.*properties not listed.*collection.*metadata.*eo:cloud_cover",
21332133
str(recwarn.pop(UserWarning).message),
21342134
)
21352135
else:

0 commit comments

Comments
 (0)