Skip to content

Commit 5c05ced

Browse files
committed
Merge branch 'master' into hv_issue719-job-manager-threaded-job-start
2 parents 883e7d7 + 05a132c commit 5c05ced

16 files changed

Lines changed: 476 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111

1212
- `openeo.testing.io.TestDataLoader`: unit test utility to compactly load (and optionally preprocess) tests data (text/JSON/...)
13+
- `openeo.Connection`: automatically retry API requests on `429 Too Many Requests` HTTP errors, with appropriate delay if possible ([#441](https://github.com/Open-EO/openeo-python-client/issues/441))
1314

1415
### Changed
1516

docs/basics.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ and observations are filtered out:
317317

318318
.. image:: _static/images/basics/evi-masked-composite.png
319319

320-
.. _aggregate-spatial-evi
320+
.. _aggregate-spatial-evi:
321321

322322
Aggregated EVI timeseries
323323
===========================

docs/conf.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
'sphinx.ext.viewcode',
4242
'sphinx.ext.doctest',
4343
'myst_parser',
44+
"sphinx.ext.intersphinx",
4445
]
4546

4647
import sphinx_autodoc_typehints
@@ -194,3 +195,13 @@
194195
author, 'openeo', 'One line description of project.',
195196
'Miscellaneous'),
196197
]
198+
199+
200+
# Mapping for external documentation
201+
intersphinx_mapping = {
202+
"python": ("https://docs.python.org/3", None),
203+
"numpy": ("https://numpy.org/doc/stable/", None),
204+
"xarray": ("https://docs.xarray.dev/en/stable/", None),
205+
"pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None),
206+
"urllib3": ("https://urllib3.readthedocs.io/en/stable/", None),
207+
}

docs/cookbook/job_manager.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.. _job-manager
1+
.. _job-manager:
22

33
====================================
44
Multi Backend Job Manager

docs/datacube_construction.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ TODO
200200

201201

202202
.. _multi-result-process-graphs:
203+
203204
Building process graphs with multiple result nodes
204205
===================================================
205206

docs/udf.rst

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -374,11 +374,10 @@ the datacube.
374374
375375
376376
.. warning::
377-
378-
The ``apply_neighborhood`` is the most versatile, but also most complex process. Make sure to keep an eye on the dimensions
379-
and the shape of the DataArray returned by your UDF. For instance, a very common error is to somehow 'flip' the spatial dimensions.
380-
Debugging the UDF locally can help, but then you will want to try and reproduce the input that you get also on the backend.
381-
This can typically be achieved by using logging to inspect the DataArrays passed into your UDF backend side.
377+
The ``apply_neighborhood`` is the most versatile, but also most complex process. Make sure to keep an eye on the dimensions
378+
and the shape of the DataArray returned by your UDF. For instance, a very common error is to somehow 'flip' the spatial dimensions.
379+
Debugging the UDF locally can help, but then you will want to try and reproduce the input that you get also on the backend.
380+
This can typically be achieved by using logging to inspect the DataArrays passed into your UDF backend side.
382381

383382

384383

openeo/extra/job_management/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@
2929
import shapely.errors
3030
import shapely.geometry.base
3131
import shapely.wkt
32-
from requests.adapters import HTTPAdapter, Retry
32+
from requests.adapters import HTTPAdapter
33+
from urllib3.util import Retry
3334

