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