• 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

92.72
/routingpy/routers/graphhopper.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
from typing import List, Optional, Tuple, Union  # noqa: F401
1✔
19

20
from .. import convert, utils
1✔
21
from ..client_base import DEFAULT
1✔
22
from ..client_default import Client
1✔
23
from ..direction import Direction, Directions
1✔
24
from ..isochrone import Isochrone, Isochrones
1✔
25
from ..matrix import Matrix
1✔
26

27

28
class Graphhopper:
1✔
29
    """Performs requests to the Graphhopper API services."""
30

31
    _DEFAULT_BASE_URL = "https://graphhopper.com/api/1"
1✔
32

33
    def __init__(
1✔
34
        self,
35
        api_key: Optional[str] = None,
36
        base_url: Optional[str] = _DEFAULT_BASE_URL,
37
        user_agent: Optional[str] = None,
38
        timeout: Optional[int] = DEFAULT,
39
        retry_timeout: Optional[int] = None,
40
        retry_over_query_limit: Optional[bool] = False,
41
        skip_api_error: Optional[bool] = None,
42
        client=Client,
43
        **client_kwargs
44
    ):
45
        """
46
        Initializes an graphhopper client.
47

48
        :param api_key: GH API key. Required if https://graphhopper.com/api is used.
49
        :type api_key: str
50

51
        :param base_url: The base URL for the request. Defaults to the ORS API
52
            server. Should not have a trailing slash.
53
        :type base_url: str
54

55
        :param user_agent: User Agent to be used when requesting.
56
            Default :attr:`routingpy.routers.options.default_user_agent`.
57
        :type user_agent: str
58

59
        :param timeout: Combined connect and read timeout for HTTP requests, in
60
            seconds. Specify ``None`` for no timeout. Default :attr:`routingpy.routers.options.default_timeout`.
61
        :type timeout: int or None
62

63
        :param retry_timeout: Timeout across multiple retriable requests, in
64
            seconds.  Default :attr:`routingpy.routers.options.default_retry_timeout`.
65
        :type retry_timeout: int
66

67
        :param retry_over_query_limit: If True, client will not raise an exception
68
            on HTTP 429, but instead jitter a sleeping timer to pause between
69
            requests until HTTP 200 or retry_timeout is reached.
70
            Default :attr:`routingpy.routers.options.default_retry_over_query_limit`.
71
        :type retry_over_query_limit: bool
72

73
        :param skip_api_error: Continue with batch processing if a :class:`routingpy.exceptions.RouterApiError` is
74
            encountered (e.g. no route found). If False, processing will discontinue and raise an error.
75
            Default :attr:`routingpy.routers.options.default_skip_api_error`.
76
        :type skip_api_error: bool
77

78
        :param client: A client class for request handling. Needs to be derived from :class:`routingpy.client_base.BaseClient`
79
        :type client: abc.ABCMeta
80

81
        :param client_kwargs: Additional arguments passed to the client, such as headers or proxies.
82
        :type client_kwargs: dict
83

84
        """
85

86
        if base_url == self._DEFAULT_BASE_URL and api_key is None:
1✔
87
            raise KeyError("API key must be specified.")
×
88
        self.key = api_key
1✔
89

90
        self.client = client(
1✔
91
            base_url,
92
            user_agent,
93
            timeout,
94
            retry_timeout,
95
            retry_over_query_limit,
96
            skip_api_error,
97
            **client_kwargs
98
        )
99

100
    def directions(  # noqa: C901
1✔
101
        self,
102
        locations: Union[List[List[float]], Tuple[Tuple[float]]],
103
        profile: str,
104
        format: Optional[str] = None,
105
        optimize: Optional[bool] = None,
106
        instructions: Optional[bool] = None,
107
        locale: Optional[str] = None,
108
        elevation: Optional[bool] = None,
109
        points_encoded: Optional[bool] = True,
110
        calc_points: Optional[bool] = None,
111
        debug: Optional[bool] = None,
112
        point_hints: Optional[List[str]] = None,
113
        details: Optional[List[str]] = None,
114
        ch_disable: Optional[bool] = None,
115
        custom_model: Optional[dict] = None,
116
        headings: Optional[List[int]] = None,
117
        heading_penalty: Optional[int] = None,
118
        pass_through: Optional[bool] = None,
119
        algorithm: Optional[str] = None,
120
        round_trip_distance: Optional[int] = None,
121
        round_trip_seed: Optional[int] = None,
122
        alternative_route_max_paths: Optional[int] = None,
123
        alternative_route_max_weight_factor: Optional[float] = None,
124
        alternative_route_max_share_factor: Optional[float] = None,
125
        dry_run: Optional[bool] = None,
126
        snap_preventions: Optional[List[str]] = None,
127
        curbsides: Optional[List[str]] = None,
128
        **direction_kwargs
129
    ):
