Skip to content

Commit b27c519

Browse files
SlowMo24matthiasschaubdependabot[bot]
authored
fix(response): reoder exception handling to correctly parse a broken response (#164)
split exception handling into three parts dedicated to the kind of exception that could happen Co-authored-by: Matthias (~talfus-laddus) <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 parent af13807 commit b27c519

8 files changed

+84
-49
lines changed

CHANGELOG.md

+9
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,17 @@
44

55
### Fixed
66

7+
- Ordering of exception handling to correctly parse a broken response
78
- Assert that the expected columns are also present if the result is empty
89

10+
### Removed
11+
12+
- unused attributes `response` and `parameters`/`params` from `OhsomeResponse`
13+
14+
### Added
15+
16+
- init variable `data` to `OhsomeResponse`
17+
918
## 0.3.2
1019

1120
### Fixed

ohsome/clients.py

+39-36
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
import pandas as pd
1414
import requests
1515
import shapely
16-
from requests import Session
16+
from requests import Session, Response
1717
from requests.adapters import HTTPAdapter
18-
from requests.exceptions import RetryError
18+
from requests.exceptions import RetryError, JSONDecodeError
1919
from urllib3 import Retry
2020

2121
from ohsome import OhsomeException, OhsomeResponse
@@ -332,37 +332,36 @@ def _handle_request(self) -> OhsomeResponse:
332332
Handles request to ohsome API
333333
:return:
334334
"""
335-
ohsome_exception = None
336-
response = None
337335

338336
try:
339-
response = self._session().post(url=self._url, data=self._parameters)
340-
response.raise_for_status()
341-
response.json()
337+
response = self._post_request()
338+
self._check_response(response)
339+
data = self._get_response_data(response)
340+
except OhsomeException as ohsome_exception:
341+
if self.log:
342+
ohsome_exception.log(self.log_dir)
343+
raise ohsome_exception
342344

343-
except requests.exceptions.HTTPError as e:
344-
try:
345-
error_message = e.response.json()["message"]
346-
except json.decoder.JSONDecodeError:
347-
error_message = f"Invalid URL: Is {self._url} valid?"
345+
return OhsomeResponse(data=data, url=self._url)
348346

349-
ohsome_exception = OhsomeException(
350-
message=error_message,
347+
def _post_request(self) -> Response:
348+
try:
349+
response = self._session().post(url=self._url, data=self._parameters)
350+
except KeyboardInterrupt:
351+
raise OhsomeException(
352+
message="Keyboard Interrupt: Query was interrupted by the user.",
351353
url=self._url,
352354
params=self._parameters,
353-
error_code=e.response.status_code,
354-
response=e.response,
355+
error_code=440,
355356
)
356-
357357
except requests.exceptions.ConnectionError as e:
358-
ohsome_exception = OhsomeException(
358+
raise OhsomeException(
359359
message="Connection Error: Query could not be sent. Make sure there are no network "
360360
f"problems and that the ohsome API URL {self._url} is valid.",
361361
url=self._url,
362362
params=self._parameters,
363363
response=e.response,
364364
)
365-
366365
except requests.exceptions.RequestException as e:
367366
if isinstance(e, RetryError):
368367
# retry one last time without retries, this will raise the original error instead of a cryptic retry
@@ -371,53 +370,57 @@ def _handle_request(self) -> OhsomeResponse:
371370
self._OhsomeBaseClient__retry = False
372371
self._handle_request()
373372

374-
ohsome_exception = OhsomeException(
373+
raise OhsomeException(
375374
message=str(e),
376375
url=self._url,
377376
params=self._parameters,
378377
response=e.response,
379378
)
379+
return response
380380

381-
except KeyboardInterrupt:
382-
ohsome_exception = OhsomeException(
383-
message="Keyboard Interrupt: Query was interrupted by the user.",
381+
def _check_response(self, response: Response) -> None:
382+
try:
383+
response.raise_for_status()
384+
except requests.exceptions.HTTPError as e:
385+
try:
386+
error_message = e.response.json()["message"]
387+
except json.decoder.JSONDecodeError:
388+
error_message = f"Invalid URL: Is {self._url} valid?"
389+
390+
raise OhsomeException(
391+
message=error_message,
384392
url=self._url,
385393
params=self._parameters,
386-
error_code=440,
394+
error_code=e.response.status_code,
395+
response=e.response,
387396
)
388397

389-
except ValueError as e:
398+
def _get_response_data(self, response: Response) -> dict:
399+
try:
400+
return response.json()
401+
except (ValueError, JSONDecodeError) as e:
390402
if response:
391403
error_code, message = extract_error_message_from_invalid_json(
392404
response.text
393405
)
394406
else:
395407
message = str(e)
396408
error_code = None
397-
ohsome_exception = OhsomeException(
409+
raise OhsomeException(
398410
message=message,
399411
url=self._url,
400412
error_code=error_code,
401413
params=self._parameters,
402414
response=response,
403415
)
404-
405416
except AttributeError:
406-
ohsome_exception = OhsomeException(
417+
raise OhsomeException(
407418
message=f"Seems like {self._url} is not a valid endpoint.",
408419
url=self._url,
409420
error_code=404,
410421
params=self._parameters,
411422
)
412423

413-
# If there has been an error and logging is enabled, write it to file
414-
if ohsome_exception:
415-
if self.log:
416-
ohsome_exception.log(self.log_dir)
417-
raise ohsome_exception
418-
419-
return OhsomeResponse(response, url=self._url, params=self._parameters)
420-
421424
def _format_parameters(self, params):
422425
"""
423426
Check and format parameters of the query

ohsome/helper.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import datetime
77
import json
88
import re
9-
from typing import Tuple, Union, List
9+
from typing import Tuple, Union, List, Optional
1010

1111
import geopandas as gpd
1212
import numpy as np
@@ -245,12 +245,12 @@ def format_list_parameters(parameters: dict) -> dict:
245245
return parameters
246246

247247

248-
def find_groupby_names(url):
248+
def find_groupby_names(url: Optional[str]) -> List[str]:
249249
"""
250250
Get the groupBy names
251251
:return:
252252
"""
253-
return [name.strip("/") for name in url.split("groupBy")[1:]]
253+
return [name.strip("/") for name in url.split("groupBy")[1:]] if url else []
254254

255255

256256
def extract_error_message_from_invalid_json(responsetext: str) -> Tuple[int, str]:

ohsome/response.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,10 @@
1616
class OhsomeResponse:
1717
"""Contains the response of the request to the ohsome API"""
1818

19-
def __init__(self, response=None, url=None, params=None):
19+
def __init__(self, data: dict, url: str = None):
2020
"""Initialize the OhsomeResponse class."""
21-
self.response = response
21+
self.data = data
2222
self.url = url
23-
self.parameters = params
24-
self.data = response.json()
2523

2624
def as_dataframe(
2725
self, multi_index: Optional[bool] = True, explode_tags: Optional[tuple] = ()

ohsome/test/conftest.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,4 @@ def dummy_ohsome_response() -> OhsomeResponse:
116116
)
117117
response = Response()
118118
response._content = test_gdf.to_json().encode()
119-
return OhsomeResponse(response=response)
119+
return OhsomeResponse(data=response.json())

ohsome/test/test_exceptions.py

+26-1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,31 @@ def test_timeout_error(base_client):
4343
)
4444

