Skip to content

Commit af39718

Browse files
pacesmsmithara
andauthored
VirES WPS fixes (#121)
* Sending WPS POST requests with the proper content type header. * Handle gracefully relative response URLs. * Handle gracefully xlink:href references. * Implementing re-trials for failed WPS asynchronous job status requests. - 3 re-tries after 20 seconds - changing the default logging level to ERROR * Style autofixes by pre-commit * Version bump and release notes for v0.12.2 --------- Co-authored-by: Ashley Smith <[email protected]>
1 parent 3570b7e commit af39718

File tree

6 files changed

+85
-25
lines changed

6 files changed

+85
-25
lines changed

docs/release_notes.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,14 @@ Release notes
44
Change log
55
----------
66

7+
Changes from 0.12.1 to 0.12.2
8+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
9+
10+
- **Internal WPS fixes which may be required to access the server in the future**
11+
- Improved robustness during asynchronous requests (the client now repeats the failed job status polling 3 times with 20 seconds interval)
12+
13+
See `PR#121 <https://github.com/ESA-VirES/VirES-Python-Client/pull/121>`_ for details
14+
715
Changes from 0.12.0 to 0.12.1
816
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
917

src/viresclient/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@
3535
from ._config import ClientConfig, set_token
3636
from ._data_handling import ReturnedData, ReturnedDataFile
3737

38-
__version__ = "0.12.1"
38+
__version__ = "0.12.2"

src/viresclient/_client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
"NO_LOGGING": CRITICAL + 1,
7171
}
7272

