• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mthh / routingpy / 19018276231

02 Nov 2025 09:17PM UTC coverage: 88.943%. First build
19018276231

Pull #150

github

web-flow
Merge e90838d9f into eb20b436a
Pull Request #150: feat: add geotiff support valhalla isochrones

9 of 9 new or added lines in 1 file covered. (100.0%)

1649 of 1854 relevant lines covered (88.94%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.51
/routingpy/client_default.py
1
# -*- coding: utf-8 -*-
2
# Copyright (C) 2021 GIS OPS UG
3
#
4
#
5
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
6
# use this file except in compliance with the License. You may obtain a copy of
7
# the License at
8
#
9
#     http://www.apache.org/licenses/LICENSE-2.0
10
#
11
# Unless required by applicable law or agreed to in writing, software
12
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14
# License for the specific language governing permissions and limitations under
15
# the License.
16
#
17

18
import copy
1✔
19
import json
1✔
20
import random
1✔
21
import time
1✔
22
import warnings
1✔
23
from datetime import datetime
1✔
24

25
import requests
1✔
26

27
from . import exceptions
1✔
28
from .client_base import _RETRIABLE_STATUSES, DEFAULT, BaseClient, options
1✔
29
from .utils import get_ordinal
1✔
30

31

32
class Client(BaseClient):
1✔
33
    """Default client class for requests handling, which is passed to each router. Uses the requests package."""
34

35
    def __init__(
1✔
36
        self,
37
        base_url,
38
        user_agent=None,
39
        timeout=DEFAULT,
40
        retry_timeout=None,
41
        retry_over_query_limit=None,
42
        skip_api_error=None,
43
        **kwargs
44
    ):
45
        """
46
        :param base_url: The base URL for the request. All routers must provide a default.
47
            Should not have a trailing slash.
48
        :type base_url: string
49

50
        :param user_agent: User-Agent to send with the requests to routing API.
51
            Overrides ``options.default_user_agent``.
52
        :type user_agent: string
53

54
        :param timeout: Combined connect and read timeout for HTTP requests, in
55
            seconds. Specify "None" for no timeout.
56
        :type timeout: int
57

58
        :param retry_timeout: Timeout across multiple retriable requests, in
59
            seconds.
60
        :type retry_timeout: int
61

62
        :param retry_over_query_limit: If True, client will not raise an exception
63
            on HTTP 429, but instead jitter a sleeping timer to pause between
64
            requests until HTTP 200 or retry_timeout is reached.
65
        :type retry_over_query_limit: bool
66

67
        :param skip_api_error: Continue with batch processing if a :class:`routingpy.exceptions.RouterApiError` is
68
            encountered (e.g. no route found). If False, processing will discontinue and raise an error. Default False.
69
        :type skip_api_error: bool
70

71
        :param kwargs: Additional arguments, such as headers or proxies.
72
        :type kwargs: dict
73
        """
74

75
        self._session = requests.Session()
1✔
76
        super(Client, self).__init__(
1✔
77
            base_url,
78
            user_agent=user_agent,
79
            timeout=timeout,
80
            retry_timeout=retry_timeout,
81
            retry_over_query_limit=retry_over_query_limit,
82
            skip_api_error=skip_api_error,
83
            **kwargs
84
        )
85

86
        self.kwargs = kwargs or {}
1✔
87
        try:
1✔
88
            self.headers.update(self.kwargs["headers"])
1✔
89
        except KeyError:
1✔
90
            pass
1✔
91

92
        self.kwargs["headers"] = self.headers
1✔
93
        self.kwargs["timeout"] = self.timeout
1✔
94

95
        self.proxies = self.kwargs.get("proxies") or options.default_proxies
1✔
96
        if self.proxies:
1✔
97
            self.kwargs["proxies"] = self.proxies
1✔
98

99
    def _request(
1✔
100
        self,
101
        url,
102
        get_params={},
103
        post_params=None,
104
        first_request_time=None,
105
        retry_counter=0,
106
        dry_run=None,
107
    ):
108
        """Performs HTTP GET/POST with credentials, returning the body as
109
        JSON.
110

111
        :param url: URL path for the request. Should begin with a slash.
112
        :type url: string
113

114
        :param get_params: HTTP GET parameters.
115
        :type get_params: dict or list of tuples
116

117
        :param post_params: HTTP POST parameters. Only specified by calling method.
118
        :type post_params: dict
119

120
        :param first_request_time: The time of the first request (None if no
121
            retries have occurred).
122
        :type first_request_time: :class:`datetime.datetime`
123

124
        :param retry_counter: The number of this retry, or zero for first attempt.
125
        :type retry_counter: int
126

127
        :param dry_run: If true, only prints URL and parameters. true or false.
128
        :type dry_run: bool
129

130
        :raises routingpy.exceptions.RouterApiError: when the API returns an error due to faulty configuration.
131
        :raises routingpy.exceptions.RouterServerError: when the API returns a server error.
132
        :raises routingpy.exceptions.RouterError: when anything else happened while requesting.
133
        :raises routingpy.exceptions.JSONParseError: when the JSON response can't be parsed.
134
        :raises routingpy.exceptions.Timeout: when the request timed out.
135
        :raises routingpy.exceptions.TransportError: when something went wrong while trying to
136
            execute a request.
137

138
        :returns: raw JSON response or GeoTIFF image
139
        :rtype: dict or bytes
140
        """
141

142
        if not first_request_time:
1✔
143
            first_request_time = datetime.now()
1✔
144

145
        elapsed = datetime.now() - first_request_time
1✔
146
        if elapsed > self.retry_timeout:
1✔
147
            raise exceptions.Timeout()
1✔
148

149
        if retry_counter > 0:
1✔
150
            # 0.5 * (1.5 ^ i) is an increased sleep time of 1.5x per iteration,
151
            # starting at 0.5s when retry_counter=1. The first retry will occur
152
            # at 1, so subtract that first.
153
            delay_seconds = 1.5 ** (retry_counter - 1)
1✔
154

155
            # Jitter this value by 50% and pause.
156
            time.sleep(delay_seconds * (random.random() + 0.5))
1✔
157

158
        authed_url = self._generate_auth_url(url, get_params)
1✔
159

160
        final_requests_kwargs = copy.copy(self.kwargs)
1✔
161

162
        # Determine GET/POST.
163
        requests_method = self._session.get
1✔
164
        if post_params is not None:
1✔
165
            requests_method = self._session.post
1✔
166
            if final_requests_kwargs["headers"]["Content-Type"] == "application/json":
1✔
167
                final_requests_kwargs["json"] = post_params
1✔
168
            else:
169
                # Send as x-www-form-urlencoded key-value pair string (e.g. Mapbox API)
170
                final_requests_kwargs["data"] = post_params
1✔
171

172
        # Only print URL and parameters for dry_run
173
        if dry_run:
1✔
174
            print(
1✔
175
                "url:\n{}\nParameters:\n{}".format(
176
                    self.base_url + authed_url, json.dumps(final_requests_kwargs, indent=2)
177
                )
178
            )
179
            return
1✔
180

181
        try:
1✔
182
            response = requests_method(self.base_url + authed_url, **final_requests_kwargs)
1✔
183
            self._req = response.request
1✔
184

185
        except requests.exceptions.Timeout:
×
186
            raise exceptions.Timeout()
×
187

188
        tried = retry_counter + 1
1✔
189

190
        if response.status_code in _RETRIABLE_STATUSES:
1✔
191
            # Retry request.
192
            warnings.warn(
1✔
193
                "Server down.\nRetrying for the {}{} time.".format(tried, get_ordinal(tried)),
194
                UserWarning,
195
            )
196
            return self._request(url, get_params, post_params, first_request_time, retry_counter + 1)
1✔
197

198
        try:
1✔
199
            return self._get_body(response)
1✔
200

201
        except exceptions.RouterApiError:
1✔
202
            if self.skip_api_error:
1✔
203
                warnings.warn(
1✔
204
                    "Router {} returned an API error with "
205
                    "the following message:\n{}".format(self.__class__.__name__, response.text)
206
                )
207
                return
1✔
208

209
            raise
1✔
210

211
        except exceptions.RetriableRequest as e:
1✔
212
            if isinstance(e, exceptions.OverQueryLimit) and not self.retry_over_query_limit:
1✔
213
                raise
1✔
214

215
            warnings.warn(
×
216
                "Rate limit exceeded.\nRetrying for the {}{} time.".format(tried, get_ordinal(tried)),
217
                UserWarning,
218
            )
219
            # Retry request.
220
            return self._request(url, get_params, post_params, first_request_time, retry_counter + 1)
×
221

222
    @property
1✔
223
    def req(self):
1✔
224
        """Holds the :class:`requests.PreparedRequest` property for the last request."""
225
        return self._req
1✔
226

227
    @staticmethod
1✔
228
    def _get_body(response):
1✔
229
        status_code = response.status_code
1✔
230
        content_type = response.headers["content-type"]
1✔
231

232
        if status_code == 200:
1✔
233
            if content_type == "image/tiff":
1✔
234
                return response.content
1✔
235

236
            else:
237
                try:
1✔
238
                    return response.json()
1✔
239

240
                except json.decoder.JSONDecodeError:
×
241
                    raise exceptions.JSONParseError(
×
242
                        "Can't decode JSON response:{}".format(response.text)
243
                    )
244

245
        if status_code == 429:
1✔
246
            raise exceptions.OverQueryLimit(status_code, response.text)
1✔
247

248
        if 400 <= status_code < 500:
1✔
249
            raise exceptions.RouterApiError(status_code, response.text)
1✔
250

251
        if 500 <= status_code:
×
252
            raise exceptions.RouterServerError(status_code, response.text)
×
253

254
        if status_code != 200:
×
255
            raise exceptions.RouterError(status_code, response.text)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc