Repository: GIScience/openrouteservice-py

Issue:
JSONDecodeError  when requesting directions in gpx format 
<!-- If you have a question rather than an actual issue, pls use our forum:-->

<!-- https://ask.openrouteservice.org/c/sdks-->

#### Here's what I did
<!-- best paste the results of 'dry_run' (see README.md), so we can see your configuration -->
```coordinates_lon_lat = [[-0.0858923, 51.5050313],
 [-0.0743414, 51.5015841],
 [-0.05273140000000039, 51.46430039999999],
 [-0.0531279, 51.4637547],
 [0.0320358, 51.3579627],
 [0.030053528946478628, 51.3510888399767],
 [0.0373899, 51.3508184],
 [0.05585272964791204, 51.33450105290082],
 [0.0528395, 51.3311335],
 [0.1014503, 51.246361],
 [0.14227140698558516, 51.18576100547525],
 [0.1458905, 51.1853518],
 [0.1459325, 51.1858536]]

gpx = directions.directions(clnt, coordinates_lon_lat, profile ='cycling-regular', 
                    format = 'gpx',preference = 'recommended' )
```

The same request is processed without any errors with format='json' or  format='geojson' and the correct route is returned

dry_run output:
```url:
https://api.openrouteservice.org/v2/directions/driving-car/json?
Headers:
{
  "headers": {
    "User-Agent": "ORSClientPython.v2.3.0",
    "Content-type": "application/json",
    "Authorization": "5b3ce3597851110001cf62484bacb83d2107475198ea22650a25b872"
  },
  "timeout": 60,
  "json": {
    "coordinates": [
      [
        8.34234,
        48.23424
      ],
      [
        8.34423,
        48.26424
      ]
    ]
  }
}```
---
#### Here's what I got
<!-- we :heart: json outputs -->
```---------------------------------------------------------------------------
JSONDecodeError                           Traceback (most recent call last)
~/miniconda3/envs/ox/lib/python3.8/site-packages/openrouteservice/client.py in _get_body(response)
    228         try:
--> 229             body = response.json()
    230         except json.JSONDecodeError:

~/miniconda3/envs/ox/lib/python3.8/site-packages/requests/models.py in json(self, **kwargs)
    897                     pass
--> 898         return complexjson.loads(self.text, **kwargs)
    899 

~/miniconda3/envs/ox/lib/python3.8/json/__init__.py in loads(s, cls, object_hook, parse_float, parse_int, parse_constant, object_pairs_hook, **kw)
    356             parse_constant is None and object_pairs_hook is None and not kw):
--> 357         return _default_decoder.decode(s)
    358     if cls is None:

~/miniconda3/envs/ox/lib/python3.8/json/decoder.py in decode(self, s, _w)
    336         """
--> 337         obj, end = self.raw_decode(s, idx=_w(s, 0).end())
    338         end = _w(s, end).end()

~/miniconda3/envs/ox/lib/python3.8/json/decoder.py in raw_decode(self, s, idx)
    354         except StopIteration as err:
--> 355             raise JSONDecodeError("Expecting value", s, err.value) from None
    356         return obj, end

JSONDecodeError: Expecting value: line 1 column 1 (char 0)

During handling of the above exception, another exception occurred:

HTTPError                                 Traceback (most recent call last)
<ipython-input-129-0570940137d3> in <module>
----> 1 gpx = directions.directions(clnt, coordinates_lon_lat, profile ='cycling-regular', 
      2                     format = 'gpx',preference = 'recommended' )

~/miniconda3/envs/ox/lib/python3.8/site-packages/openrouteservice/directions.py in directions(client, coordinates, profile, format_out, format, preference, units, language, geometry, geometry_simplify, instructions, instructions_format, alternative_routes, roundabout_exits, attributes, maneuvers, radiuses, bearings, skip_segments, continue_straight, elevation, extra_info, suppress_warnings, optimized, optimize_waypoints, options, validate, dry_run)
    279         params['options'] = options
    280 
--> 281     return client.request("/v2/directions/" + profile + '/' + format, {}, post_json=params, dry_run=dry_run)
    282 
    283 

~/miniconda3/envs/ox/lib/python3.8/site-packages/openrouteservice/client.py in request(self, url, get_params, first_request_time, retry_counter, requests_kwargs, post_json, dry_run)
    202 
    203         try:
--> 204             result = self._get_body(response)
    205 
    206             return result

~/miniconda3/envs/ox/lib/python3.8/site-packages/openrouteservice/client.py in _get_body(response)
    229             body = response.json()
    230         except json.JSONDecodeError:
--> 231             raise exceptions.HTTPError(response.status_code)
    232 
    233         # error = body.get('error')

HTTPError: HTTP Error: 200
```
---
#### Here's what I was expecting
<!-- try being as explicit as possible here so we know how to fix this issue -->
Same directions in GPX format