130
        """Get directions between an origin point and a destination point.
131

132
        Use ``direction_kwargs`` for any missing ``directions`` request options.
133

134
        For more information, visit https://docs.graphhopper.com/openapi/routing/postroute.
135

136
        :param locations: The coordinates tuple the route should be calculated
137
            from in order of visit.
138
        :type locations: list of list or tuple of tuple
139

140
        :param profile: The vehicle for which the route should be calculated. One of ["car" "bike" "foot" "hike" "mtb"
141
            "racingbike" "scooter" "truck" "small_truck"]. Default "car".
142
        :type profile: str
143

144
        :param format: Specifies the resulting format of the route, for json the content type will be application/json.
145
            Default "json".
146
        :type format: str
147

148
        :param locale: Language for routing instructions. The locale of the resulting turn instructions.
149
            E.g. pt_PT for Portuguese or de for German. Default "en".
150
        :type locale: str
151

152
        :param optimize: If false the order of the locations will be identical to the order of the point parameters.
153
            If you have more than 2 points you can set this optimize parameter to ``True`` and the points will be sorted
154
            regarding the minimum overall time - e.g. suiteable for sightseeing tours or salesman.
155
            Keep in mind that the location limit of the Route Optimization API applies and the credit costs are higher!
156
            Note to all customers with a self-hosted license: this parameter is only available if your package includes
157
            the Route Optimization API. Default False.
158
        :type optimize: bool
159

160
        :param instructions: Specifies whether to return turn-by-turn instructions.
161
            Default True.
162
        :type instructions: bool
163

164
        :param elevation: If true a third dimension - the elevation - is included in the polyline or in the GeoJson.
165
            IMPORTANT: If enabled you have to use a modified version of the decoding method or set points_encoded to false.
166
            See the points_encoded attribute for more details. Additionally a request can fail if the vehicle does not
167
            support elevation. See the features object for every vehicle.
168
            Default False.
169
        :type elevation: bool
170

171
        :param points_encoded: If ``False`` the coordinates in point and snapped_waypoints are returned as array using the order
172
            [lon,lat,elevation] for every point. If true the coordinates will be encoded as string leading to less bandwith usage.
173
            Default True.
174
        :type points_encoded: bool
175

176
        :param calc_points: If the points for the route should be calculated at all, printing out only distance and time.
177
            Default True.
178
        :type calc_points: bool
179

180
        :param debug: If ``True``, the output will be formated.
181
            Default False.
182
        :type debug: bool
183

184
        :param point_hints: The point_hints is typically a road name to which the associated point parameter should be
185
            snapped to. Specify no point_hint parameter or the same number as you have locations. Optional.
186
        :type point_hints: list of str
187

188
        :param details: Optional parameter to retrieve path details. You can request additional details for the route:
189
            street_name, time, distance, max_speed, toll, road_class, road_class_link, road_access, road_environment,
190
            lanes, and surface.
191
        :type details: list of str
192

193
        :param ch_disable: Always use ch_disable=true in combination with one or more parameters of this table.
194
            Default False.
195
        :type ch_disable: bool
196

197
        :param custom_model: The custom_model modifies the routing behaviour of the specified profile.
198
            See https://docs.graphhopper.com/openapi/custom-model
199
        :type custom_model: dict
200

201
        :param headings: Optional parameter. Favour a heading direction for a certain point. Specify either one heading for the start point or as
202
            many as there are points. In this case headings are associated by their order to the specific points.
203
            Headings are given as north based clockwise angle between 0 and 360 degree.
204
        :type headings: list of int
205

206
        :param heading_penalty: Optional parameter. Penalty for omitting a specified heading. The penalty corresponds to the accepted time
207
            delay in seconds in comparison to the route without a heading.
208
            Default 120.
209
        :type heading_penalty: int
210

211
        :param pass_through: Optional parameter. If true u-turns are avoided at via-points with regard to the heading_penalty.
212
            Default False.
213
        :type pass_through: bool
214

215
        :param algorithm: Optional parameter. round_trip or alternative_route.
216
        :type algorithm: str
217

218
        :param round_trip_distance: If algorithm=round_trip this parameter configures approximative length of the resulting round trip.
219
            Default 10000.
220
        :type round_trip_distance: int
221

222
        :param round_trip_seed: If algorithm=round_trip this parameter introduces randomness if e.g. the first try wasn't good.
223
            Default 0.
224
        :type round_trip_seed: int
225

226
        :param alternative_route_max_paths: If algorithm=alternative_route this parameter sets the number of maximum paths
227
            which should be calculated. Increasing can lead to worse alternatives.
228
            Default 2.
229
        :type alternative_route_max_paths: int
230

231
        :param alternative_route_max_weight_factor: If algorithm=alternative_route this parameter sets the factor by which the alternatives
232
            routes can be longer than the optimal route. Increasing can lead to worse alternatives.
233
            Default 1.4.
234
        :type alternative_route_max_weight_factor: float
235

236
        :param alternative_route_max_share_factor: If algorithm=alternative_route this parameter specifies how much alternatives
237
            routes can have maximum in common with the optimal route. Increasing can lead to worse alternatives.
238
            Default 0.6.
239
        :type alternative_route_max_share_factor: float
240

241
        :param dry_run: Print URL and parameters without sending the request.
242
        :type dry_run: bool
243

244
        :param snap_preventions: Optional parameter to avoid snapping to a certain road class or road environment.
245
            Currently supported values are motorway, trunk, ferry, tunnel, bridge and ford. Optional.
246
        :type snap_preventions: list of str
247

248
        :param curbsides: One of "any", "right", "left". It specifies on which side a point should be relative to the driver
249
            when she leaves/arrives at a start/target/via point. You need to specify this parameter for either none
250
            or all points. Only supported for motor vehicles and OpenStreetMap.
251
        :type curbsides: list of str
252

253
        :returns: One or multiple route(s) from provided coordinates and restrictions.
254
        :rtype: :class:`routingpy.direction.Direction` or :class:`routingpy.direction.Directions`
255

256
        .. versionchanged:: 0.3.0
257
           `point_hint` used to be bool, which was not the right usage.
258

259
        .. versionadded:: 0.3.0
260
           ``snap_prevention``, ``curb_side``, ``turn_costs`` parameters
261

262
        .. versionchanged:: 1.2.0
263
           Renamed `point_hint` to `point_hints`, `heading` to `headings`,
264
           `snap_prevention` to `snap_preventions`, `curb_side` to `curbsides`,
265

266
        .. versionadded:: 1.2.0
267
           Added `custom_model` parameter
268

269
        .. deprecated:: 1.2.0
270
           Removed `weighting`, `block_area`, `avoid`, `turn_costs` parameters
271
        """
