geocodio-library-python 1.4.0__tar.gz → 1.5.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.5.0}/CHANGELOG.md +6 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/PKG-INFO +44 -1
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/README.md +43 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/pyproject.toml +1 -1
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/_version.py +1 -1
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/client.py +29 -4
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/exceptions.py +21 -3
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/models.py +32 -0
- geocodio_library_python-1.5.0/tests/unit/test_warnings.py +364 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/uv.lock +2 -2
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/.github/workflows/ci.yml +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/.github/workflows/publish.yml +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/.github/workflows/test-act-event-ci.json +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/.github/workflows/test-act-event-publish.json +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/.gitignore +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/LICENSE +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/requirements-dev.txt +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/setup.cfg +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/smoke.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/smoke_lists.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/__init__.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/distance.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/test_pypi_release.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/__init__.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/conftest.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/e2e/__init__.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/e2e/test_api.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/e2e/test_batch_reverse.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/e2e/test_distance.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/e2e/test_lists_api.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/test_workflows.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/__init__.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_client.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_distance.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_errors.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_exceptions.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_geocode.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_geocoding.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_models.py +0 -0
- {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_reverse.py +0 -0
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.5.0] - 2026-10-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- 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.
|
|
14
|
+
- `GeocodioError.warnings` (and `GeocodioErrorDetail.warnings`), exposing warnings attached to error responses.
|
|
15
|
+
|
|
10
16
|
## [1.4.0] - 2026-08-28
|
|
11
17
|
|
|
12
18
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: geocodio-library-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.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:
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -1365,6 +1389,7 @@ class Geocodio:
|
|
|
1365
1389
|
first_page_url=pagination_info.get("first_page_url"),
|
|
1366
1390
|
next_page_url=pagination_info.get("next_page_url"),
|
|
1367
1391
|
prev_page_url=pagination_info.get("prev_page_url"),
|
|
1392
|
+
warnings=parse_warnings(pagination_info),
|
|
1368
1393
|
)
|
|
1369
1394
|
|
|
1370
1395
|
def get_distance_matrix_job_results(
|
|
@@ -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,7 @@ 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.
|
|
736
752
|
"""
|
|
737
753
|
|
|
738
754
|
id: int
|
|
@@ -746,10 +762,14 @@ class DistanceJobResponse:
|
|
|
746
762
|
download_url: Optional[str] = None
|
|
747
763
|
calculations_completed: Optional[int] = None
|
|
748
764
|
progress: Optional[int] = None
|
|
765
|
+
warnings: List[str] = field(default_factory=list)
|
|
749
766
|
|
|
750
767
|
@classmethod
|
|
751
768
|
def from_api(cls, data: Dict[str, Any]) -> "DistanceJobResponse":
|
|
752
769
|
"""Create from API response data."""
|
|
770
|
+
# Warnings sit beside the nested "data" key, so read them first
|
|
771
|
+
warnings = parse_warnings(data)
|
|
772
|
+
|
|
753
773
|
# Handle nested "data" key for status responses
|
|
754
774
|
if "data" in data and isinstance(data["data"], dict):
|
|
755
775
|
data = data["data"]
|
|
@@ -771,6 +791,7 @@ class DistanceJobResponse:
|
|
|
771
791
|
download_url=data.get("download_url"),
|
|
772
792
|
calculations_completed=data.get("calculations_completed"),
|
|
773
793
|
progress=data.get("progress"),
|
|
794
|
+
warnings=warnings,
|
|
774
795
|
)
|
|
775
796
|
|
|
776
797
|
|
|
@@ -793,6 +814,9 @@ class GeocodingResult:
|
|
|
793
814
|
match_type: Optional[str] = None
|
|
794
815
|
address_lines: Optional[List[str]] = None
|
|
795
816
|
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
817
|
+
# This result's ``_warnings`` (e.g. a skipped ffiec append). For batch
|
|
818
|
+
# requests, also the warnings attached to this query's response.
|
|
819
|
+
warnings: List[str] = field(default_factory=list)
|
|
796
820
|
|
|
797
821
|
@property
|
|
798
822
|
def matched(self) -> bool:
|
|
@@ -820,11 +844,15 @@ class GeocodingResponse:
|
|
|
820
844
|
results: Flat list of results, one per submitted query.
|
|
821
845
|
raw: The untouched JSON payload as returned by the API.
|
|
822
846
|
rate_limit: Rate limit state from the response headers, when present.
|
|
847
|
+
warnings: Non-fatal advisories from the API's ``_warnings`` key, e.g.
|
|
848
|
+
an unrecognized field name. For batch requests, the de-duplicated
|
|
849
|
+
warnings from every query's response.
|
|
823
850
|
"""
|
|
824
851
|
|
|
825
852
|
results: List[GeocodingResult] = field(default_factory=list)
|
|
826
853
|
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
827
854
|
rate_limit: Optional[RateLimit] = None
|
|
855
|
+
warnings: List[str] = field(default_factory=list)
|
|
828
856
|
|
|
829
857
|
def to_dict(self) -> Dict[str, Any]:
|
|
830
858
|
"""
|
|
@@ -851,6 +879,8 @@ class ListProcessingState:
|
|
|
851
879
|
class ListResponse:
|
|
852
880
|
"""
|
|
853
881
|
status, download_url, expires_at are not always present.
|
|
882
|
+
|
|
883
|
+
warnings holds non-fatal advisories from the API's ``_warnings`` key.
|
|
854
884
|
"""
|
|
855
885
|
|
|
856
886
|
id: str
|
|
@@ -859,6 +889,7 @@ class ListResponse:
|
|
|
859
889
|
download_url: Optional[str] = None
|
|
860
890
|
expires_at: Optional[str] = None
|
|
861
891
|
http_response: Optional[httpx.Response] = None
|
|
892
|
+
warnings: List[str] = field(default_factory=list)
|
|
862
893
|
|
|
863
894
|
|
|
864
895
|
@dataclass(slots=True, frozen=True)
|
|
@@ -876,3 +907,4 @@ class PaginatedResponse:
|
|
|
876
907
|
first_page_url: str
|
|
877
908
|
next_page_url: Optional[str] = None
|
|
878
909
|
prev_page_url: Optional[str] = None
|
|
910
|
+
warnings: List[str] = field(default_factory=list)
|
|
@@ -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.5.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.5.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
|
|
File without changes
|
|
File without changes
|
{geocodio_library_python-1.4.0 → geocodio_library_python-1.5.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
|
|
File without changes
|
{geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
{geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_geocoding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|