73+
DEFAULT_LOGGING_LEVEL = "ERROR"
74+
7375
# File type to WPS output name
7476
RESPONSE_TYPES = {
7577
"csv": "text/csv",
@@ -258,7 +260,7 @@ def __init__(
258260
url=None,
259261
token=None,
260262
config=None,
261-
logging_level="NO_LOGGING",
263+
logging_level=DEFAULT_LOGGING_LEVEL,
262264
server_type=None,
263265
):
264266
self._server_type = server_type

src/viresclient/_client_aeolus.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
import pandas as pd
55

6-
from ._client import ClientRequest, WPSInputs
6+
from ._client import DEFAULT_LOGGING_LEVEL, ClientRequest, WPSInputs
77
from ._data import CONFIG_AEOLUS
88
from ._data_handling import ReturnedDataFile
99

@@ -222,7 +222,9 @@ class AeolusRequest(ClientRequest):
222222
223223
"""
224224

225-
def __init__(self, url=None, token=None, config=None, logging_level="NO_LOGGING"):
225+
def __init__(
226+
self, url=None, token=None, config=None, logging_level=DEFAULT_LOGGING_LEVEL
227+
):
226228
super().__init__(url, token, config, logging_level, server_type="Aeolus")
227229
# self._available = self._set_available_data()
228230
self._request_inputs = AeolusWPSInputs()

src/viresclient/_client_swarm.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from pandas import read_csv
1313
from tqdm import tqdm
1414

15-
from ._client import TEMPLATE_FILES, ClientRequest, WPSInputs
15+
from ._client import DEFAULT_LOGGING_LEVEL, TEMPLATE_FILES, ClientRequest, WPSInputs
1616
from ._data import CONFIG_SWARM
1717
from ._data_handling import ReturnedDataFile
1818
from ._wps.environment import JINJA2_ENVIRONMENT
@@ -1344,7 +1344,9 @@ class SwarmRequest(ClientRequest):
13441344
"SwarmCI",
13451345
]
13461346

1347-
def __init__(self, url=None, token=None, config=None, logging_level="NO_LOGGING"):
1347+
def __init__(
1348+
self, url=None, token=None, config=None, logging_level=DEFAULT_LOGGING_LEVEL
1349+
):
13481350
super().__init__(url, token, config, logging_level, server_type="Swarm")
13491351

13501352
self._available = self._get_available_data()

src/viresclient/_wps/wps.py

Lines changed: 65 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -27,16 +27,12 @@
2727
# THE SOFTWARE.
2828
# -------------------------------------------------------------------------------
2929

30-
try:
31-
from urllib.error import HTTPError
32-
from urllib.request import Request, urlopen
33-
except ImportError:
34-
# Python 2 backward compatibility
35-
from urllib2 import urlopen, Request, HTTPError
36-
3730
from contextlib import closing
3831
from logging import LoggerAdapter, getLogger
3932
from time import sleep
33+
from urllib.error import HTTPError
34+
from urllib.parse import urljoin
35+
from urllib.request import Request, urlopen
4036
from xml.etree import ElementTree
4137

4238
from .time_util import Timer
@@ -104,6 +100,10 @@ class WPS10Service:
104100
request
105101
"""
106102

103+
DEFAULT_CONTENT_TYPE = "application/xml; charset=utf-8"
104+
RETRY_TIME = 20 # seconds
105+
STATUS_POLL_RETRIES = 3 # re-try attempts
106+
107107
STATUS = {
108108
"{http://www.opengis.net/wps/1.0.0}ProcessAccepted": "ACCEPTED",
109109
"{http://www.opengis.net/wps/1.0.0}ProcessFailed": "FAILED",
@@ -120,12 +120,17 @@ def __init__(self, url, headers=None, logger=None):
120120
self.headers = headers or {}
121121
self.logger = self._LoggerAdapter(logger or getLogger(__name__), {})
122122

123-
def retrieve(self, request, handler=None):
123+
def retrieve(self, request, handler=None, content_type=None):
124124
"""Send a synchronous POST WPS request to a server and retrieve
125125
the output.
126126
"""
127+
headers = {
128+
**self.headers,
129+
"Content-Type": content_type or self.DEFAULT_CONTENT_TYPE,
130+
}
131+
127132
return self._retrieve(
128-
Request(self.url, request, self.headers), handler, self.error_handler
133+
Request(self.url, request, headers), handler, self.error_handler
129134
)
130135

131136
def retrieve_async(
@@ -136,16 +141,20 @@ def retrieve_async(
136141
cleanup_handler=None,
137142
polling_interval=1,
138143
output_name="output",
144+
content_type=None,
139145
):
140146
"""Send an asynchronous POST WPS request to a server and retrieve
141147
the output.
142148
"""
143149
timer = Timer()
144150
status, percentCompleted, status_url, execute_response = self.submit_async(
145-
request
151+
request,
152+
content_type=content_type,
146153
)
147154
wpsstatus = WPSStatus()
148-
wpsstatus.update(status, percentCompleted, status_url, execute_response)
155+
wpsstatus.update(
156+
status, percentCompleted, urljoin(self.url, status_url), execute_response
157+
)
149158

150159
def log_wpsstatus(wpsstatus):
151160
self.logger.info(
@@ -169,7 +178,7 @@ def log_wpsstatus_percentCompleted(wpsstatus):
169178

170179
last_status = wpsstatus.status
171180
last_percentCompleted = wpsstatus.percentCompleted
172-
wpsstatus.update(*self.poll_status(wpsstatus.url))
181+
wpsstatus.update(*self.poll_status(urljoin(self.url, wpsstatus.url)))
173182

174183
if wpsstatus.status != last_status:
175184
log_wpsstatus(wpsstatus)
@@ -197,7 +206,9 @@ def retrieve_async_output(self, status_url, output_name, handler=None):
197206
"""Retrieve asynchronous job output reference."""
198207
self.logger.debug("Retrieving asynchronous job output '%s'.", output_name)
199208
output_url = self.parse_output_reference(status_url, output_name)
200-
return self._retrieve(Request(output_url, None, self.headers), handler)
209+
return self._retrieve(
210+
Request(urljoin(self.url, output_url), None, self.headers), handler
211+
)
201212

202213
@staticmethod
203214
def parse_output_reference(xml, identifier):
@@ -210,25 +221,60 @@ def parse_output_reference(xml, identifier):
210221
elm_reference = elm.find(
211222
"./{http://www.opengis.net/wps/1.0.0}Reference"
212223
)
213-
return elm_reference.attrib["href"]
224+
return (
225+
elm_reference.attrib.get("{http://www.w3.org/1999/xlink}href")
226+
or elm_reference.attrib["href"]
227+
)
214228

215-
def submit_async(self, request):
229+
def submit_async(self, request, content_type=None):
216230
"""Send a POST WPS asynchronous request to a server and retrieve
217231
the status URL.
218232
"""
219233
self.logger.debug("Submitting asynchronous job.")
234+
headers = {
235+
**self.headers,
236+
"Content-Type": content_type or self.DEFAULT_CONTENT_TYPE,
237+
}
220238
return self._retrieve(
221-
Request(self.url, request, self.headers),
239+
Request(self.url, request, headers),
222240
self.parse_status,
223241
self.error_handler,
224242
)
225243

226244
def poll_status(self, status_url):
227245
"""Poll status of an asynchronous WPS job."""
228246
self.logger.debug("Polling asynchronous job status.")
229-
return self._retrieve(
230-
Request(status_url, None, self.headers), self.parse_status
231-
)
247+
248+
for index in range(self.STATUS_POLL_RETRIES + 1):
249+
250+
if index == 0:
251+
self.logger.debug("Polling asynchronous job status.")
252+
else:
253+
self.logger.debug(
254+
"Polling asynchronous job status. Retry attempt #%s.", index
255+
)
256+
257+
try:
258+
return self._retrieve(
259+
Request(status_url, None, self.headers), self.parse_status
260+
)
261+
except Exception as error:
262+
if index < self.STATUS_POLL_RETRIES:
263+
self.logger.error(
264+
"Status poll failed. Retrying in %s seconds. %s: %s",
265+
self.RETRY_TIME,
266+
error.__class__.__name__,
267+
error,
268+
)
269+
else:
270+
self.logger.error(
271+
"Status poll failed. No more retries. %s: %s",
272+
error.__class__.__name__,
273+
error,
274+
)
275+
raise
276+
277+
sleep(self.RETRY_TIME)
232278

233279
@classmethod
234280
def parse_status(cls, response):

0 commit comments

Comments
 (0)