272

273
        params = {"profile": profile}
1✔
274

275
        if locations is not None:
1✔
276
            params["points"] = locations
1✔
277

278
        get_params = {}
1✔
279

280
        if self.key is not None:
1✔
281
            get_params["key"] = self.key
1✔
282

283
        if format is not None:
1✔
284
            params["type"] = format
1✔
285

286
        if optimize is not None:
1✔
287
            params["optimize"] = optimize
1✔
288

289
        if instructions is not None:
1✔
290
            params["instructions"] = instructions
1✔
291

292
        if locale is not None:
1✔
293
            params["locale"] = locale
1✔
294

295
        if elevation is not None:
1✔
296
            params["elevation"] = elevation
1✔
297

298
        if points_encoded is not None:
1✔
299
            params["points_encoded"] = points_encoded
1✔
300

301
        if calc_points is not None:
1✔
302
            params["calc_points"] = calc_points
1✔
303

304
        if debug is not None:
1✔
305
            params["debug"] = debug
1✔
306

307
        if point_hints is not None:
1✔
308
            params["point_hints"] = point_hints
1✔
309

310
        if snap_preventions:
1✔
311
            params["snap_preventions"] = snap_preventions
1✔
312

313
        if curbsides:
1✔
314
            params["curbsides"] = curbsides