The same request generated as Python snippet from ORS API playground works correctly:

```import requests

body = {"coordinates":[[-0.0858923,51.5050313],[-0.0743414,51.5015841],[-0.05273140000000039,51.46430039999999],[-0.0531279,51.4637547],[0.0320358,51.3579627],[0.030053528946478628,51.3510888399767],[0.0373899,51.3508184],[0.05585272964791204,51.33450105290082],[0.0528395,51.3311335],[0.1014503,51.246361],[0.14227140698558516,51.18576100547525],[0.1458905,51.1853518],[0.1459325,51.1858536]],"preference":"recommended"}

headers = {
    'Accept': 'application/json, application/geo+json, application/gpx+xml, img/png; charset=utf-8',
    'Authorization': '5b3ce3597851110001cf62484bacb83d2107475198ea22650a25b872',
    'Content-Type': 'application/json; charset=utf-8'
}
call = requests.post('https://api.openrouteservice.org/v2/directions/cycling-regular/gpx', json=body, headers=headers)

print(call.status_code, call.reason)
print(call.text)
```
---
#### Here's what I think could be improved
API response in GPX format should be correctly parsed or delivered as is

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience) Report
> Merging [#67](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience) (834e959) into [master](https://codecov.io/gh/GIScience/openrouteservice-py/commit/ed5e5cd8ec406fad05381bf7bda64a77ce4bfe89?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience) (ed5e5cd) will **decrease** coverage by `0.37%`.
> The diff coverage is `71.42%`.

[![Impacted file tree graph](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67/graphs/tree.svg?width=650&height=150&src=pr&token=QqGC8XfCiI&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience)](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience)

```diff
@@             Coverage Diff             @@
##            master      #67      +/-   ##
===========================================
- Coverage   100.00%   99.62%   -0.38%     
===========================================
  Files           12       12              
  Lines          525      527       +2     
===========================================
  Hits           525      525              
- Misses           0        2       +2     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `99.62% <71.42%> (-0.38%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience) | Coverage Δ | |
|---|---|---|
| [openrouteservice/directions.py](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience#diff-b3BlbnJvdXRlc2VydmljZS9kaXJlY3Rpb25zLnB5) | `100.00% <ø> (ø)` | |
| [openrouteservice/client.py](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience#diff-b3BlbnJvdXRlc2VydmljZS9jbGllbnQucHk=) | `98.40% <71.42%> (-1.60%)` | :arrow_down: |
| [openrouteservice/convert.py](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience#diff-b3BlbnJvdXRlc2VydmljZS9jb252ZXJ0LnB5) | `100.00% <0.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience). Last update [ed5e5cd...834e959](https://codecov.io/gh/GIScience/openrouteservice-py/pull/67?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=GIScience).

jayaddison: cc @koebi @MichaelsJP (not urgent; I noticed issue #67 a while ago and figured it'd be worth attempting an implementation)
MichaelsJP: @jayaddison Thx a lot for the contribution. :ok_hand:  I'll have a look at it the next days. 

Patch:
diff --git a/openrouteservice/client.py b/openrouteservice/client.py
index ea763c4..3711d80 100644
--- a/openrouteservice/client.py
+++ b/openrouteservice/client.py
@@ -20,6 +20,7 @@
 
 from datetime import datetime
 from datetime import timedelta
+import cgi
 import functools
 import requests
 import json
@@ -257,10 +258,15 @@ class Client(object):
     @staticmethod
     def _get_body(response):
         """Returns the body of a response object, raises status code exceptions if necessary."""
-        try:
-            body = response.json()
-        except json.JSONDecodeError:  # pragma: no cover
-            raise exceptions.HTTPError(response.status_code)
+        content_type = response.headers["Content-Type"]
+        mime_type, _ = cgi.parse_header(content_type)
+        if mime_type == "application/gpx+xml":
+            body = response.text
+        else:
+            try:
+                body = response.json()
+            except json.JSONDecodeError:  # pragma: no cover
+                raise exceptions.HTTPError(response.status_code)
 
         # error = body.get('error')
         status_code = response.status_code
diff --git a/openrouteservice/directions.py b/openrouteservice/directions.py
index c42cdc1..5b6a69b 100644
--- a/openrouteservice/directions.py
+++ b/openrouteservice/directions.py
@@ -71,7 +71,7 @@ def directions(
     :param format: Specifies the response format. One of ['json', 'geojson', 'gpx']. Default "json".
         Geometry format for "json" is Google's encodedpolyline. The GPX schema the response is validated
         against can be found here:
-        https://raw.githubusercontent.com/GIScience/openrouteservice-schema/master/gpx/v1/ors-gpx.xsd.
+        https://raw.githubusercontent.com/GIScience/openrouteservice-schema/master/gpx/v2/ors-gpx.xsd.
     :type format: str
 
     :param format_out: DEPRECATED.


Test patch:
diff --git a/test/test_directions.py b/test/test_directions.py
index b6e2db8..6fb3b66 100644
--- a/test/test_directions.py
+++ b/test/test_directions.py
@@ -25,7 +25,7 @@ import test as _test
 from copy import deepcopy
 
 from openrouteservice import exceptions
-from test.test_helper import ENDPOINT_DICT
+from test.test_helper import ENDPOINT_DICT, GPX_RESPONSE
 
 
 class DirectionsTest(_test.TestCase):
@@ -48,6 +48,26 @@ class DirectionsTest(_test.TestCase):
         self.assertEqual(resp, self.valid_query)
         self.assertIn("sample_key", responses.calls[0].request.headers.values())
 
+    @responses.activate
+    def test_directions_gpx(self):
+        query = deepcopy(self.valid_query)
+        query["format"] = "gpx"
+
+        responses.add(
+            responses.POST,
+            "https://api.openrouteservice.org/v2/directions/{}/gpx".format(
+                self.valid_query["profile"]
+            ),
+            body=GPX_RESPONSE,
+            status=200,
+            content_type="application/gpx+xml;charset=UTF-8",
+        )
+
+        resp = self.client.directions(**query)
+
+        self.assertEqual(resp, GPX_RESPONSE)
+        self.assertIn("sample_key", responses.calls[0].request.headers.values())
+
     @responses.activate
     def test_directions_incompatible_parameters(self):
         self.valid_query["optimized"] = True
diff --git a/test/test_helper.py b/test/test_helper.py
index f2d12b8..459e554 100644
--- a/test/test_helper.py
+++ b/test/test_helper.py
@@ -225,3 +225,29 @@ ENDPOINT_DICT = {
         "matrix": PARAM_LIST_TWO,
     },
 }
+
+GPX_RESPONSE = """
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<gpx version="1.0" creator="openrouteservice" xmlns="https://raw.githubusercontent.com/GIScience/openrouteservice-schema/master/gpx/v2/ors-gpx.xsd">
+  <metadata>
+    <name>openrouteservice directions</name>
+    <desc>This is a directions instructions file as GPX, generated from openrouteservice</desc>
+    <author><name>openrouteservice</name>
+      <email id="support" domain="openrouteservice.heigit.org"/>
+      <link href="https://openrouteservice.org/">
+        <text>https://openrouteservice.org/</text>
+        <type>text/html</type>
+      </link>
+    </author>
+    <copyright author="openrouteservice.org | OpenStreetMap contributors">
+      <year>2021</year>
+      <license>LGPL 3.0</license>
+    </copyright>
+    <time>2021-07-25T17:26:59.023Z</time>
+    <bounds maxLat="51.501602" maxLon="0.147508" minLat="51.185303" minLon="-0.076743"/>
+    <extensions>
+      <system-message></system-message>
+    </extensions>
+  </metadata>
+</gpx>
+"""