justrouting 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- justrouting-0.1.0/LICENSE +21 -0
- justrouting-0.1.0/PKG-INFO +281 -0
- justrouting-0.1.0/README.md +254 -0
- justrouting-0.1.0/pyproject.toml +42 -0
- justrouting-0.1.0/setup.cfg +4 -0
- justrouting-0.1.0/src/justrouting/__init__.py +129 -0
- justrouting-0.1.0/src/justrouting/client.py +139 -0
- justrouting-0.1.0/src/justrouting/consts.py +30 -0
- justrouting-0.1.0/src/justrouting/error.py +269 -0
- justrouting-0.1.0/src/justrouting/geo.py +203 -0
- justrouting-0.1.0/src/justrouting/health.py +48 -0
- justrouting-0.1.0/src/justrouting/matrix.py +185 -0
- justrouting-0.1.0/src/justrouting/optimization.py +558 -0
- justrouting-0.1.0/src/justrouting/routes.py +348 -0
- justrouting-0.1.0/src/justrouting/transport.py +258 -0
- justrouting-0.1.0/src/justrouting.egg-info/PKG-INFO +281 -0
- justrouting-0.1.0/src/justrouting.egg-info/SOURCES.txt +25 -0
- justrouting-0.1.0/src/justrouting.egg-info/dependency_links.txt +1 -0
- justrouting-0.1.0/src/justrouting.egg-info/requires.txt +5 -0
- justrouting-0.1.0/src/justrouting.egg-info/top_level.txt +1 -0
- justrouting-0.1.0/tests/test_client.py +122 -0
- justrouting-0.1.0/tests/test_geo.py +125 -0
- justrouting-0.1.0/tests/test_integration.py +139 -0
- justrouting-0.1.0/tests/test_matrix.py +147 -0
- justrouting-0.1.0/tests/test_optimization.py +204 -0
- justrouting-0.1.0/tests/test_routes.py +209 -0
- justrouting-0.1.0/tests/test_transport.py +351 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 justrouting
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: justrouting
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the JustRouting API — routing, distance matrices, and vehicle routing optimization across Southeast Asia.
|
|
5
|
+
Author: justrouting
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://justrouting.tech
|
|
8
|
+
Keywords: routing,osrm,vroom,matrix,optimization,logistics
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: GIS
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
24
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
25
|
+
Requires-Dist: twine>=5; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# JustRouting Python Client
|
|
29
|
+
|
|
30
|
+
Official Python client for the [JustRouting](https://justrouting.tech) API — routing, distance matrices, and vehicle routing optimization across Southeast Asia.
|
|
31
|
+
|
|
32
|
+
No dependencies outside the standard library. Requires Python 3.9+.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```shell
|
|
37
|
+
# (Not yet available on PyPI)
|
|
38
|
+
pip install justrouting
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> Or install from a clone of this repository:
|
|
42
|
+
|
|
43
|
+
```shell
|
|
44
|
+
pip install -e .
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import justrouting
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Quickstart
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
import justrouting
|
|
55
|
+
|
|
56
|
+
client = justrouting.Client("YOUR_API_KEY")
|
|
57
|
+
|
|
58
|
+
route = client.routes.get(
|
|
59
|
+
justrouting.RouteRequest(
|
|
60
|
+
origin=[103.8198, 1.3521],
|
|
61
|
+
destination=[101.6869, 3.1390],
|
|
62
|
+
),
|
|
63
|
+
timeout=30,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
print(f"Distance: {route.distance / 1000:.1f} km")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
> The coordinates above are Singapore and Kuala Lumpur, which span two countries. See [Coordinates must share a country](#coordinates-must-share-a-country) — the runnable examples use same-country pairs.
|
|
70
|
+
|
|
71
|
+
## Services
|
|
72
|
+
|
|
73
|
+
A `Client` exposes four services.
|
|
74
|
+
|
|
75
|
+
### Routes
|
|
76
|
+
|
|
77
|
+
`Routes.get` returns the best route. `Routes.get_all` additionally returns alternatives and the snapped input waypoints.
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
route = client.routes.get(justrouting.RouteRequest(
|
|
81
|
+
origin=[103.8198, 1.3521],
|
|
82
|
+
destination=[103.9915, 1.3644],
|
|
83
|
+
waypoints=[[103.8514, 1.2897]], # stops in order
|
|
84
|
+
overview="full", # full geometry
|
|
85
|
+
steps=True, # turn-by-turn
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
print(route.distance) # metres
|
|
89
|
+
print(route.duration) # seconds
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`route.geometry` holds whichever encoding you asked for:
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
polyline = route.geometry.polyline() # default, and "polyline6"
|
|
96
|
+
line = route.geometry.geojson() # when geometries="geojson"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Matrix
|
|
100
|
+
|
|
101
|
+
Travel time and distance between many points at once.
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
m = client.matrix.get(justrouting.MatrixRequest(
|
|
105
|
+
coordinates=[depot, stop_a, stop_b],
|
|
106
|
+
sources=[0], # only the depot row; cheaper than N×N
|
|
107
|
+
destinations=[1, 2],
|
|
108
|
+
))
|
|
109
|
+
|
|
110
|
+
seconds = m.duration(0, 1)
|
|
111
|
+
if seconds is not None:
|
|
112
|
+
print(f"depot -> stopA: {seconds / 60:.0f} min")
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The accessors return `None` for unreachable pairs. The API reports those as `null`, which is deliberately kept distinct from a genuine zero.
|
|
116
|
+
|
|
117
|
+
### Optimization
|
|
118
|
+
|
|
119
|
+
Assign tasks to a fleet and order each vehicle's stops.
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
solution = client.optimization.solve(justrouting.OptimizationRequest(
|
|
123
|
+
vehicles=[justrouting.Vehicle(
|
|
124
|
+
id=1, start=depot, end=depot, capacity=[4],
|
|
125
|
+
)],
|
|
126
|
+
jobs=[
|
|
127
|
+
justrouting.Job(id=1, location=stop_a, delivery=[1], service=300),
|
|
128
|
+
justrouting.Job(id=2, location=stop_b, delivery=[2], service=300),
|
|
129
|
+
],
|
|
130
|
+
))
|
|
131
|
+
|
|
132
|
+
for route in solution.routes:
|
|
133
|
+
print(f"vehicle {route.vehicle}: {len(route.steps)} stops")
|
|
134
|
+
print(len(solution.unassigned), "task(s) could not be served")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Use `Shipments` instead of `Jobs` for pickup-and-delivery pairs that must be served in order by the same vehicle.
|
|
138
|
+
|
|
139
|
+
### Health
|
|
140
|
+
|
|
141
|
+
The only call that works without an API key, which makes it a useful connectivity check.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
health = client.health.get()
|
|
145
|
+
print(health.ok(), health.upstreams)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Error handling
|
|
149
|
+
|
|
150
|
+
Every failure raises a subclass of `justrouting.Error`. Classify it with `except` clauses rather than matching on message text:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
try:
|
|
154
|
+
route = client.routes.get(req)
|
|
155
|
+
except justrouting.QuotaExceededError:
|
|
156
|
+
# daily allowance used up — retrying will not help
|
|
157
|
+
except justrouting.RateLimitedError:
|
|
158
|
+
# throttled; the client already retried
|
|
159
|
+
except justrouting.CrossCountryError:
|
|
160
|
+
# coordinates span more than one country
|
|
161
|
+
except justrouting.NoRouteError:
|
|
162
|
+
# no road connects these points
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
| Exception | Meaning |
|
|
166
|
+
| --- | --- |
|
|
167
|
+
| `UnauthorizedError` | API key missing, invalid, or revoked |
|
|
168
|
+
| `RateLimitedError` | Throttled (per-second limit or daily quota) |
|
|
169
|
+
| `QuotaExceededError` | Daily quota exhausted; retrying will not help (subclasses `RateLimitedError`) |
|
|
170
|
+
| `PlanLimitExceededError` | Too many matrix coordinates, jobs, or vehicles |
|
|
171
|
+
| `CrossCountryError` | Coordinates span more than one country |
|
|
172
|
+
| `InvalidCoordinatesError` | Coordinate malformed or out of range |
|
|
173
|
+
| `NoRouteError` | No route exists between the points |
|
|
174
|
+
| `UpstreamUnavailableError` | Routing engine unreachable; usually transient |
|
|
175
|
+
| `InvalidRequestError` | Rejected locally before any request was sent |
|
|
176
|
+
|
|
177
|
+
Catch `justrouting.Error` itself when you need the status code, engine code, or raw body:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
except justrouting.Error as e:
|
|
181
|
+
print(f"HTTP {e.status_code}: {e.message}")
|
|
182
|
+
print(e.body) # raw response body, truncated
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Failures that never produced an API response raise `TransportError` (network-level, retried like a 5xx) or `DecodeError` (unparseable success response) instead; neither is a subclass of `Error`.
|
|
186
|
+
|
|
187
|
+
## Configuration
|
|
188
|
+
|
|
189
|
+
| Argument | Default | Purpose |
|
|
190
|
+
| --- | --- | --- |
|
|
191
|
+
| `api_key` | — | Sent as `Authorization: Bearer` on every authenticated request |
|
|
192
|
+
| `base_url` | `https://api.justrouting.tech` | Target a local or staging server |
|
|
193
|
+
| `user_agent` | `justrouting-py/<version>` | Identify your application |
|
|
194
|
+
| `max_retries` | `2` | Retry budget on top of the initial attempt |
|
|
195
|
+
| `backoff` | 500ms → 8s, jittered | Callable replacing the retry delay schedule |
|
|
196
|
+
| `timeout` | `30` | Seconds per HTTP attempt |
|
|
197
|
+
|
|
198
|
+
Invalid options raise `ValueError` immediately. (The Go client defers them to the first request because its constructor cannot fail; Python constructors can.)
|
|
199
|
+
|
|
200
|
+
### Timeouts and retries
|
|
201
|
+
|
|
202
|
+
Rate limits (429), server errors (5xx), and transport failures are retried with exponential backoff and jitter; a `Retry-After` header takes precedence when present. Other 4xx responses are returned immediately — they would fail identically on a retry and would still consume quota.
|
|
203
|
+
|
|
204
|
+
`Client(timeout=...)` covers a single HTTP attempt, like `http.Client.Timeout` in Go. Pass the per-call `timeout` argument to bound the whole retry sequence:
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
route = client.routes.get(req, timeout=30) # 30s total, retries included
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Proxy configuration follows the standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables.
|
|
211
|
+
|
|
212
|
+
## Things to know
|
|
213
|
+
|
|
214
|
+
### Coordinates are `[longitude, latitude]`
|
|
215
|
+
|
|
216
|
+
This is the GeoJSON order, and the reverse of the "lat, lng" used by most map UIs. Swapped coordinates are usually caught locally — a longitude in the latitude slot fails the `[-90, 90]` check before a request is sent — but a swap that stays in range will silently route somewhere unexpected.
|
|
217
|
+
|
|
218
|
+
`Point` is a `list` subclass, so both of these work:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
origin=[103.8198, 1.3521]
|
|
222
|
+
origin=justrouting.Point([103.8198, 1.3521])
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Coordinates must share a country
|
|
226
|
+
|
|
227
|
+
Every coordinate in a single request must fall within one country; the API routes each request to a per-country engine. A Singapore → Kuala Lumpur request fails with `CrossCountryError`.
|
|
228
|
+
|
|
229
|
+
Supported countries: Brunei, Cambodia, Indonesia, Laos, Malaysia, Myanmar, the Philippines, Singapore, Thailand, and Vietnam.
|
|
230
|
+
|
|
231
|
+
### Plan limits
|
|
232
|
+
|
|
233
|
+
| | Free | Hobby |
|
|
234
|
+
| --- | --- | --- |
|
|
235
|
+
| Requests per day | 100 | 10,000 |
|
|
236
|
+
| Requests per second | 5 | 10 |
|
|
237
|
+
| Matrix coordinates | 100 | 500 |
|
|
238
|
+
| Jobs per optimization | 100 | 1,000 |
|
|
239
|
+
| Vehicles per optimization | 10 | 50 |
|
|
240
|
+
|
|
241
|
+
Exceeding a size limit raises `PlanLimitExceededError`; exhausting the daily allowance raises `QuotaExceededError`.
|
|
242
|
+
|
|
243
|
+
## Examples
|
|
244
|
+
|
|
245
|
+
Runnable scripts live in [`examples/`](./examples):
|
|
246
|
+
|
|
247
|
+
```shell
|
|
248
|
+
export JUSTROUTING_API_KEY=<your key>
|
|
249
|
+
python examples/route.py
|
|
250
|
+
python examples/matrix.py
|
|
251
|
+
python examples/optimization.py
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
```shell
|
|
257
|
+
pip install -e '.[dev]'
|
|
258
|
+
pytest
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
The default suite runs entirely against local `http.server` instances — no network access and no API key.
|
|
262
|
+
|
|
263
|
+
Integration tests hit a live API and are behind a marker, so they never run by accident:
|
|
264
|
+
|
|
265
|
+
```shell
|
|
266
|
+
pytest -m integration # health and auth only
|
|
267
|
+
JUSTROUTING_API_KEY=<key> pytest -m integration
|
|
268
|
+
JUSTROUTING_BASE_URL=http://localhost:8080 pytest -m integration
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Differences from the Go client
|
|
272
|
+
|
|
273
|
+
* Exceptions replace sentinel errors and `errors.Is`; `QuotaExceededError` subclasses `RateLimitedError`.
|
|
274
|
+
* Constructor options are keyword arguments; invalid ones raise `ValueError` immediately instead of being deferred to the first request.
|
|
275
|
+
* There is no `context.Context`; the per-call `timeout` argument plays the role of a context deadline.
|
|
276
|
+
* Matrix accessors return `float | None` instead of `(float, ok)`.
|
|
277
|
+
* There is no custom-HTTP-client option; use `base_url`, `timeout`, and the standard proxy environment variables instead.
|
|
278
|
+
|
|
279
|
+
## License
|
|
280
|
+
|
|
281
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# JustRouting Python Client
|
|
2
|
+
|
|
3
|
+
Official Python client for the [JustRouting](https://justrouting.tech) API — routing, distance matrices, and vehicle routing optimization across Southeast Asia.
|
|
4
|
+
|
|
5
|
+
No dependencies outside the standard library. Requires Python 3.9+.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```shell
|
|
10
|
+
# (Not yet available on PyPI)
|
|
11
|
+
pip install justrouting
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
> Or install from a clone of this repository:
|
|
15
|
+
|
|
16
|
+
```shell
|
|
17
|
+
pip install -e .
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
import justrouting
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quickstart
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import justrouting
|
|
28
|
+
|
|
29
|
+
client = justrouting.Client("YOUR_API_KEY")
|
|
30
|
+
|
|
31
|
+
route = client.routes.get(
|
|
32
|
+
justrouting.RouteRequest(
|
|
33
|
+
origin=[103.8198, 1.3521],
|
|
34
|
+
destination=[101.6869, 3.1390],
|
|
35
|
+
),
|
|
36
|
+
timeout=30,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
print(f"Distance: {route.distance / 1000:.1f} km")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
> The coordinates above are Singapore and Kuala Lumpur, which span two countries. See [Coordinates must share a country](#coordinates-must-share-a-country) — the runnable examples use same-country pairs.
|
|
43
|
+
|
|
44
|
+
## Services
|
|
45
|
+
|
|
46
|
+
A `Client` exposes four services.
|
|
47
|
+
|
|
48
|
+
### Routes
|
|
49
|
+
|
|
50
|
+
`Routes.get` returns the best route. `Routes.get_all` additionally returns alternatives and the snapped input waypoints.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
route = client.routes.get(justrouting.RouteRequest(
|
|
54
|
+
origin=[103.8198, 1.3521],
|
|
55
|
+
destination=[103.9915, 1.3644],
|
|
56
|
+
waypoints=[[103.8514, 1.2897]], # stops in order
|
|
57
|
+
overview="full", # full geometry
|
|
58
|
+
steps=True, # turn-by-turn
|
|
59
|
+
))
|
|
60
|
+
|
|
61
|
+
print(route.distance) # metres
|
|
62
|
+
print(route.duration) # seconds
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`route.geometry` holds whichever encoding you asked for:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
polyline = route.geometry.polyline() # default, and "polyline6"
|
|
69
|
+
line = route.geometry.geojson() # when geometries="geojson"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Matrix
|
|
73
|
+
|
|
74
|
+
Travel time and distance between many points at once.
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
m = client.matrix.get(justrouting.MatrixRequest(
|
|
78
|
+
coordinates=[depot, stop_a, stop_b],
|
|
79
|
+
sources=[0], # only the depot row; cheaper than N×N
|
|
80
|
+
destinations=[1, 2],
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
seconds = m.duration(0, 1)
|
|
84
|
+
if seconds is not None:
|
|
85
|
+
print(f"depot -> stopA: {seconds / 60:.0f} min")
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The accessors return `None` for unreachable pairs. The API reports those as `null`, which is deliberately kept distinct from a genuine zero.
|
|
89
|
+
|
|
90
|
+
### Optimization
|
|
91
|
+
|
|
92
|
+
Assign tasks to a fleet and order each vehicle's stops.
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
solution = client.optimization.solve(justrouting.OptimizationRequest(
|
|
96
|
+
vehicles=[justrouting.Vehicle(
|
|
97
|
+
id=1, start=depot, end=depot, capacity=[4],
|
|
98
|
+
)],
|
|
99
|
+
jobs=[
|
|
100
|
+
justrouting.Job(id=1, location=stop_a, delivery=[1], service=300),
|
|
101
|
+
justrouting.Job(id=2, location=stop_b, delivery=[2], service=300),
|
|
102
|
+
],
|
|
103
|
+
))
|
|
104
|
+
|
|
105
|
+
for route in solution.routes:
|
|
106
|
+
print(f"vehicle {route.vehicle}: {len(route.steps)} stops")
|
|
107
|
+
print(len(solution.unassigned), "task(s) could not be served")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Use `Shipments` instead of `Jobs` for pickup-and-delivery pairs that must be served in order by the same vehicle.
|
|
111
|
+
|
|
112
|
+
### Health
|
|
113
|
+
|
|
114
|
+
The only call that works without an API key, which makes it a useful connectivity check.
|
|
115
|
+
|
|
116
|
+
```python
|
|
117
|
+
health = client.health.get()
|
|
118
|
+
print(health.ok(), health.upstreams)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Error handling
|
|
122
|
+
|
|
123
|
+
Every failure raises a subclass of `justrouting.Error`. Classify it with `except` clauses rather than matching on message text:
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
try:
|
|
127
|
+
route = client.routes.get(req)
|
|
128
|
+
except justrouting.QuotaExceededError:
|
|
129
|
+
# daily allowance used up — retrying will not help
|
|
130
|
+
except justrouting.RateLimitedError:
|
|
131
|
+
# throttled; the client already retried
|
|
132
|
+
except justrouting.CrossCountryError:
|
|
133
|
+
# coordinates span more than one country
|
|
134
|
+
except justrouting.NoRouteError:
|
|
135
|
+
# no road connects these points
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
| Exception | Meaning |
|
|
139
|
+
| --- | --- |
|
|
140
|
+
| `UnauthorizedError` | API key missing, invalid, or revoked |
|
|
141
|
+
| `RateLimitedError` | Throttled (per-second limit or daily quota) |
|
|
142
|
+
| `QuotaExceededError` | Daily quota exhausted; retrying will not help (subclasses `RateLimitedError`) |
|
|
143
|
+
| `PlanLimitExceededError` | Too many matrix coordinates, jobs, or vehicles |
|
|
144
|
+
| `CrossCountryError` | Coordinates span more than one country |
|
|
145
|
+
| `InvalidCoordinatesError` | Coordinate malformed or out of range |
|
|
146
|
+
| `NoRouteError` | No route exists between the points |
|
|
147
|
+
| `UpstreamUnavailableError` | Routing engine unreachable; usually transient |
|
|
148
|
+
| `InvalidRequestError` | Rejected locally before any request was sent |
|
|
149
|
+
|
|
150
|
+
Catch `justrouting.Error` itself when you need the status code, engine code, or raw body:
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
except justrouting.Error as e:
|
|
154
|
+
print(f"HTTP {e.status_code}: {e.message}")
|
|
155
|
+
print(e.body) # raw response body, truncated
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Failures that never produced an API response raise `TransportError` (network-level, retried like a 5xx) or `DecodeError` (unparseable success response) instead; neither is a subclass of `Error`.
|
|
159
|
+
|
|
160
|
+
## Configuration
|
|
161
|
+
|
|
162
|
+
| Argument | Default | Purpose |
|
|
163
|
+
| --- | --- | --- |
|
|
164
|
+
| `api_key` | — | Sent as `Authorization: Bearer` on every authenticated request |
|
|
165
|
+
| `base_url` | `https://api.justrouting.tech` | Target a local or staging server |
|
|
166
|
+
| `user_agent` | `justrouting-py/<version>` | Identify your application |
|
|
167
|
+
| `max_retries` | `2` | Retry budget on top of the initial attempt |
|
|
168
|
+
| `backoff` | 500ms → 8s, jittered | Callable replacing the retry delay schedule |
|
|
169
|
+
| `timeout` | `30` | Seconds per HTTP attempt |
|
|
170
|
+
|
|
171
|
+
Invalid options raise `ValueError` immediately. (The Go client defers them to the first request because its constructor cannot fail; Python constructors can.)
|
|
172
|
+
|
|
173
|
+
### Timeouts and retries
|
|
174
|
+
|
|
175
|
+
Rate limits (429), server errors (5xx), and transport failures are retried with exponential backoff and jitter; a `Retry-After` header takes precedence when present. Other 4xx responses are returned immediately — they would fail identically on a retry and would still consume quota.
|
|
176
|
+
|
|
177
|
+
`Client(timeout=...)` covers a single HTTP attempt, like `http.Client.Timeout` in Go. Pass the per-call `timeout` argument to bound the whole retry sequence:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
route = client.routes.get(req, timeout=30) # 30s total, retries included
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Proxy configuration follows the standard `HTTP_PROXY` / `HTTPS_PROXY` environment variables.
|
|
184
|
+
|
|
185
|
+
## Things to know
|
|
186
|
+
|
|
187
|
+
### Coordinates are `[longitude, latitude]`
|
|
188
|
+
|
|
189
|
+
This is the GeoJSON order, and the reverse of the "lat, lng" used by most map UIs. Swapped coordinates are usually caught locally — a longitude in the latitude slot fails the `[-90, 90]` check before a request is sent — but a swap that stays in range will silently route somewhere unexpected.
|
|
190
|
+
|
|
191
|
+
`Point` is a `list` subclass, so both of these work:
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
origin=[103.8198, 1.3521]
|
|
195
|
+
origin=justrouting.Point([103.8198, 1.3521])
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Coordinates must share a country
|
|
199
|
+
|
|
200
|
+
Every coordinate in a single request must fall within one country; the API routes each request to a per-country engine. A Singapore → Kuala Lumpur request fails with `CrossCountryError`.
|
|
201
|
+
|
|
202
|
+
Supported countries: Brunei, Cambodia, Indonesia, Laos, Malaysia, Myanmar, the Philippines, Singapore, Thailand, and Vietnam.
|
|
203
|
+
|
|
204
|
+
### Plan limits
|
|
205
|
+
|
|
206
|
+
| | Free | Hobby |
|
|
207
|
+
| --- | --- | --- |
|
|
208
|
+
| Requests per day | 100 | 10,000 |
|
|
209
|
+
| Requests per second | 5 | 10 |
|
|
210
|
+
| Matrix coordinates | 100 | 500 |
|
|
211
|
+
| Jobs per optimization | 100 | 1,000 |
|
|
212
|
+
| Vehicles per optimization | 10 | 50 |
|
|
213
|
+
|
|
214
|
+
Exceeding a size limit raises `PlanLimitExceededError`; exhausting the daily allowance raises `QuotaExceededError`.
|
|
215
|
+
|
|
216
|
+
## Examples
|
|
217
|
+
|
|
218
|
+
Runnable scripts live in [`examples/`](./examples):
|
|
219
|
+
|
|
220
|
+
```shell
|
|
221
|
+
export JUSTROUTING_API_KEY=<your key>
|
|
222
|
+
python examples/route.py
|
|
223
|
+
python examples/matrix.py
|
|
224
|
+
python examples/optimization.py
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Development
|
|
228
|
+
|
|
229
|
+
```shell
|
|
230
|
+
pip install -e '.[dev]'
|
|
231
|
+
pytest
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The default suite runs entirely against local `http.server` instances — no network access and no API key.
|
|
235
|
+
|
|
236
|
+
Integration tests hit a live API and are behind a marker, so they never run by accident:
|
|
237
|
+
|
|
238
|
+
```shell
|
|
239
|
+
pytest -m integration # health and auth only
|
|
240
|
+
JUSTROUTING_API_KEY=<key> pytest -m integration
|
|
241
|
+
JUSTROUTING_BASE_URL=http://localhost:8080 pytest -m integration
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Differences from the Go client
|
|
245
|
+
|
|
246
|
+
* Exceptions replace sentinel errors and `errors.Is`; `QuotaExceededError` subclasses `RateLimitedError`.
|
|
247
|
+
* Constructor options are keyword arguments; invalid ones raise `ValueError` immediately instead of being deferred to the first request.
|
|
248
|
+
* There is no `context.Context`; the per-call `timeout` argument plays the role of a context deadline.
|
|
249
|
+
* Matrix accessors return `float | None` instead of `(float, ok)`.
|
|
250
|
+
* There is no custom-HTTP-client option; use `base_url`, `timeout`, and the standard proxy environment variables instead.
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
[MIT](./LICENSE)
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "justrouting"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python client for the JustRouting API — routing, distance matrices, and vehicle routing optimization across Southeast Asia."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{ name = "justrouting" }]
|
|
12
|
+
keywords = ["routing", "osrm", "vroom", "matrix", "optimization", "logistics"]
|
|
13
|
+
requires-python = ">=3.9"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Topic :: Scientific/Engineering :: GIS",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://justrouting.tech"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest>=7", "build>=1.2", "twine>=5"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.packages.find]
|
|
34
|
+
where = ["src"]
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
pythonpath = ["src"]
|
|
39
|
+
addopts = "-m 'not integration'"
|
|
40
|
+
markers = [
|
|
41
|
+
"integration: tests that hit the live JustRouting API (excluded by default)",
|
|
42
|
+
]
|