1✔
315

316
        ### all below params will only work if ch is disabled
317

318
        if details is not None:
1✔
319
            params["details"] = details
1✔
320

321
        if ch_disable is not None:
1✔
322
            params["ch.disable"] = ch_disable
1✔
323

324
        if custom_model is not None:
1✔
325
            params["custom_model"] = custom_model
1✔
326

327
        if headings is not None:
1✔
328
            params["headings"] = headings
×
329

330
        if heading_penalty is not None:
1✔
331
            params["heading_penalty"] = heading_penalty
1✔
332

333
        if pass_through is not None:
1✔
334
            params["pass_through"] = pass_through
1✔
335

336
        if algorithm is not None:
1✔
337
            params["algorithm"] = algorithm
1✔
338

339
            if algorithm == "round_trip":
1✔
340
                if round_trip_distance is not None:
×
341
                    params["round_trip.distance"] = round_trip_distance
×
342

343
                if round_trip_seed is not None:
×
344
                    params["round_trip.seed"] = round_trip_seed
×
345

346
            if algorithm == "alternative_route":
1✔
347
                if alternative_route_max_paths is not None:
1✔
348
                    params["alternative_route.max_paths"] = alternative_route_max_paths
1✔
349

350
                if alternative_route_max_weight_factor is not None:
1✔
351
                    params["alternative_route.max_weight_factor"] = alternative_route_max_weight_factor
1✔
352

353
                if alternative_route_max_share_factor:
1✔
354
                    params["alternative_route_max_share_factor"] = alternative_route_max_share_factor
1✔
355

356
        params = utils.deep_merge_dicts(params, direction_kwargs)
1✔
357

358
        return self.parse_directions_json(
1✔
359
            self.client._request("/route", get_params=get_params, post_params=params, dry_run=dry_run),
360
            algorithm,
361
            elevation,
362
            points_encoded,
363
        )
364

365
    @staticmethod
1✔
366
    def parse_directions_json(response, algorithm, elevation, points_encoded):
1✔
367
        if response is None:  # pragma: no cover
368
            if algorithm == "alternative_route":
369
                return Directions()
370
            else:
371
                return Direction()
372

373
        if algorithm == "alternative_route":
1✔
374
            routes = []
1✔
375
            for route in response["paths"]:
1✔
376
                geometry = (
1✔
377
                    utils.decode_polyline5(route["points"], elevation)
378
                    if points_encoded
379
                    else route["points"]["coordinates"]
380
                )
381
                routes.append(
1✔
382
                    Direction(
383
                        geometry=geometry,
384
                        duration=int(route["time"] / 1000),
385
                        distance=int(route["distance"]),
386
                        raw=route,
387
                    )
388
                )
389
            return Directions(routes, response)
1✔
390
        else:
391
            geometry = (
1✔
392
                utils.decode_polyline5(response["paths"][0]["points"], elevation)
393
                if points_encoded
394
                else response["paths"][0]["points"]["coordinates"]
395
            )
396
            return Direction(
1✔
397
                geometry=geometry,
398
                duration=int(response["paths"][0]["time"] / 1000),
399
                distance=int(response["paths"][0]["distance"]),
400
                raw=response,
401
            )
402

403
    def isochrones(
1✔
404
        self,
405
        locations: Union[Tuple[float], List[float]],
406
        profile: str,
407
        intervals: Union[List[int], Tuple[int]],
408
        type: Optional[str] = "json",
409
        buckets: Optional[int] = 1,
410
        interval_type: Optional[str] = "time",
411
        reverse_flow: Optional[bool] = None,
412
        debug: Optional[bool] = None,
413
        dry_run: Optional[bool] = None,
414
        **isochrones_kwargs
415
    ):
