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.
Files changed (40) hide show
  1. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/CHANGELOG.md +6 -0
  2. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/PKG-INFO +44 -1
  3. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/README.md +43 -0
  4. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/pyproject.toml +1 -1
  5. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/_version.py +1 -1
  6. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/client.py +29 -4
  7. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/exceptions.py +21 -3
  8. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/models.py +32 -0
  9. geocodio_library_python-1.5.0/tests/unit/test_warnings.py +364 -0
  10. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/uv.lock +2 -2
  11. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/.github/workflows/ci.yml +0 -0
  12. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/.github/workflows/publish.yml +0 -0
  13. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/.github/workflows/test-act-event-ci.json +0 -0
  14. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/.github/workflows/test-act-event-publish.json +0 -0
  15. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/.gitignore +0 -0
  16. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/LICENSE +0 -0
  17. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/requirements-dev.txt +0 -0
  18. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/setup.cfg +0 -0
  19. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/smoke.py +0 -0
  20. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/smoke_lists.py +0 -0
  21. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/__init__.py +0 -0
  22. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/src/geocodio/distance.py +0 -0
  23. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/test_pypi_release.py +0 -0
  24. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/__init__.py +0 -0
  25. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/conftest.py +0 -0
  26. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/e2e/__init__.py +0 -0
  27. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/e2e/test_api.py +0 -0
  28. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/e2e/test_batch_reverse.py +0 -0
  29. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/e2e/test_distance.py +0 -0
  30. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/e2e/test_lists_api.py +0 -0
  31. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/test_workflows.py +0 -0
  32. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/__init__.py +0 -0
  33. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_client.py +0 -0
  34. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_distance.py +0 -0
  35. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_errors.py +0 -0
  36. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_exceptions.py +0 -0
  37. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_geocode.py +0 -0
  38. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_geocoding.py +0 -0
  39. {geocodio_library_python-1.4.0 → geocodio_library_python-1.5.0}/tests/unit/test_models.py +0 -0
  40. {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.4.0
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:
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "geocodio-library-python"
7
- version = "1.4.0"
7
+ version = "1.5.0"
8
8
  description = "A Python client for the Geocodio API"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,3 +1,3 @@
1
1
  """Version information for geocodio package."""
2
2
 
3
- __version__ = "1.4.0"
3
+ __version__ = "1.5.0"
@@ -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, raw=response_json, rate_limit=rate_limit
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, raw=response_json, rate_limit=rate_limit
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__(self, detail: Union[str, GeocodioErrorDetail]):
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(message=detail)
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 = 3
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.4.0"
152
+ version = "1.5.0"
153
153
  source = { editable = "." }
154
154
  dependencies = [
155
155
  { name = "httpx" },