geocodio-library-python 1.4.0__tar.gz → 1.6.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.
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/CHANGELOG.md +12 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/PKG-INFO +78 -3
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/README.md +77 -2
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/pyproject.toml +1 -1
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/src/geocodio/__init__.py +5 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/src/geocodio/_version.py +1 -1
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/src/geocodio/client.py +43 -4
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/src/geocodio/distance.py +10 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/src/geocodio/exceptions.py +21 -3
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/src/geocodio/models.py +35 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/test_distance.py +85 -0
- geocodio_library_python-1.6.0/tests/unit/test_warnings.py +364 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/uv.lock +2 -2
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/.github/workflows/ci.yml +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/.github/workflows/publish.yml +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/.github/workflows/test-act-event-ci.json +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/.github/workflows/test-act-event-publish.json +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/.gitignore +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/LICENSE +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/requirements-dev.txt +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/setup.cfg +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/smoke.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/smoke_lists.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/test_pypi_release.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/__init__.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/conftest.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/e2e/__init__.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/e2e/test_api.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/e2e/test_batch_reverse.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/e2e/test_distance.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/e2e/test_lists_api.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/test_workflows.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/__init__.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/test_client.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/test_errors.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/test_exceptions.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/test_geocode.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/test_geocoding.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/test_models.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/test_reverse.py +0 -0
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.6.0] - 2026-10-12
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `calculation_type` parameter on `distance_matrix()` and `create_distance_matrix_job()`, with new `DISTANCE_CALCULATION_TYPE_MATRIX` and `DISTANCE_CALCULATION_TYPE_PAIRS` constants. `"pairs"` measures origin i against destination i only, instead of every origin against every destination.
|
|
14
|
+
- `DistanceJobResponse.calculation_type`, read from job create, status and list responses (`None` when the API does not send it).
|
|
15
|
+
|
|
16
|
+
## [1.5.0] - 2026-10-06
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Warnings support: the API's `_warnings` response key is now parsed into a `warnings` list on `GeocodingResponse`, `GeocodingResult`, `ListResponse`, `DistanceJobResponse` and `PaginatedResponse` (empty when the API sent none). Previously the key was only reachable through `GeocodingResponse.raw`, and was dropped entirely for list and distance matrix job responses. For batch requests, `GeocodingResult.warnings` includes the warnings attached to that query and `GeocodingResponse.warnings` holds the de-duplicated set across all queries.
|
|
20
|
+
- `GeocodioError.warnings` (and `GeocodioErrorDetail.warnings`), exposing warnings attached to error responses.
|
|
21
|
+
|
|
10
22
|
## [1.4.0] - 2026-08-28
|
|
11
23
|
|
|
12
24
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: geocodio-library-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
4
4
|
Summary: A Python client for the Geocodio API
|
|
5
5
|
Project-URL: Homepage, https://www.geocod.io
|
|
6
6
|
Project-URL: Documentation, https://www.geocod.io/docs/?python
|
|
@@ -207,6 +207,49 @@ response.rate_limit.reset # unix timestamp, when sent by the API
|
|
|
207
207
|
client.rate_limit # the most recent rate limit state seen
|
|
208
208
|
```
|
|
209
209
|
|
|
210
|
+
### Warnings
|
|
211
|
+
|
|
212
|
+
The API reports non-fatal advisories under a `_warnings` key — a misspelled
|
|
213
|
+
field name, an unexpected query parameter, a superseded API version, or an
|
|
214
|
+
append that had to be skipped. The request still succeeds, so nothing is raised
|
|
215
|
+
or logged; the warnings are parsed onto the response as a `warnings` list, which
|
|
216
|
+
is empty when the API sent none:
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
response = client.geocode("1109 N Highland St, Arlington VA", fields=["congress"])
|
|
220
|
+
|
|
221
|
+
for warning in response.warnings:
|
|
222
|
+
print(warning)
|
|
223
|
+
# "The field congress is not recognized. Did you mean cd?"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Warnings show up in a few places, depending on what raised them:
|
|
227
|
+
|
|
228
|
+
| Where | Applies to |
|
|
229
|
+
| -- | -- |
|
|
230
|
+
| `response.warnings` | Single `geocode()` and `reverse()`. For batch requests, the de-duplicated warnings from every query |
|
|
231
|
+
| `response.results[i].warnings` | An individual result, e.g. an `ffiec` append skipped because the match is not street-level. For batch requests, this also includes the warnings attached to that query |
|
|
232
|
+
| `list_response.warnings` | `create_list()`, `get_list()` and `get_lists()` |
|
|
233
|
+
| `job.warnings` | `create_distance_matrix_job()`, `distance_matrix_job_status()` and `distance_matrix_jobs()` |
|
|
234
|
+
|
|
235
|
+
Warnings are also attached to error responses, where they are available on the
|
|
236
|
+
exception:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
from geocodio.exceptions import GeocodioError
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
response = client.geocode("1109 N Highland St", fields=["congress"])
|
|
243
|
+
except GeocodioError as e:
|
|
244
|
+
for warning in e.warnings:
|
|
245
|
+
print(warning) # "The field congress is not recognized. Did you mean cd?"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
> [!TIP]
|
|
249
|
+
> Warnings are worth logging during development — they are how the API tells
|
|
250
|
+
> you a field append was silently skipped, which otherwise looks like missing
|
|
251
|
+
> data.
|
|
252
|
+
|
|
210
253
|
### Address components
|
|
211
254
|
|
|
212
255
|
For forward geocoding requests it is possible to supply [individual address components](https://www.geocod.io/docs/#single-address) instead of a full address string:
|
|
@@ -297,6 +340,8 @@ from geocodio import (
|
|
|
297
340
|
DISTANCE_ORDER_BY_DURATION,
|
|
298
341
|
DISTANCE_SORT_ASC, # Default
|
|
299
342
|
DISTANCE_SORT_DESC,
|
|
343
|
+
DISTANCE_CALCULATION_TYPE_MATRIX, # Default - every origin × every destination (distance matrix only)
|
|
344
|
+
DISTANCE_CALCULATION_TYPE_PAIRS, # Origin i to destination i only (distance matrix only)
|
|
300
345
|
)
|
|
301
346
|
```
|
|
302
347
|
|
|
@@ -423,7 +468,13 @@ response = client.distance(
|
|
|
423
468
|
#### Distance matrix (multiple origins × destinations)
|
|
424
469
|
|
|
425
470
|
```python
|
|
426
|
-
from geocodio import
|
|
471
|
+
from geocodio import (
|
|
472
|
+
Geocodio,
|
|
473
|
+
Coordinate,
|
|
474
|
+
DISTANCE_MODE_DRIVING,
|
|
475
|
+
DISTANCE_UNITS_KM,
|
|
476
|
+
DISTANCE_CALCULATION_TYPE_PAIRS,
|
|
477
|
+
)
|
|
427
478
|
|
|
428
479
|
client = Geocodio("YOUR_API_KEY")
|
|
429
480
|
|
|
@@ -462,8 +513,18 @@ destinations = [
|
|
|
462
513
|
Coordinate(37.7949, -122.3994, "customer_2")
|
|
463
514
|
]
|
|
464
515
|
response = client.distance_matrix(origins=origins, destinations=destinations)
|
|
516
|
+
|
|
517
|
+
# One-to-one pairs: origin i is measured against destination i only,
|
|
518
|
+
# so each result has a single destination
|
|
519
|
+
response = client.distance_matrix(
|
|
520
|
+
origins=["38.8977,-77.0365,home", "38.886672,-77.094735,office"],
|
|
521
|
+
destinations=["38.9072,-77.0369,capitol", "38.8814,-77.0916,pentagon"],
|
|
522
|
+
calculation_type=DISTANCE_CALCULATION_TYPE_PAIRS
|
|
523
|
+
)
|
|
465
524
|
```
|
|
466
525
|
|
|
526
|
+
> **Note:** With `DISTANCE_CALCULATION_TYPE_PAIRS`, `origins` and `destinations` must be the same length, and the `max_results`, `max_distance`, `max_duration`, `min_distance` and `min_duration` filters are not supported. The API returns a 422 error otherwise.
|
|
527
|
+
|
|
467
528
|
#### Nearest mode (find closest destinations)
|
|
468
529
|
|
|
469
530
|
```python
|
|
@@ -514,7 +575,12 @@ response = client.distance_matrix(
|
|
|
514
575
|
For large distance matrix calculations, use async jobs that process in the background.
|
|
515
576
|
|
|
516
577
|
```python
|
|
517
|
-
from geocodio import
|
|
578
|
+
from geocodio import (
|
|
579
|
+
Geocodio,
|
|
580
|
+
DISTANCE_MODE_DRIVING,
|
|
581
|
+
DISTANCE_UNITS_MILES,
|
|
582
|
+
DISTANCE_CALCULATION_TYPE_PAIRS,
|
|
583
|
+
)
|
|
518
584
|
|
|
519
585
|
client = Geocodio("YOUR_API_KEY")
|
|
520
586
|
|
|
@@ -540,6 +606,15 @@ job = client.create_distance_matrix_job(
|
|
|
540
606
|
mode=DISTANCE_MODE_STRAIGHTLINE
|
|
541
607
|
)
|
|
542
608
|
|
|
609
|
+
# One-to-one pairs instead of a full matrix
|
|
610
|
+
job = client.create_distance_matrix_job(
|
|
611
|
+
name="Commutes",
|
|
612
|
+
origins=["38.8977,-77.0365,home", "38.886672,-77.094735,office"],
|
|
613
|
+
destinations=["38.9072,-77.0369,capitol", "38.8814,-77.0916,pentagon"],
|
|
614
|
+
calculation_type=DISTANCE_CALCULATION_TYPE_PAIRS
|
|
615
|
+
)
|
|
616
|
+
print(job.calculation_type) # "pairs"
|
|
617
|
+
|
|
543
618
|
# Check job status
|
|
544
619
|
status = client.distance_matrix_job_status(job.id)
|
|
545
620
|
print(status.status) # "ENQUEUED", "PROCESSING", "COMPLETED", or "FAILED"
|
|
@@ -172,6 +172,49 @@ response.rate_limit.reset # unix timestamp, when sent by the API
|
|
|
172
172
|
client.rate_limit # the most recent rate limit state seen
|
|
173
173
|
```
|
|
174
174
|
|
|
175
|
+
### Warnings
|
|
176
|
+
|
|
177
|
+
The API reports non-fatal advisories under a `_warnings` key — a misspelled
|
|
178
|
+
field name, an unexpected query parameter, a superseded API version, or an
|
|
179
|
+
append that had to be skipped. The request still succeeds, so nothing is raised
|
|
180
|
+
or logged; the warnings are parsed onto the response as a `warnings` list, which
|
|
181
|
+
is empty when the API sent none:
|
|
182
|
+
|
|
183
|
+
```python
|
|
184
|
+
response = client.geocode("1109 N Highland St, Arlington VA", fields=["congress"])
|
|
185
|
+
|
|
186
|
+
for warning in response.warnings:
|
|
187
|
+
print(warning)
|
|
188
|
+
# "The field congress is not recognized. Did you mean cd?"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Warnings show up in a few places, depending on what raised them:
|
|
192
|
+
|
|
193
|
+
| Where | Applies to |
|
|
194
|
+
| -- | -- |
|
|
195
|
+
| `response.warnings` | Single `geocode()` and `reverse()`. For batch requests, the de-duplicated warnings from every query |
|
|
196
|
+
| `response.results[i].warnings` | An individual result, e.g. an `ffiec` append skipped because the match is not street-level. For batch requests, this also includes the warnings attached to that query |
|
|
197
|
+
| `list_response.warnings` | `create_list()`, `get_list()` and `get_lists()` |
|
|
198
|
+
| `job.warnings` | `create_distance_matrix_job()`, `distance_matrix_job_status()` and `distance_matrix_jobs()` |
|
|
199
|
+
|
|
200
|
+
Warnings are also attached to error responses, where they are available on the
|
|
201
|
+
exception:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from geocodio.exceptions import GeocodioError
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
response = client.geocode("1109 N Highland St", fields=["congress"])
|
|
208
|
+
except GeocodioError as e:
|
|
209
|
+
for warning in e.warnings:
|
|
210
|
+
print(warning) # "The field congress is not recognized. Did you mean cd?"
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
> [!TIP]
|
|
214
|
+
> Warnings are worth logging during development — they are how the API tells
|
|
215
|
+
> you a field append was silently skipped, which otherwise looks like missing
|
|
216
|
+
> data.
|
|
217
|
+
|
|
175
218
|
### Address components
|
|
176
219
|
|
|
177
220
|
For forward geocoding requests it is possible to supply [individual address components](https://www.geocod.io/docs/#single-address) instead of a full address string:
|
|
@@ -262,6 +305,8 @@ from geocodio import (
|
|
|
262
305
|
DISTANCE_ORDER_BY_DURATION,
|
|
263
306
|
DISTANCE_SORT_ASC, # Default
|
|
264
307
|
DISTANCE_SORT_DESC,
|
|
308
|
+
DISTANCE_CALCULATION_TYPE_MATRIX, # Default - every origin × every destination (distance matrix only)
|
|
309
|
+
DISTANCE_CALCULATION_TYPE_PAIRS, # Origin i to destination i only (distance matrix only)
|
|
265
310
|
)
|
|
266
311
|
```
|
|
267
312
|
|
|
@@ -388,7 +433,13 @@ response = client.distance(
|
|
|
388
433
|
#### Distance matrix (multiple origins × destinations)
|
|
389
434
|
|
|
390
435
|
```python
|
|
391
|
-
from geocodio import
|
|
436
|
+
from geocodio import (
|
|
437
|
+
Geocodio,
|
|
438
|
+
Coordinate,
|
|
439
|
+
DISTANCE_MODE_DRIVING,
|
|
440
|
+
DISTANCE_UNITS_KM,
|
|
441
|
+
DISTANCE_CALCULATION_TYPE_PAIRS,
|
|
442
|
+
)
|
|
392
443
|
|
|
393
444
|
client = Geocodio("YOUR_API_KEY")
|
|
394
445
|
|
|
@@ -427,8 +478,18 @@ destinations = [
|
|
|
427
478
|
Coordinate(37.7949, -122.3994, "customer_2")
|
|
428
479
|
]
|
|
429
480
|
response = client.distance_matrix(origins=origins, destinations=destinations)
|
|
481
|
+
|
|
482
|
+
# One-to-one pairs: origin i is measured against destination i only,
|
|
483
|
+
# so each result has a single destination
|
|
484
|
+
response = client.distance_matrix(
|
|
485
|
+
origins=["38.8977,-77.0365,home", "38.886672,-77.094735,office"],
|
|
486
|
+
destinations=["38.9072,-77.0369,capitol", "38.8814,-77.0916,pentagon"],
|
|
487
|
+
calculation_type=DISTANCE_CALCULATION_TYPE_PAIRS
|
|
488
|
+
)
|
|
430
489
|
```
|
|
431
490
|
|
|
491
|
+
> **Note:** With `DISTANCE_CALCULATION_TYPE_PAIRS`, `origins` and `destinations` must be the same length, and the `max_results`, `max_distance`, `max_duration`, `min_distance` and `min_duration` filters are not supported. The API returns a 422 error otherwise.
|
|
492
|
+
|
|
432
493
|
#### Nearest mode (find closest destinations)
|
|
433
494
|
|
|
434
495
|
```python
|
|
@@ -479,7 +540,12 @@ response = client.distance_matrix(
|
|
|
479
540
|
For large distance matrix calculations, use async jobs that process in the background.
|
|
480
541
|
|
|
481
542
|
```python
|
|
482
|
-
from geocodio import
|
|
543
|
+
from geocodio import (
|
|
544
|
+
Geocodio,
|
|
545
|
+
DISTANCE_MODE_DRIVING,
|
|
546
|
+
DISTANCE_UNITS_MILES,
|
|
547
|
+
DISTANCE_CALCULATION_TYPE_PAIRS,
|
|
548
|
+
)
|
|
483
549
|
|
|
484
550
|
client = Geocodio("YOUR_API_KEY")
|
|
485
551
|
|
|
@@ -505,6 +571,15 @@ job = client.create_distance_matrix_job(
|
|
|
505
571
|
mode=DISTANCE_MODE_STRAIGHTLINE
|
|
506
572
|
)
|
|
507
573
|
|
|
574
|
+
# One-to-one pairs instead of a full matrix
|
|
575
|
+
job = client.create_distance_matrix_job(
|
|
576
|
+
name="Commutes",
|
|
577
|
+
origins=["38.8977,-77.0365,home", "38.886672,-77.094735,office"],
|
|
578
|
+
destinations=["38.9072,-77.0369,capitol", "38.8814,-77.0916,pentagon"],
|
|
579
|
+
calculation_type=DISTANCE_CALCULATION_TYPE_PAIRS
|
|
580
|
+
)
|
|
581
|
+
print(job.calculation_type) # "pairs"
|
|
582
|
+
|
|
508
583
|
# Check job status
|
|
509
584
|
status = client.distance_matrix_job_status(job.id)
|
|
510
585
|
print(status.status) # "ENQUEUED", "PROCESSING", "COMPLETED", or "FAILED"
|
|
@@ -8,6 +8,8 @@ from .client import Geocodio
|
|
|
8
8
|
|
|
9
9
|
# Distance API exports
|
|
10
10
|
from .distance import (
|
|
11
|
+
DISTANCE_CALCULATION_TYPE_MATRIX,
|
|
12
|
+
DISTANCE_CALCULATION_TYPE_PAIRS,
|
|
11
13
|
DISTANCE_MODE_DRIVING,
|
|
12
14
|
DISTANCE_MODE_HAVERSINE,
|
|
13
15
|
DISTANCE_MODE_STRAIGHTLINE,
|
|
@@ -52,4 +54,7 @@ __all__ = [
|
|
|
52
54
|
# Distance sort constants
|
|
53
55
|
"DISTANCE_SORT_ASC",
|
|
54
56
|
"DISTANCE_SORT_DESC",
|
|
57
|
+
# Distance calculation type constants
|
|
58
|
+
"DISTANCE_CALCULATION_TYPE_MATRIX",
|
|
59
|
+
"DISTANCE_CALCULATION_TYPE_PAIRS",
|
|
55
60
|
]
|
|
@@ -66,6 +66,7 @@ from geocodio.models import (
|
|
|
66
66
|
Timezone,
|
|
67
67
|
UKLegislativeDistrict,
|
|
68
68
|
ZIP4Data,
|
|
69
|
+
parse_warnings,
|
|
69
70
|
)
|
|
70
71
|
|
|
71
72
|
|
|
@@ -370,12 +371,19 @@ class Geocodio:
|
|
|
370
371
|
exception_mappings = self.get_status_exception_mappings()
|
|
371
372
|
# dump the type and content of the exception mappings for debugging
|
|
372
373
|
logger.error(f"Error response: {resp.status_code} - {resp.text}")
|
|
374
|
+
|
|
375
|
+
try:
|
|
376
|
+
warnings = parse_warnings(resp.json())
|
|
377
|
+
except ValueError:
|
|
378
|
+
warnings = []
|
|
379
|
+
|
|
373
380
|
if resp.status_code in exception_mappings:
|
|
374
381
|
exception_class = exception_mappings[resp.status_code]
|
|
375
|
-
raise exception_class(resp.text)
|
|
382
|
+
raise exception_class(resp.text, warnings=warnings)
|
|
376
383
|
else:
|
|
377
384
|
raise GeocodioServerError(
|
|
378
|
-
f"Unrecognized status code {resp.status_code}: {resp.text}"
|
|
385
|
+
f"Unrecognized status code {resp.status_code}: {resp.text}",
|
|
386
|
+
warnings=warnings,
|
|
379
387
|
)
|
|
380
388
|
|
|
381
389
|
def _parse_geocoding_response(
|
|
@@ -395,9 +403,14 @@ class Geocodio:
|
|
|
395
403
|
and "response" in response_json["results"][0]
|
|
396
404
|
):
|
|
397
405
|
results = []
|
|
406
|
+
batch_warnings: List[str] = parse_warnings(response_json)
|
|
398
407
|
for res in response_json["results"]:
|
|
399
408
|
query = res.get("query", "")
|
|
400
409
|
matches = res.get("response", {}).get("results") or []
|
|
410
|
+
query_warnings = parse_warnings(res.get("response"))
|
|
411
|
+
batch_warnings.extend(
|
|
412
|
+
w for w in query_warnings if w not in batch_warnings
|
|
413
|
+
)
|
|
401
414
|
|
|
402
415
|
# Unmatched query (e.g. an unparseable address): keep an entry
|
|
403
416
|
# so the result list stays aligned with the submitted addresses,
|
|
@@ -412,6 +425,7 @@ class Geocodio:
|
|
|
412
425
|
accuracy_type="",
|
|
413
426
|
source="",
|
|
414
427
|
query=query,
|
|
428
|
+
warnings=query_warnings,
|
|
415
429
|
)
|
|
416
430
|
)
|
|
417
431
|
continue
|
|
@@ -433,10 +447,14 @@ class Geocodio:
|
|
|
433
447
|
match_type=top.get("match_type"),
|
|
434
448
|
address_lines=top.get("address_lines"),
|
|
435
449
|
raw=top,
|
|
450
|
+
warnings=query_warnings + parse_warnings(top),
|
|
436
451
|
)
|
|
437
452
|
)
|
|
438
453
|
return GeocodingResponse(
|
|
439
|
-
results=results,
|
|
454
|
+
results=results,
|
|
455
|
+
raw=response_json,
|
|
456
|
+
rate_limit=rate_limit,
|
|
457
|
+
warnings=batch_warnings,
|
|
440
458
|
)
|
|
441
459
|
|
|
442
460
|
# Handle single response format
|
|
@@ -455,11 +473,15 @@ class Geocodio:
|
|
|
455
473
|
match_type=res.get("match_type"),
|
|
456
474
|
address_lines=res.get("address_lines"),
|
|
457
475
|
raw=res,
|
|
476
|
+
warnings=parse_warnings(res),
|
|
458
477
|
)
|
|
459
478
|
for res in response_json.get("results", [])
|
|
460
479
|
]
|
|
461
480
|
return GeocodingResponse(
|
|
462
|
-
results=results,
|
|
481
|
+
results=results,
|
|
482
|
+
raw=response_json,
|
|
483
|
+
rate_limit=rate_limit,
|
|
484
|
+
warnings=parse_warnings(response_json),
|
|
463
485
|
)
|
|
464
486
|
|
|
465
487
|
# ──────────────────────────────────────────────────────────────────────────
|
|
@@ -566,6 +588,7 @@ class Geocodio:
|
|
|
566
588
|
first_page_url=pagination_info.get("first_page_url"),
|
|
567
589
|
next_page_url=pagination_info.get("next_page_url"),
|
|
568
590
|
prev_page_url=pagination_info.get("prev_page_url"),
|
|
591
|
+
warnings=parse_warnings(pagination_info),
|
|
569
592
|
)
|
|
570
593
|
|
|
571
594
|
def get_list(self, list_id: str) -> ListResponse:
|
|
@@ -617,6 +640,7 @@ class Geocodio:
|
|
|
617
640
|
download_url=response_json.get("download_url"),
|
|
618
641
|
expires_at=response_json.get("expires_at"),
|
|
619
642
|
http_response=response,
|
|
643
|
+
warnings=parse_warnings(response_json),
|
|
620
644
|
)
|
|
621
645
|
|
|
622
646
|
@staticmethod
|
|
@@ -1144,6 +1168,7 @@ class Geocodio:
|
|
|
1144
1168
|
min_duration: Optional[int] = None,
|
|
1145
1169
|
order_by: str = DISTANCE_ORDER_BY_DISTANCE,
|
|
1146
1170
|
sort_order: str = DISTANCE_SORT_ASC,
|
|
1171
|
+
calculation_type: Optional[str] = None,
|
|
1147
1172
|
) -> DistanceMatrixResponse:
|
|
1148
1173
|
"""
|
|
1149
1174
|
Calculate distance matrix (multiple origins × destinations).
|
|
@@ -1162,6 +1187,10 @@ class Geocodio:
|
|
|
1162
1187
|
min_duration: Minimum duration filter (seconds, driving mode only).
|
|
1163
1188
|
order_by: Sort results by 'distance' or 'duration'.
|
|
1164
1189
|
sort_order: Sort direction ('asc' or 'desc').
|
|
1190
|
+
calculation_type: 'matrix' (default, every origin × every destination)
|
|
1191
|
+
or 'pairs' (origin i to destination i only; origins and
|
|
1192
|
+
destinations must be the same length and the max/min filters
|
|
1193
|
+
are not allowed).
|
|
1165
1194
|
|
|
1166
1195
|
Returns:
|
|
1167
1196
|
DistanceMatrixResponse with results for each origin.
|
|
@@ -1210,6 +1239,8 @@ class Geocodio:
|
|
|
1210
1239
|
body["order_by"] = order_by
|
|
1211
1240
|
if sort_order != DISTANCE_SORT_ASC:
|
|
1212
1241
|
body["sort"] = sort_order
|
|
1242
|
+
if calculation_type is not None:
|
|
1243
|
+
body["calculation_type"] = calculation_type
|
|
1213
1244
|
|
|
1214
1245
|
response = self._request(
|
|
1215
1246
|
"POST", endpoint, json=body, timeout=self.batch_timeout
|
|
@@ -1231,6 +1262,7 @@ class Geocodio:
|
|
|
1231
1262
|
min_duration: Optional[int] = None,
|
|
1232
1263
|
order_by: str = DISTANCE_ORDER_BY_DISTANCE,
|
|
1233
1264
|
sort_order: str = DISTANCE_SORT_ASC,
|
|
1265
|
+
calculation_type: Optional[str] = None,
|
|
1234
1266
|
) -> DistanceJobResponse:
|
|
1235
1267
|
"""
|
|
1236
1268
|
Create an async distance matrix job for large calculations.
|
|
@@ -1249,6 +1281,10 @@ class Geocodio:
|
|
|
1249
1281
|
min_duration: Minimum duration filter (seconds, driving mode only).
|
|
1250
1282
|
order_by: Sort results by 'distance' or 'duration'.
|
|
1251
1283
|
sort_order: Sort direction ('asc' or 'desc').
|
|
1284
|
+
calculation_type: 'matrix' (default, every origin × every destination)
|
|
1285
|
+
or 'pairs' (origin i to destination i only; origins and
|
|
1286
|
+
destinations must be the same length and the max/min filters
|
|
1287
|
+
are not allowed).
|
|
1252
1288
|
|
|
1253
1289
|
Returns:
|
|
1254
1290
|
DistanceJobResponse with job ID and status.
|
|
@@ -1307,6 +1343,8 @@ class Geocodio:
|
|
|
1307
1343
|
body["order_by"] = order_by
|
|
1308
1344
|
if sort_order != DISTANCE_SORT_ASC:
|
|
1309
1345
|
body["sort"] = sort_order
|
|
1346
|
+
if calculation_type is not None:
|
|
1347
|
+
body["calculation_type"] = calculation_type
|
|
1310
1348
|
|
|
1311
1349
|
response = self._request("POST", endpoint, json=body, timeout=self.list_timeout)
|
|
1312
1350
|
return DistanceJobResponse.from_api(response.json())
|
|
@@ -1365,6 +1403,7 @@ class Geocodio:
|
|
|
1365
1403
|
first_page_url=pagination_info.get("first_page_url"),
|
|
1366
1404
|
next_page_url=pagination_info.get("next_page_url"),
|
|
1367
1405
|
prev_page_url=pagination_info.get("prev_page_url"),
|
|
1406
|
+
warnings=parse_warnings(pagination_info),
|
|
1368
1407
|
)
|
|
1369
1408
|
|
|
1370
1409
|
def get_distance_matrix_job_results(
|
|
@@ -37,6 +37,13 @@ DISTANCE_ORDER_BY_DURATION = "duration"
|
|
|
37
37
|
DISTANCE_SORT_ASC = "asc"
|
|
38
38
|
DISTANCE_SORT_DESC = "desc"
|
|
39
39
|
|
|
40
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
41
|
+
# Calculation Type Constants (distance matrix and distance matrix jobs only)
|
|
42
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
DISTANCE_CALCULATION_TYPE_MATRIX = "matrix" # Every origin × every destination
|
|
45
|
+
DISTANCE_CALCULATION_TYPE_PAIRS = "pairs" # Origin i to destination i only
|
|
46
|
+
|
|
40
47
|
|
|
41
48
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
42
49
|
# Coordinate Class
|
|
@@ -263,6 +270,9 @@ __all__ = [
|
|
|
263
270
|
# Sort constants
|
|
264
271
|
"DISTANCE_SORT_ASC",
|
|
265
272
|
"DISTANCE_SORT_DESC",
|
|
273
|
+
# Calculation type constants
|
|
274
|
+
"DISTANCE_CALCULATION_TYPE_MATRIX",
|
|
275
|
+
"DISTANCE_CALCULATION_TYPE_PAIRS",
|
|
266
276
|
# Classes
|
|
267
277
|
"Coordinate",
|
|
268
278
|
# Functions
|
|
@@ -5,7 +5,7 @@ Structured exception hierarchy for the Geocodio Python client.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from dataclasses import dataclass
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
9
|
from typing import List, Optional, Union
|
|
10
10
|
|
|
11
11
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -22,6 +22,7 @@ class GeocodioErrorDetail:
|
|
|
22
22
|
message: str
|
|
23
23
|
code: Optional[int] = None # e.g. HTTP status or internal
|
|
24
24
|
errors: Optional[List[str]] = None # field‑specific validation messages
|
|
25
|
+
warnings: List[str] = field(default_factory=list) # from ``_warnings``
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -32,13 +33,30 @@ class GeocodioErrorDetail:
|
|
|
32
33
|
class GeocodioError(Exception):
|
|
33
34
|
"""Root of the library’s exception hierarchy."""
|
|
34
35
|
|
|
35
|
-
def __init__(
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
detail: Union[str, GeocodioErrorDetail],
|
|
39
|
+
warnings: Optional[List[str]] = None,
|
|
40
|
+
):
|
|
36
41
|
if isinstance(detail, str):
|
|
37
|
-
self.detail = GeocodioErrorDetail(
|
|
42
|
+
self.detail = GeocodioErrorDetail(
|
|
43
|
+
message=detail, warnings=list(warnings or [])
|
|
44
|
+
)
|
|
38
45
|
else:
|
|
39
46
|
self.detail = detail
|
|
40
47
|
super().__init__(self.detail.message)
|
|
41
48
|
|
|
49
|
+
@property
|
|
50
|
+
def warnings(self) -> List[str]:
|
|
51
|
+
"""
|
|
52
|
+
Non-fatal advisories the API returned alongside the error.
|
|
53
|
+
|
|
54
|
+
The API appends a ``_warnings`` key to error responses just as it does
|
|
55
|
+
to successful ones, e.g. to point out a misspelled field name in a
|
|
56
|
+
request that failed for an unrelated reason.
|
|
57
|
+
"""
|
|
58
|
+
return self.detail.warnings
|
|
59
|
+
|
|
42
60
|
def __str__(self) -> str: # prettier default printing
|
|
43
61
|
return self.detail.message
|
|
44
62
|
|
|
@@ -14,6 +14,21 @@ import httpx
|
|
|
14
14
|
T = TypeVar("T", bound="ExtrasMixin")
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def parse_warnings(data: Any) -> List[str]:
|
|
18
|
+
"""
|
|
19
|
+
Read the ``_warnings`` key from an API payload.
|
|
20
|
+
|
|
21
|
+
The API only sends the key when at least one warning was raised, so a
|
|
22
|
+
missing key (or a payload that is not an object) yields an empty list.
|
|
23
|
+
"""
|
|
24
|
+
if not isinstance(data, dict):
|
|
25
|
+
return []
|
|
26
|
+
warnings = data.get("_warnings")
|
|
27
|
+
if not isinstance(warnings, list):
|
|
28
|
+
return []
|
|
29
|
+
return [str(warning) for warning in warnings]
|
|
30
|
+
|
|
31
|
+
|
|
17
32
|
class ExtrasMixin:
|
|
18
33
|
"""Mixin to provide additional functionality for API response models."""
|
|
19
34
|
|
|
@@ -733,6 +748,8 @@ class DistanceJobResponse:
|
|
|
733
748
|
total_calculations: Total number of distance calculations.
|
|
734
749
|
download_url: URL to download results (when completed).
|
|
735
750
|
calculations_completed: Number of completed calculations.
|
|
751
|
+
warnings: Non-fatal advisories from the API's ``_warnings`` key.
|
|
752
|
+
calculation_type: 'matrix' or 'pairs' (None if the API did not send it).
|
|
736
753
|
"""
|
|
737
754
|
|
|
738
755
|
id: int
|
|
@@ -746,10 +763,15 @@ class DistanceJobResponse:
|
|
|
746
763
|
download_url: Optional[str] = None
|
|
747
764
|
calculations_completed: Optional[int] = None
|
|
748
765
|
progress: Optional[int] = None
|
|
766
|
+
warnings: List[str] = field(default_factory=list)
|
|
767
|
+
calculation_type: Optional[str] = None
|
|
749
768
|
|
|
750
769
|
@classmethod
|
|
751
770
|
def from_api(cls, data: Dict[str, Any]) -> "DistanceJobResponse":
|
|
752
771
|
"""Create from API response data."""
|
|
772
|
+
# Warnings sit beside the nested "data" key, so read them first
|
|
773
|
+
warnings = parse_warnings(data)
|
|
774
|
+
|
|
753
775
|
# Handle nested "data" key for status responses
|
|
754
776
|
if "data" in data and isinstance(data["data"], dict):
|
|
755
777
|
data = data["data"]
|
|
@@ -771,6 +793,8 @@ class DistanceJobResponse:
|
|
|
771
793
|
download_url=data.get("download_url"),
|
|
772
794
|
calculations_completed=data.get("calculations_completed"),
|
|
773
795
|
progress=data.get("progress"),
|
|
796
|
+
warnings=warnings,
|
|
797
|
+
calculation_type=data.get("calculation_type"),
|
|
774
798
|
)
|
|
775
799
|
|
|
776
800
|
|
|
@@ -793,6 +817,9 @@ class GeocodingResult:
|
|
|
793
817
|
match_type: Optional[str] = None
|
|
794
818
|
address_lines: Optional[List[str]] = None
|
|
795
819
|
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
820
|
+
# This result's ``_warnings`` (e.g. a skipped ffiec append). For batch
|
|
821
|
+
# requests, also the warnings attached to this query's response.
|
|
822
|
+
warnings: List[str] = field(default_factory=list)
|
|
796
823
|
|
|
797
824
|
@property
|
|
798
825
|
def matched(self) -> bool:
|
|
@@ -820,11 +847,15 @@ class GeocodingResponse:
|
|
|
820
847
|
results: Flat list of results, one per submitted query.
|
|
821
848
|
raw: The untouched JSON payload as returned by the API.
|
|
822
849
|
rate_limit: Rate limit state from the response headers, when present.
|
|
850
|
+
warnings: Non-fatal advisories from the API's ``_warnings`` key, e.g.
|
|
851
|
+
an unrecognized field name. For batch requests, the de-duplicated
|
|
852
|
+
warnings from every query's response.
|
|
823
853
|
"""
|
|
824
854
|
|
|
825
855
|
results: List[GeocodingResult] = field(default_factory=list)
|
|
826
856
|
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
827
857
|
rate_limit: Optional[RateLimit] = None
|
|
858
|
+
warnings: List[str] = field(default_factory=list)
|
|
828
859
|
|
|
829
860
|
def to_dict(self) -> Dict[str, Any]:
|
|
830
861
|
"""
|
|
@@ -851,6 +882,8 @@ class ListProcessingState:
|
|
|
851
882
|
class ListResponse:
|
|
852
883
|
"""
|
|
853
884
|
status, download_url, expires_at are not always present.
|
|
885
|
+
|
|
886
|
+
warnings holds non-fatal advisories from the API's ``_warnings`` key.
|
|
854
887
|
"""
|
|
855
888
|
|
|
856
889
|
id: str
|
|
@@ -859,6 +892,7 @@ class ListResponse:
|
|
|
859
892
|
download_url: Optional[str] = None
|
|
860
893
|
expires_at: Optional[str] = None
|
|
861
894
|
http_response: Optional[httpx.Response] = None
|
|
895
|
+
warnings: List[str] = field(default_factory=list)
|
|
862
896
|
|
|
863
897
|
|
|
864
898
|
@dataclass(slots=True, frozen=True)
|
|
@@ -876,3 +910,4 @@ class PaginatedResponse:
|
|
|
876
910
|
first_page_url: str
|
|
877
911
|
next_page_url: Optional[str] = None
|
|
878
912
|
prev_page_url: Optional[str] = None
|
|
913
|
+
warnings: List[str] = field(default_factory=list)
|
|
@@ -8,6 +8,7 @@ import httpx
|
|
|
8
8
|
import pytest
|
|
9
9
|
|
|
10
10
|
from geocodio import (
|
|
11
|
+
DISTANCE_CALCULATION_TYPE_PAIRS,
|
|
11
12
|
DISTANCE_MODE_DRIVING,
|
|
12
13
|
DISTANCE_MODE_HAVERSINE,
|
|
13
14
|
DISTANCE_MODE_STRAIGHTLINE,
|
|
@@ -441,6 +442,39 @@ class TestDistanceMatrix:
|
|
|
441
442
|
destinations=[Coordinate(38.8895, -77.0353, "dest1")],
|
|
442
443
|
)
|
|
443
444
|
|
|
445
|
+
def test_distance_matrix_sends_calculation_type(self, client, httpx_mock):
|
|
446
|
+
"""Test that calculation_type is sent in the POST body when given."""
|
|
447
|
+
|
|
448
|
+
def response_callback(request):
|
|
449
|
+
body = json.loads(request.content)
|
|
450
|
+
assert body["calculation_type"] == "pairs"
|
|
451
|
+
return httpx.Response(200, json=sample_distance_matrix_response())
|
|
452
|
+
|
|
453
|
+
httpx_mock.add_callback(callback=response_callback)
|
|
454
|
+
|
|
455
|
+
client.distance_matrix(
|
|
456
|
+
origins=["38.8977,-77.0365,home", "38.886672,-77.094735,office"],
|
|
457
|
+
destinations=["38.9072,-77.0369,capitol", "38.8814,-77.0916,pentagon"],
|
|
458
|
+
calculation_type=DISTANCE_CALCULATION_TYPE_PAIRS,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def test_distance_matrix_omits_calculation_type_by_default(
|
|
462
|
+
self, client, httpx_mock
|
|
463
|
+
):
|
|
464
|
+
"""Test that calculation_type is left out when not given."""
|
|
465
|
+
|
|
466
|
+
def response_callback(request):
|
|
467
|
+
body = json.loads(request.content)
|
|
468
|
+
assert "calculation_type" not in body
|
|
469
|
+
return httpx.Response(200, json=sample_distance_matrix_response())
|
|
470
|
+
|
|
471
|
+
httpx_mock.add_callback(callback=response_callback)
|
|
472
|
+
|
|
473
|
+
client.distance_matrix(
|
|
474
|
+
origins=["38.8977,-77.0365"],
|
|
475
|
+
destinations=["38.9072,-77.0369"],
|
|
476
|
+
)
|
|
477
|
+
|
|
444
478
|
|
|
445
479
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
446
480
|
# Distance Job Method Tests
|
|
@@ -572,6 +606,57 @@ class TestDistanceJobs:
|
|
|
572
606
|
callback_url="https://example.com/webhook",
|
|
573
607
|
)
|
|
574
608
|
|
|
609
|
+
def test_create_job_sends_calculation_type(self, client, httpx_mock):
|
|
610
|
+
"""Test creating a job with calculation_type."""
|
|
611
|
+
|
|
612
|
+
def response_callback(request):
|
|
613
|
+
body = json.loads(request.content)
|
|
614
|
+
assert body["calculation_type"] == "pairs"
|
|
615
|
+
return httpx.Response(200, json=sample_job_create_response())
|
|
616
|
+
|
|
617
|
+
httpx_mock.add_callback(callback=response_callback)
|
|
618
|
+
|
|
619
|
+
client.create_distance_matrix_job(
|
|
620
|
+
name="Commutes",
|
|
621
|
+
origins=[(38.8977, -77.0365), (38.886672, -77.094735)],
|
|
622
|
+
destinations=[(38.9072, -77.0369), (38.8814, -77.0916)],
|
|
623
|
+
calculation_type=DISTANCE_CALCULATION_TYPE_PAIRS,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
def test_create_job_omits_calculation_type_by_default(self, client, httpx_mock):
|
|
627
|
+
"""Test that calculation_type is left out of the job when not given."""
|
|
628
|
+
|
|
629
|
+
def response_callback(request):
|
|
630
|
+
body = json.loads(request.content)
|
|
631
|
+
assert "calculation_type" not in body
|
|
632
|
+
return httpx.Response(200, json=sample_job_create_response())
|
|
633
|
+
|
|
634
|
+
httpx_mock.add_callback(callback=response_callback)
|
|
635
|
+
|
|
636
|
+
client.create_distance_matrix_job(
|
|
637
|
+
name="My Job",
|
|
638
|
+
origins=[(38.8977, -77.0365)],
|
|
639
|
+
destinations=[(38.8895, -77.0353)],
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
def test_job_status_parses_calculation_type(self, client, httpx_mock):
|
|
643
|
+
"""Test that calculation_type is read from the job response."""
|
|
644
|
+
payload = sample_job_status_response()
|
|
645
|
+
payload["data"]["calculation_type"] = "pairs"
|
|
646
|
+
httpx_mock.add_callback(
|
|
647
|
+
callback=lambda request: httpx.Response(200, json=payload)
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
response = client.distance_matrix_job_status(123)
|
|
651
|
+
|
|
652
|
+
assert response.calculation_type == "pairs"
|
|
653
|
+
|
|
654
|
+
def test_job_without_calculation_type_defaults_to_none(self):
|
|
655
|
+
"""Test that responses predating calculation_type still parse."""
|
|
656
|
+
response = DistanceJobResponse.from_api(sample_job_create_response())
|
|
657
|
+
|
|
658
|
+
assert response.calculation_type is None
|
|
659
|
+
|
|
575
660
|
def test_job_status(self, client, httpx_mock):
|
|
576
661
|
"""Test getting job status."""
|
|
577
662
|
httpx_mock.add_callback(
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The API reports non-fatal advisories under a `_warnings` key -- an
|
|
3
|
+
unrecognized field name, a superseded API version, an append that was
|
|
4
|
+
skipped. These tests lock in that the key is parsed onto every response
|
|
5
|
+
shape, so a future model refactor cannot silently drop it.
|
|
6
|
+
|
|
7
|
+
The shapes are the ones the OpenAPI specification models (see the `Warnings`
|
|
8
|
+
schema): top-level on single geocode/reverse, per result, per batch item,
|
|
9
|
+
and on the lists and distance-jobs responses.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from geocodio.exceptions import (
|
|
15
|
+
AuthenticationError,
|
|
16
|
+
GeocodioError,
|
|
17
|
+
InvalidRequestError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
FIELD_WARNING = "The field congress is not recognized. Did you mean cd?"
|
|
21
|
+
VERSION_WARNING = (
|
|
22
|
+
"There is a newer API version available, please consider upgrading to v2."
|
|
23
|
+
)
|
|
24
|
+
FIELDS_FORMAT_WARNING = (
|
|
25
|
+
"The fields parameter should contain a comma-separated list of fields "
|
|
26
|
+
"instead of an array"
|
|
27
|
+
)
|
|
28
|
+
FFIEC_WARNING = "ffiec field was skipped since result is not street-level"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def result(formatted_address: str, **extra) -> dict:
|
|
32
|
+
return {
|
|
33
|
+
"address_components": {"city": "Arlington", "country": "US"},
|
|
34
|
+
"formatted_address": formatted_address,
|
|
35
|
+
"location": {"lat": 38.886665, "lng": -77.094733},
|
|
36
|
+
"accuracy": 1,
|
|
37
|
+
"accuracy_type": "rooftop",
|
|
38
|
+
"source": "Arlington",
|
|
39
|
+
**extra,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
44
|
+
# Geocoding responses
|
|
45
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_single_forward_geocode_warnings(client, httpx_mock):
|
|
49
|
+
httpx_mock.add_response(
|
|
50
|
+
json={
|
|
51
|
+
"results": [result("1109 N Highland St, Arlington, VA 22201")],
|
|
52
|
+
"_warnings": [FIELD_WARNING],
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
response = client.geocode("1109 N Highland St, Arlington VA", fields=["congress"])
|
|
57
|
+
|
|
58
|
+
assert response.warnings == [FIELD_WARNING]
|
|
59
|
+
assert response.results[0].warnings == []
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_single_reverse_geocode_warnings(client, httpx_mock):
|
|
63
|
+
warning = (
|
|
64
|
+
"Ignoring parameter zipcode as it was not expected. Did you mean postal_code?"
|
|
65
|
+
)
|
|
66
|
+
httpx_mock.add_response(
|
|
67
|
+
json={
|
|
68
|
+
"results": [result("1109 N Highland St, Arlington, VA 22201")],
|
|
69
|
+
"_warnings": [warning],
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
response = client.reverse("38.886665,-77.094733")
|
|
74
|
+
|
|
75
|
+
assert response.warnings == [warning]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_per_result_warnings(client, httpx_mock):
|
|
79
|
+
httpx_mock.add_response(
|
|
80
|
+
json={
|
|
81
|
+
"results": [
|
|
82
|
+
result(
|
|
83
|
+
"Arlington, VA 22201",
|
|
84
|
+
accuracy_type="place",
|
|
85
|
+
_warnings=[FFIEC_WARNING],
|
|
86
|
+
)
|
|
87
|
+
],
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
response = client.geocode("22201", fields=["ffiec"])
|
|
92
|
+
|
|
93
|
+
assert response.warnings == []
|
|
94
|
+
assert response.results[0].warnings == [FFIEC_WARNING]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_batch_forward_geocode_warnings(client, httpx_mock):
|
|
98
|
+
httpx_mock.add_response(
|
|
99
|
+
json={
|
|
100
|
+
"results": [
|
|
101
|
+
{
|
|
102
|
+
"query": "1109 N Highland St, Arlington VA",
|
|
103
|
+
"response": {
|
|
104
|
+
"results": [
|
|
105
|
+
result(
|
|
106
|
+
"1109 N Highland St, Arlington, VA 22201",
|
|
107
|
+
_warnings=[FFIEC_WARNING],
|
|
108
|
+
)
|
|
109
|
+
],
|
|
110
|
+
"_warnings": [FIELD_WARNING],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
"query": "525 University Ave, Toronto, ON, Canada",
|
|
115
|
+
"response": {
|
|
116
|
+
"results": [result("525 University Ave, Toronto, ON M5G")],
|
|
117
|
+
"_warnings": [FIELD_WARNING],
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
]
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
response = client.geocode(
|
|
125
|
+
[
|
|
126
|
+
"1109 N Highland St, Arlington VA",
|
|
127
|
+
"525 University Ave, Toronto, ON, Canada",
|
|
128
|
+
],
|
|
129
|
+
fields=["congress"],
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Per query: that query's warnings, then the result's own
|
|
133
|
+
assert response.results[0].warnings == [FIELD_WARNING, FFIEC_WARNING]
|
|
134
|
+
assert response.results[1].warnings == [FIELD_WARNING]
|
|
135
|
+
# Across the batch: each warning once
|
|
136
|
+
assert response.warnings == [FIELD_WARNING]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_batch_reverse_geocode_warnings(client, httpx_mock):
|
|
140
|
+
httpx_mock.add_response(
|
|
141
|
+
json={
|
|
142
|
+
"results": [
|
|
143
|
+
{
|
|
144
|
+
"query": "35.9746000,-77.9658000",
|
|
145
|
+
"response": {
|
|
146
|
+
"results": [result("101 W Washington St, Nashville, NC 27856")],
|
|
147
|
+
"_warnings": [VERSION_WARNING],
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
response = client.reverse(["35.9746000,-77.9658000"])
|
|
155
|
+
|
|
156
|
+
assert response.results[0].warnings == [VERSION_WARNING]
|
|
157
|
+
assert response.warnings == [VERSION_WARNING]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_batch_warnings_kept_for_unmatched_query(client, httpx_mock):
|
|
161
|
+
httpx_mock.add_response(
|
|
162
|
+
json={
|
|
163
|
+
"results": [
|
|
164
|
+
{
|
|
165
|
+
"query": "not an address",
|
|
166
|
+
"response": {"results": [], "_warnings": [FIELD_WARNING]},
|
|
167
|
+
},
|
|
168
|
+
]
|
|
169
|
+
}
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
response = client.geocode(["not an address"], fields=["congress"])
|
|
173
|
+
|
|
174
|
+
assert response.results[0].matched is False
|
|
175
|
+
assert response.results[0].warnings == [FIELD_WARNING]
|
|
176
|
+
assert response.warnings == [FIELD_WARNING]
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_no_warnings_when_api_sends_none(client, httpx_mock):
|
|
180
|
+
httpx_mock.add_response(
|
|
181
|
+
json={"results": [result("1109 N Highland St, Arlington, VA 22201")]}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
response = client.geocode("1109 N Highland St, Arlington VA")
|
|
185
|
+
|
|
186
|
+
assert response.warnings == []
|
|
187
|
+
assert response.results[0].warnings == []
|
|
188
|
+
assert "_warnings" not in response.raw
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def test_warnings_remain_on_raw_payload(client, httpx_mock):
|
|
192
|
+
httpx_mock.add_response(
|
|
193
|
+
json={
|
|
194
|
+
"results": [result("1109 N Highland St, Arlington, VA 22201")],
|
|
195
|
+
"_warnings": [FIELD_WARNING],
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
response = client.geocode("1109 N Highland St, Arlington VA", fields=["congress"])
|
|
200
|
+
|
|
201
|
+
assert response.raw["_warnings"] == [FIELD_WARNING]
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
205
|
+
# Lists responses
|
|
206
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_create_list_warnings(client, httpx_mock):
|
|
210
|
+
warning = (
|
|
211
|
+
"The following field was not recognized and has been skipped: "
|
|
212
|
+
"congressional_district"
|
|
213
|
+
)
|
|
214
|
+
httpx_mock.add_response(
|
|
215
|
+
json={
|
|
216
|
+
"id": 42,
|
|
217
|
+
"file": {"filename": "inline.csv"},
|
|
218
|
+
"status": {"state": "PROCESSING"},
|
|
219
|
+
"_warnings": [warning],
|
|
220
|
+
}
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
response = client.create_list(
|
|
224
|
+
file="address\n1109 N Highland St, Arlington VA",
|
|
225
|
+
filename="inline.csv",
|
|
226
|
+
fields=["congressional_district"],
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
assert response.warnings == [warning]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_get_list_warnings(client, httpx_mock):
|
|
233
|
+
httpx_mock.add_response(
|
|
234
|
+
json={
|
|
235
|
+
"id": 42,
|
|
236
|
+
"file": {"filename": "inline.csv"},
|
|
237
|
+
"status": {"state": "COMPLETED"},
|
|
238
|
+
"_warnings": [FIELDS_FORMAT_WARNING],
|
|
239
|
+
}
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
assert client.get_list("42").warnings == [FIELDS_FORMAT_WARNING]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_get_lists_warnings(client, httpx_mock):
|
|
246
|
+
httpx_mock.add_response(
|
|
247
|
+
json={
|
|
248
|
+
"current_page": 1,
|
|
249
|
+
"data": [],
|
|
250
|
+
"from": 0,
|
|
251
|
+
"to": 0,
|
|
252
|
+
"path": "/v2/lists",
|
|
253
|
+
"per_page": 15,
|
|
254
|
+
"first_page_url": "/v2/lists?page=1",
|
|
255
|
+
"_warnings": [FIELDS_FORMAT_WARNING],
|
|
256
|
+
}
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
assert client.get_lists().warnings == [FIELDS_FORMAT_WARNING]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
263
|
+
# Distance matrix job responses
|
|
264
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def test_create_distance_matrix_job_warnings(client, httpx_mock):
|
|
268
|
+
httpx_mock.add_response(
|
|
269
|
+
json={
|
|
270
|
+
"id": 123,
|
|
271
|
+
"identifier": "dmj_abc123",
|
|
272
|
+
"status": "ENQUEUED",
|
|
273
|
+
"name": "Store coverage",
|
|
274
|
+
"created_at": "2026-09-25T12:00:00.000000Z",
|
|
275
|
+
"origins_count": 1,
|
|
276
|
+
"destinations_count": 1,
|
|
277
|
+
"total_calculations": 1,
|
|
278
|
+
"_warnings": [VERSION_WARNING],
|
|
279
|
+
}
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
response = client.create_distance_matrix_job(
|
|
283
|
+
name="Store coverage",
|
|
284
|
+
origins=[(38.886665, -77.094733)],
|
|
285
|
+
destinations=[(38.897675, -77.036547)],
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
assert response.warnings == [VERSION_WARNING]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_distance_matrix_job_status_warnings(client, httpx_mock):
|
|
292
|
+
# Warnings sit beside the nested "data" key, not inside it
|
|
293
|
+
httpx_mock.add_response(
|
|
294
|
+
json={
|
|
295
|
+
"data": {"id": 123, "identifier": "dmj_abc123", "status": "COMPLETED"},
|
|
296
|
+
"_warnings": [FIELDS_FORMAT_WARNING],
|
|
297
|
+
}
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
response = client.distance_matrix_job_status("dmj_abc123")
|
|
301
|
+
|
|
302
|
+
assert response.status == "COMPLETED"
|
|
303
|
+
assert response.warnings == [FIELDS_FORMAT_WARNING]
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def test_distance_matrix_jobs_warnings(client, httpx_mock):
|
|
307
|
+
httpx_mock.add_response(
|
|
308
|
+
json={
|
|
309
|
+
"current_page": 1,
|
|
310
|
+
"data": [],
|
|
311
|
+
"from": 0,
|
|
312
|
+
"to": 0,
|
|
313
|
+
"path": "/v2/distance-jobs",
|
|
314
|
+
"per_page": 10,
|
|
315
|
+
"first_page_url": "/v2/distance-jobs?page=1",
|
|
316
|
+
"_warnings": [FIELDS_FORMAT_WARNING],
|
|
317
|
+
}
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
assert client.distance_matrix_jobs().warnings == [FIELDS_FORMAT_WARNING]
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
324
|
+
# Error responses
|
|
325
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_error_response_warnings(client, httpx_mock):
|
|
329
|
+
httpx_mock.add_response(
|
|
330
|
+
status_code=422,
|
|
331
|
+
json={
|
|
332
|
+
"error": "Could not geocode address. Postal code or city required.",
|
|
333
|
+
"_warnings": [FIELD_WARNING],
|
|
334
|
+
},
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
with pytest.raises(InvalidRequestError) as exc_info:
|
|
338
|
+
client.geocode("1109 N Highland St", fields=["congress"])
|
|
339
|
+
|
|
340
|
+
assert exc_info.value.warnings == [FIELD_WARNING]
|
|
341
|
+
assert exc_info.value.detail.warnings == [FIELD_WARNING]
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def test_error_response_without_warnings(client, httpx_mock):
|
|
345
|
+
httpx_mock.add_response(status_code=403, json={"error": "Invalid API key"})
|
|
346
|
+
|
|
347
|
+
with pytest.raises(AuthenticationError) as exc_info:
|
|
348
|
+
client.geocode("1109 N Highland St, Arlington VA")
|
|
349
|
+
|
|
350
|
+
assert exc_info.value.warnings == []
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
def test_non_json_error_response_has_no_warnings(client, httpx_mock):
|
|
354
|
+
httpx_mock.add_response(status_code=502, text="<html>Bad Gateway</html>")
|
|
355
|
+
|
|
356
|
+
with pytest.raises(GeocodioError) as exc_info:
|
|
357
|
+
client.geocode("1109 N Highland St, Arlington VA")
|
|
358
|
+
|
|
359
|
+
assert exc_info.value.warnings == []
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def test_error_constructed_directly_has_no_warnings():
|
|
363
|
+
assert GeocodioError("boom").warnings == []
|
|
364
|
+
assert GeocodioError("boom", warnings=[FIELD_WARNING]).warnings == [FIELD_WARNING]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
version = 1
|
|
2
|
-
revision =
|
|
2
|
+
revision = 5
|
|
3
3
|
requires-python = ">=3.11"
|
|
4
4
|
|
|
5
5
|
[[package]]
|
|
@@ -149,7 +149,7 @@ wheels = [
|
|
|
149
149
|
|
|
150
150
|
[[package]]
|
|
151
151
|
name = "geocodio-library-python"
|
|
152
|
-
version = "1.
|
|
152
|
+
version = "1.6.0"
|
|
153
153
|
source = { editable = "." }
|
|
154
154
|
dependencies = [
|
|
155
155
|
{ name = "httpx" },
|
|
File without changes
|
{geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/e2e/test_batch_reverse.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/test_exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
{geocodio_library_python-1.4.0 → geocodio_library_python-1.6.0}/tests/unit/test_geocoding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|