416
        """Gets isochrones or equidistants for a range of time/distance values around a given set of coordinates.
417

418
        Use ``isochrones_kwargs`` for missing ``isochrones`` request options.
419

420
        For more details visit https://docs.graphhopper.com/openapi/isochrones.
421

422
        :param locations: One coordinate pair denoting the location.
423
        :type locations: tuple of float or list of float
424

425
        :param profile: Specifies the mode of transport.
426
            One of "car" "bike" "foot" "hike" "mtb" "racingbike" "scooter" "truck" "small_truck". Default "car".
427
        :type profile: str
428

429
        :param intervals: Maximum range to calculate distances/durations for. You can also specify
430
            the ``buckets`` variable to break the single value into more isochrones. For compatibility reasons,
431
            this parameter is expressed as list. In meters or seconds depending on `interval_type`.
432
        :type intervals: list of int or tuple of int
433

434
        :param interval_type: Set ``time`` for isochrones or ``distance`` for equidistants.
435
            Default 'time'.
436
        :type interval_type: str
437

438
        :param buckets: For how many sub intervals an additional polygon should be calculated.
439
            Default 1.
440
        :type buckets: int
441

442
        :param reverse_flow: If false the flow goes from point to the polygon,
443
            if true the flow goes from the polygon "inside" to the point.
444
            Default False.
445
        :param reverse_flow: bool
446

447
        :param debug: If true, the output will be formatted.
448
            Default False
449
        :type debug: bool
450

451
        :param dry_run: Print URL and parameters without sending the request.
452
        :param dry_run: bool
453

454
        :returns: An isochrone with the specified range.
455
        :rtype: :class:`routingpy.isochrone.Isochrones`
456
        """
457

458
        params = [("profile", profile), ("type", type)]
1✔
459
        params = [("profile", profile), ("type", type)]
1✔
460

461
        if convert.is_list(intervals):
1✔
462
            if interval_type in (None, "time"):
1✔
463
                params.append(("time_limit", intervals[0]))
1✔
464
            elif interval_type == "distance":
×
465
                params.append(("distance_limit", intervals[0]))
×
466
        else:
467
            raise TypeError("Parameter range={} must be of type list or tuple".format(range))
×
468

469
        center = [convert.format_float(f) for f in locations]
1✔
470
        center.reverse()
1✔
471
        params.append(("point", ",".join(center)))
1✔
472

473
        if self.key is not None:
1✔
474
            params.append(("key", self.key))
1✔
475

476
        if buckets is not None:
1✔
477
            params.append(("buckets", buckets))
1✔
478

479
        if reverse_flow is not None:
1✔
480
            params.append(("reverse_flow", convert.convert_bool(reverse_flow)))
1✔
481

482
        if debug is not None:
1✔
483
            params.append(("debug", convert.convert_bool(debug)))
1✔
484

485
        params.extend(isochrones_kwargs.items())
1✔
486

487
        return self.parse_isochrone_json(
1✔
488
            self.client._request("/isochrone", get_params=params, dry_run=dry_run),
489
            type,
490
            intervals[0],
491
            buckets,
492
            center,
493
            interval_type,
494
        )
495

496
    @staticmethod
1✔
497
    def parse_isochrone_json(response, type, max_range, buckets, center, interval_type):
1✔
498
        if response is None:  # pragma: no cover
499
            return Isochrones()
500

501
        isochrones = []
1✔
502
        accessor = "polygons" if type == "json" else "features"
1✔
503
        for index, polygon in enumerate(response[accessor]):
1✔
504
            isochrones.append(
1✔
505
                Isochrone(
506
                    geometry=[
507
                        l[:2] for l in polygon["geometry"]["coordinates"][0]  # noqa: E741
508
                    ],  # takes in elevation for some reason
509
                    interval=int(max_range * ((polygon["properties"]["bucket"] + 1) / buckets)),
510
                    center=center,
511
                    interval_type=interval_type,
512
                )
513
            )
514

515
        return Isochrones(isochrones, response)
1✔
516

517
    def matrix(
1✔
518
        self,
519
        locations: Union[
520
            List[Union[List[float], Tuple[float]]], Tuple[Union[List[float], Tuple[float]]]
521
        ],
522
        profile: str,
523
        sources: Optional[List[int]] = None,
524
        destinations: Optional[List[int]] = None,
525
        out_array: Optional[List[str]] = ["times", "distances"],
526
        debug=None,
527
        dry_run: Optional[bool] = None,
528
        **matrix_kwargs
529
    ):