3435
from openeo import BatchJob, Connection
3536
from openeo.extra.job_management._thread_worker import (
@@ -292,7 +293,7 @@ def _make_resilient(connection):
292293
503 Service Unavailable
293294
504 Gateway Timeout
294295
"""
295-
# TODO: refactor this helper out of this class and unify with `openeo_driver.util.http.requests_with_retry`
296+
# TODO: migrate this to now built-in retry configuration of `Connection` or `openeo.util.http.retry_adapter`?
296297
status_forcelist = [500, 502, 503, 504]
297298
retries = Retry(
298299
total=MAX_RETRIES,

openeo/rest/_connection.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@
55
from typing import Iterable, Optional, Union
66

77
import requests
8+
import urllib3.util
89
from requests import Response
910
from requests.auth import AuthBase
1011

1112
import openeo
1213
from openeo.rest import OpenEoApiError, OpenEoApiPlainError, OpenEoRestError
1314
from openeo.rest.auth.auth import NullAuth
1415
from openeo.util import ContextTimer, ensure_list, str_truncate, url_join
16+
from openeo.utils.http import HTTP_502_BAD_GATEWAY, session_with_retries
1517

1618
_log = logging.getLogger(__name__)
1719

@@ -26,15 +28,22 @@ class RestApiConnection:
2628
def __init__(
2729
self,
2830
root_url: str,
31+
*,
2932
auth: Optional[AuthBase] = None,
3033
session: Optional[requests.Session] = None,
3134
default_timeout: Optional[int] = None,
3235
slow_response_threshold: Optional[float] = None,
36+
retry: Union[urllib3.util.Retry, dict, bool, None] = None,
3337
):
3438
self._root_url = root_url
3539
self._auth = None
3640
self.auth = auth or NullAuth()
37-
self.session = session or requests.Session()
41+
if session:
42+
self.session = session
43+
elif retry is not False:
44+
self.session = session_with_retries(retry=retry)
45+
else:
46+
self.session = requests.Session()
3847
self.default_timeout = default_timeout or DEFAULT_TIMEOUT
3948
self.default_headers = {
4049
"User-Agent": "openeo-python-client/{cv} {py}/{pv} {pl}".format(
@@ -165,7 +174,7 @@ def _raise_api_error(self, response: requests.Response):
165174
_log.warning(f"Failed to parse API error response: [{status_code}] {text!r} (headers: {response.headers})")
166175

167176
# TODO: eliminate this VITO-backend specific error massaging?
168-
if status_code == 502 and "Proxy Error" in text:
177+
if status_code == HTTP_502_BAD_GATEWAY and "Proxy Error" in text:
169178
error_message = (
170179
"Received 502 Proxy Error."
171180
" This typically happens when a synchronous openEO processing request takes too long and is aborted."

openeo/rest/_testing.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
from openeo import Connection, DataCube
1919
from openeo.rest.vectorcube import VectorCube
20+
from openeo.utils.http import HTTP_201_CREATED, HTTP_202_ACCEPTED, HTTP_204_NO_CONTENT
2021

2122
OPENEO_BACKEND = "https://openeo.test/"
2223

@@ -212,7 +213,7 @@ def _handle_post_jobs(self, request, context):
212213
for field in self.extra_job_metadata_fields:
213214
job_data[field] = post_data.get(field)
214215
self.batch_jobs[job_id] = job_data
215-
context.status_code = 201
216+
context.status_code = HTTP_201_CREATED
216217
context.headers["openeo-identifier"] = job_id
217218

218219
def _get_job_id(self, request) -> str:
@@ -259,7 +260,7 @@ def _handle_post_job_results(self, request, context):
259260
self.batch_jobs[job_id]["status"] = self._get_job_status(
260261
job_id=job_id, current_status=self.batch_jobs[job_id]["status"]
261262
)
262-
context.status_code = 202
263+
context.status_code = HTTP_202_ACCEPTED
263264
else:
264265
self._set_job_status(job_id=job_id, status="error")
265266
context.status_code = failure["status_code"]
@@ -300,7 +301,7 @@ def _handle_delete_job_results(self, request, context):
300301
"""Handler of `DELETE /job/{job_id}/results` (cancel job)."""
301302
job_id = self._get_job_id(request)
302303
self._set_job_status(job_id=job_id, status="canceled")
303-
context.status_code = 204
304+
context.status_code = HTTP_204_NO_CONTENT
304305

305306
def _handle_get_job_result_asset(self, request, context):
306307
"""Handler of `GET /job/{job_id}/results/result.data` (get batch job result asset)."""

openeo/rest/connection.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
import requests
3131
import shapely.geometry.base
32+
import urllib3.util
3233
from requests.auth import AuthBase, HTTPBasicAuth
3334

3435
import openeo
@@ -85,6 +86,11 @@
8586
load_json_resource,
8687
rfc3339,
8788
)
89+
from openeo.utils.http import (
90+
HTTP_201_CREATED,
91+
HTTP_401_UNAUTHORIZED,
92+
HTTP_403_FORBIDDEN,
93+
)
8894
from openeo.utils.version import ComparableVersion
8995

9096
__all__ = ["Connection", "connect"]
@@ -114,6 +120,16 @@ class Connection(RestApiConnection):
114120
optional :class:`OidcAuthenticator` object to use for renewing OIDC tokens.
115121
:param auth: Optional ``requests.auth.AuthBase`` object to use for requests.
116122
Usage of this parameter is deprecated, use the specific authentication methods instead.
123+
:param retry: general request retry settings, can be specified as:
124+
125+
- :py:class:`urllib3.util.Retry` object
126+
or a dictionary with corresponding keyword arguments
127+
(e.g. ``total``, ``backoff_factor``, ``status_forcelist``, ...)
128+
- ``None`` (default) to use default openEO-oriented retry settings
129+
- ``False`` to disable retrying requests
130+
131+
.. versionchanged:: 0.41.0
132+
Added ``retry`` argument.
117133
"""
118134

119135
_MINIMUM_API_VERSION = ComparableVersion("1.0.0")
@@ -130,6 +146,7 @@ def __init__(
130146
refresh_token_store: Optional[RefreshTokenStore] = None,
131147
oidc_auth_renewer: Optional[OidcAuthenticator] = None,
132148
auth: Optional[AuthBase] = None,
149+
retry: Union[urllib3.util.Retry, dict, bool, None] = None,
133150
):
134151
if "://" not in url:
135152
url = "https://" + url
@@ -139,6 +156,7 @@ def __init__(
139156
root_url=self.version_discovery(url, session=session, timeout=default_timeout),
140157
auth=auth, session=session, default_timeout=default_timeout,
141158
slow_response_threshold=slow_response_threshold,
159+
retry=retry,
142160
)
143161

144162
# Initial API version check.
@@ -663,7 +681,10 @@ def _request():
663681
# Initial request attempt
664682
return _request()
665683
except OpenEoApiError as api_exc:
666-
if api_exc.http_status_code in {401, 403} and api_exc.code == "TokenInvalid":
684+
if (
685+
api_exc.http_status_code in {HTTP_401_UNAUTHORIZED, HTTP_403_FORBIDDEN}
686+
and api_exc.code == "TokenInvalid"
687+
):
667688
# Auth token expired: can we refresh?
668689
if isinstance(self.auth, OidcBearerAuth) and self._oidc_auth_renewer:
669690
msg = f"OIDC access token expired ({api_exc.http_status_code} {api_exc.code})."
@@ -1750,7 +1771,7 @@ def create_job(
17501771
)
17511772

17521773
self._preflight_validation(pg_with_metadata=pg_with_metadata, validate=validate)
1753-
response = self.post("/jobs", json=pg_with_metadata, expected_status=201)
1774+
response = self.post("/jobs", json=pg_with_metadata, expected_status=HTTP_201_CREATED)
17541775

17551776
job_id = None
17561777
if "openeo-identifier" in response.headers:
@@ -1885,6 +1906,7 @@ def connect(
18851906
session: Optional[requests.Session] = None,
18861907
default_timeout: Optional[int] = None,
18871908
auto_validate: bool = True,
1909+
retry: Union[urllib3.util.Retry, dict, bool, None] = None,
18881910
) -> Connection:
18891911
"""
18901912
This method is the entry point to OpenEO.
@@ -1904,9 +1926,19 @@ def connect(
19041926
:param auth_options: Options/arguments specific to the authentication type
19051927
:param default_timeout: default timeout (in seconds) for requests
19061928
:param auto_validate: toggle to automatically validate process graphs before execution
1929+
:param retry: general request retry settings, can be specified as:
19071930
1908-
.. versionadded:: 0.24.0
1909-
added ``auto_validate`` argument
1931+
- :py:class:`urllib3.util.Retry` object
1932+
or a dictionary with corresponding keyword arguments
1933+
(e.g. ``total``, ``backoff_factor``, ``status_forcelist``, ...)
1934+
- ``None`` (default) to use default openEO-oriented retry settings
1935+
- ``False`` to disable retrying requests
1936+
1937+
.. versionchanged:: 0.24.0
1938+
Added ``auto_validate`` argument
1939+
1940+
.. versionchanged:: 0.41.0
1941+
Added ``retry`` argument.
19101942
"""
19111943

19121944
def _config_log(message):
@@ -1931,7 +1963,13 @@ def _config_log(message):
19311963

19321964
if not url:
19331965
raise OpenEoClientException("No openEO back-end URL given or known to connect to.")
1934-
connection = Connection(url, session=session, default_timeout=default_timeout, auto_validate=auto_validate)
1966+
connection = Connection(
1967+
url,
1968+
session=session,
1969+
default_timeout=default_timeout,
1970+
auto_validate=auto_validate,
1971+
retry=retry,
1972+
)
19351973

19361974
auth_type = auth_type.lower() if isinstance(auth_type, str) else auth_type
19371975
if auth_type in {None, False, 'null', 'none'}:

0 commit comments

Comments
 (0)