4545

46+
def test_broken_response_timeout_error(base_client):
47+
"""Test whether an OhsomeException is raised in case of a JSONDecodeError."""
48+
49+
bboxes = "8.67066,49.41423,8.68177,49.4204"
50+
time = "2010-01-01/2011-01-01/P1Y"
51+
fltr = "building=* and type:way"
52+
timeout = 30
53+
54+
client = base_client
55+
with pytest.raises(ohsome.OhsomeException) as e_info:
56+
with responses.RequestsMock() as rsps:
57+
rsps.post(
58+
"https://api.ohsome.org/v1/elements/geometry",
59+
body=b'{\n "attribution" : {\n "url" : "https://ohsome.org/copyrights",\n "text" : "\xc2\xa9 OpenStreetMap contributors"\n },\n "apiVersion" : "1.10.3",\n "type" : "FeatureCollection",\n "features" : [{\n "timestamp" : "2024-07-31T10:37:31.603661767",\n "status" : 413,\n "message" : "The given query is too large in respect to the given timeout. Please use a smaller region and/or coarser time period.",\n "requestUrl" : "https://api.ohsome.org/v1/elements/geometry"\n}',
60+
)
61+
client.elements.geometry.post(
62+
bboxes=bboxes, time=time, filter=fltr, timeout=timeout
63+
)
64+
assert (
65+
"The given query is too large in respect to the given timeout. Please use a smaller region and/or coarser "
66+
"time period." in e_info.value.message
67+
)
68+
assert e_info.value.error_code == 413
69+
70+
4671
@pytest.mark.vcr
4772
def test_invalid_url():
4873
"""
@@ -194,7 +219,7 @@ def test_exception_connection_reset(base_client):
194219
"""
195220

196221
with patch(
197-
"requests.Response.raise_for_status",
222+
"requests.sessions.Session.post",
198223
MagicMock(
199224
side_effect=RequestException(
200225
"This request was failed on purpose without response!"

ohsome/test/test_response.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ def test_explode_tags_present_on_empty_result():
561561
'{"attribution":{"url":"https://ohsome.org/copyrights","text":"© OpenStreetMap contributors"},'
562562
'"apiVersion":"1.10.1","type":"FeatureCollection","features":[]}'
563563
).encode()
564-
computed_df = OhsomeResponse(response=response).as_dataframe(
564+
computed_df = OhsomeResponse(data=response.json()).as_dataframe(
565565
explode_tags=("some_key", "some_other_key")
566566
)
567567

poetry.lock

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)