530
        """Gets travel distance and time for a matrix of origins and destinations.
531

532
        Use ``matrix_kwargs`` for any missing ``matrix`` request options.
533

534
        For more details visit https://docs.graphhopper.com/openapi/matrices.
535

536
        :param locations: Specify multiple points for which the weight-, route-, time- or distance-matrix should be calculated.
537
            In this case the starts are identical to the destinations.
538
            If there are N points, then NxN entries will be calculated.
539
            The order of the point parameter is important. Specify at least three points.
540
            Is a string with the format latitude,longitude.
541
        :type locations: List[List[float]|Tuple[float]]|Tuple[List[float]|Tuple[float]]
542

543
        :param profile: Specifies the mode of transport.
544
            One of bike, car, foot or other vehicles supported by GraphHopper
545
            (see https://docs.graphhopper.com/openapi/map-data-and-routing-profiles).
546
            Default "car".
547
        :type profile: str
548

549
        :param sources: The starting points for the routes.
550
            Specifies an index referring to locations.
551
        :type sources: List[int]
552

553
        :param destinations: The destination points for the routes. Specifies an index referring to locations.
554
        :type destinations: List[int]
555

556
        :param out_array: Specifies which arrays should be included in the response. Specify one or more of the following
557
            options 'weights', 'times', 'distances'.
558
            The units of the entries of distances are meters, of times are seconds and of weights is arbitrary and it can differ
559
            for different vehicles or versions of this API.
560
            Default ["times", "distance"].
561
        :type out_array: List[str]
562

563
        :param dry_run: Print URL and parameters without sending the request.
564
        :param dry_run: bool
565

566
        :returns: A matrix from the specified sources and destinations.
567
        :rtype: :class:`routingpy.matrix.Matrix`
568
        """
569
        params = [("profile", profile)]
1✔
570

571
        if self.key is not None:
1✔
572
            params.append(("key", self.key))
1✔
573

574
        if sources is None and destinations is None:
1✔
575
            locations = (reversed([convert.format_float(f) for f in coord]) for coord in locations)
1✔
576
            params.extend([("point", ",".join(coord)) for coord in locations])
1✔
577

578
        else:
579
            sources_out = locations
1✔
580
            destinations_out = locations
1✔
581
            try:
1✔
582
                sources_out = []
1✔
583
                for idx in sources:
1✔
584
                    sources_out.append(locations[idx])
1✔
585
            except IndexError:
1✔
586
                raise IndexError("Parameter sources out of locations range at index {}.".format(idx))
1✔
587
            except TypeError:
1✔
588
                # Raised when sources == None
589
                pass
1✔
590
            try:
1✔
591
                destinations_out = []
1✔
592
                for idx in destinations:
1✔
593
                    destinations_out.append(locations[idx])
1✔
594
            except IndexError:
1✔
595
                raise IndexError(
1✔
596
                    "Parameter destinations out of locations range at index {}.".format(idx)
597
                )
598
            except TypeError:
×
599
                # Raised when destinations == None
600
                pass
×
601

602
            sources_out = (reversed([convert.format_float(f) for f in coord]) for coord in sources_out)
1✔
603
            params.extend([("from_point", ",".join(coord)) for coord in sources_out])
1✔
604

605
            destinations_out = (
1✔
606
                reversed([convert.format_float(f) for f in coord]) for coord in destinations_out
607
            )
608
            params.extend([("to_point", ",".join(coord)) for coord in destinations_out])
1✔
609

610
        if out_array is not None:
1✔
611
            for e in out_array:
1✔
612
                params.append(("out_array", e))
1✔
613

614
        if debug is not None:
1✔
615
            params.append(("debug", convert.convert_bool(debug)))
1✔
616

617
        params.extend(matrix_kwargs.items())
1✔
618

619
        return self.parse_matrix_json(
1✔
620
            self.client._request("/matrix", get_params=params, dry_run=dry_run),
621
        )
622

623
    @staticmethod
1✔
624
    def parse_matrix_json(response):
1✔
625
        if response is None:  # pragma: no cover
626
            return Matrix()
627
        durations = response.get("times")
1✔
628
        distances = response.get("distances")
1✔
629

630
        return Matrix(durations=durations, distances=distances, raw=response)
1✔
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