geocodio-library-python 1.3.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.3.0 → geocodio_library_python-1.5.0}/CHANGELOG.md +14 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/PKG-INFO +102 -1
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/README.md +101 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/pyproject.toml +1 -1
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/src/geocodio/_version.py +1 -1
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/src/geocodio/client.py +56 -7
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/src/geocodio/exceptions.py +21 -3
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/src/geocodio/models.py +161 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/e2e/test_api.py +56 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/unit/test_geocode.py +213 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/unit/test_models.py +107 -0
- geocodio_library_python-1.5.0/tests/unit/test_warnings.py +364 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/uv.lock +2 -2
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/.github/workflows/ci.yml +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/.github/workflows/publish.yml +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/.github/workflows/test-act-event-ci.json +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/.github/workflows/test-act-event-publish.json +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/.gitignore +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/LICENSE +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/requirements-dev.txt +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/setup.cfg +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/smoke.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/smoke_lists.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/src/geocodio/__init__.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/src/geocodio/distance.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/test_pypi_release.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/__init__.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/conftest.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/e2e/__init__.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/e2e/test_batch_reverse.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/e2e/test_distance.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/e2e/test_lists_api.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/test_workflows.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/unit/__init__.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/unit/test_client.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/unit/test_distance.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/unit/test_errors.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/unit/test_exceptions.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/unit/test_geocoding.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.5.0}/tests/unit/test_reverse.py +0 -0
|
@@ -7,6 +7,20 @@ 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
|
+
|
|
16
|
+
## [1.4.0] - 2026-08-28
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Public census accessors on `GeocodioFields`: `fields.census` (the requested append, most recent vintage when several are present), `fields.get_census(year)` (accepts `2023`, `"2023"` or `"census2023"`), `fields.census_years` and `fields.census_data`. The private `fields._census` dict and the dynamic `fields.census2023` attributes are unchanged.
|
|
20
|
+
- `GeocodingResult.match_type` and `GeocodingResult.address_lines`, both of which the API returns on every result and the model previously dropped.
|
|
21
|
+
- Raw response access: `GeocodingResponse.raw` / `.to_dict()` for the full untouched JSON payload, and `GeocodingResult.raw` / `.to_dict()` for a single result. Unlike `dataclasses.asdict()` these keep every key the API sent.
|
|
22
|
+
- Rate limit headers are parsed into a `RateLimit` model, exposed as `GeocodingResponse.rate_limit` and `Geocodio.rate_limit` (updated on every request, including ones that raise).
|
|
23
|
+
|
|
10
24
|
## [1.3.0] - 2026-08-25
|
|
11
25
|
|
|
12
26
|
### 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
|
|
@@ -149,6 +149,107 @@ response = client.geocode(
|
|
|
149
149
|
)
|
|
150
150
|
```
|
|
151
151
|
|
|
152
|
+
#### Census appends
|
|
153
|
+
|
|
154
|
+
Census data is keyed by vintage. `fields.census` gives you the append you
|
|
155
|
+
requested, and `fields.get_census(year)` picks a specific vintage when you
|
|
156
|
+
requested more than one:
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
response = client.geocode("1109 N Highland St, Arlington VA", fields=["census2023"])
|
|
160
|
+
census = response.results[0].fields.census
|
|
161
|
+
|
|
162
|
+
print(census.full_fips) # "510131018012004"
|
|
163
|
+
print(census.census_year) # 2023
|
|
164
|
+
|
|
165
|
+
# When several vintages were requested
|
|
166
|
+
fields = response.results[0].fields
|
|
167
|
+
fields.get_census(2023) # also accepts "2023" or "census2023"
|
|
168
|
+
fields.census_years # [2023]
|
|
169
|
+
fields.census_data # {"census2023": CensusData(...)}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`fields.census` returns the most recent vintage present. Accessing a vintage
|
|
173
|
+
directly (`fields.census2023`) continues to work.
|
|
174
|
+
|
|
175
|
+
### Raw API responses
|
|
176
|
+
|
|
177
|
+
Every geocoding response keeps the untouched JSON payload the API returned, so
|
|
178
|
+
you can cache the full response and derive new columns later without paying for
|
|
179
|
+
another lookup:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
response = client.geocode("1109 N Highland St, Arlington VA")
|
|
183
|
+
|
|
184
|
+
response.raw # the full JSON payload, exactly as returned
|
|
185
|
+
response.to_dict() # a deep copy of the same payload
|
|
186
|
+
|
|
187
|
+
result = response.results[0]
|
|
188
|
+
result.raw # the JSON object for this result
|
|
189
|
+
result.match_type # "rooftop", "unit", "building_centroid" or None
|
|
190
|
+
result.address_lines # ["1109 N Highland St", "", "Arlington, VA 22201"]
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Rate limits
|
|
194
|
+
|
|
195
|
+
The `X-RateLimit-*` response headers are parsed onto every geocoding response,
|
|
196
|
+
and the most recent values are kept on the client (including for requests that
|
|
197
|
+
raise):
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
response = client.geocode("1109 N Highland St, Arlington VA")
|
|
201
|
+
|
|
202
|
+
response.rate_limit.limit # 1000
|
|
203
|
+
response.rate_limit.remaining # 999
|
|
204
|
+
response.rate_limit.period # 60 (seconds), when sent by the API
|
|
205
|
+
response.rate_limit.reset # unix timestamp, when sent by the API
|
|
206
|
+
|
|
207
|
+
client.rate_limit # the most recent rate limit state seen
|
|
208
|
+
```
|
|
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
|
+
|
|
152
253
|
### Address components
|
|
153
254
|
|
|
154
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:
|
|
@@ -114,6 +114,107 @@ response = client.geocode(
|
|
|
114
114
|
)
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
#### Census appends
|
|
118
|
+
|
|
119
|
+
Census data is keyed by vintage. `fields.census` gives you the append you
|
|
120
|
+
requested, and `fields.get_census(year)` picks a specific vintage when you
|
|
121
|
+
requested more than one:
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
response = client.geocode("1109 N Highland St, Arlington VA", fields=["census2023"])
|
|
125
|
+
census = response.results[0].fields.census
|
|
126
|
+
|
|
127
|
+
print(census.full_fips) # "510131018012004"
|
|
128
|
+
print(census.census_year) # 2023
|
|
129
|
+
|
|
130
|
+
# When several vintages were requested
|
|
131
|
+
fields = response.results[0].fields
|
|
132
|
+
fields.get_census(2023) # also accepts "2023" or "census2023"
|
|
133
|
+
fields.census_years # [2023]
|
|
134
|
+
fields.census_data # {"census2023": CensusData(...)}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
`fields.census` returns the most recent vintage present. Accessing a vintage
|
|
138
|
+
directly (`fields.census2023`) continues to work.
|
|
139
|
+
|
|
140
|
+
### Raw API responses
|
|
141
|
+
|
|
142
|
+
Every geocoding response keeps the untouched JSON payload the API returned, so
|
|
143
|
+
you can cache the full response and derive new columns later without paying for
|
|
144
|
+
another lookup:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
response = client.geocode("1109 N Highland St, Arlington VA")
|
|
148
|
+
|
|
149
|
+
response.raw # the full JSON payload, exactly as returned
|
|
150
|
+
response.to_dict() # a deep copy of the same payload
|
|
151
|
+
|
|
152
|
+
result = response.results[0]
|
|
153
|
+
result.raw # the JSON object for this result
|
|
154
|
+
result.match_type # "rooftop", "unit", "building_centroid" or None
|
|
155
|
+
result.address_lines # ["1109 N Highland St", "", "Arlington, VA 22201"]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Rate limits
|
|
159
|
+
|
|
160
|
+
The `X-RateLimit-*` response headers are parsed onto every geocoding response,
|
|
161
|
+
and the most recent values are kept on the client (including for requests that
|
|
162
|
+
raise):
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
response = client.geocode("1109 N Highland St, Arlington VA")
|
|
166
|
+
|
|
167
|
+
response.rate_limit.limit # 1000
|
|
168
|
+
response.rate_limit.remaining # 999
|
|
169
|
+
response.rate_limit.period # 60 (seconds), when sent by the API
|
|
170
|
+
response.rate_limit.reset # unix timestamp, when sent by the API
|
|
171
|
+
|
|
172
|
+
client.rate_limit # the most recent rate limit state seen
|
|
173
|
+
```
|
|
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
|
+
|
|
117
218
|
### Address components
|
|
118
219
|
|
|
119
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:
|
|
@@ -58,6 +58,7 @@ from geocodio.models import (
|
|
|
58
58
|
Location,
|
|
59
59
|
PaginatedResponse,
|
|
60
60
|
ProvincialRiding,
|
|
61
|
+
RateLimit,
|
|
61
62
|
SchoolDistrict,
|
|
62
63
|
Social,
|
|
63
64
|
StateLegislativeDistrict,
|
|
@@ -65,6 +66,7 @@ from geocodio.models import (
|
|
|
65
66
|
Timezone,
|
|
66
67
|
UKLegislativeDistrict,
|
|
67
68
|
ZIP4Data,
|
|
69
|
+
parse_warnings,
|
|
68
70
|
)
|
|
69
71
|
|
|
70
72
|
|
|
@@ -117,6 +119,7 @@ class Geocodio:
|
|
|
117
119
|
self._http = httpx.Client(
|
|
118
120
|
base_url=f"https://{self.hostname}", verify=verify_ssl
|
|
119
121
|
)
|
|
122
|
+
self.rate_limit: Optional[RateLimit] = None
|
|
120
123
|
|
|
121
124
|
# ──────────────────────────────────────────────────────────────────────────
|
|
122
125
|
# Public methods
|
|
@@ -226,7 +229,7 @@ class Geocodio:
|
|
|
226
229
|
response = self._request(
|
|
227
230
|
"POST" if data else "GET", endpoint, params, json=data, timeout=timeout
|
|
228
231
|
)
|
|
229
|
-
return self._parse_geocoding_response(response.json())
|
|
232
|
+
return self._parse_geocoding_response(response.json(), response=response)
|
|
230
233
|
|
|
231
234
|
def reverse(
|
|
232
235
|
self,
|
|
@@ -306,7 +309,7 @@ class Geocodio:
|
|
|
306
309
|
response = self._request(
|
|
307
310
|
"POST" if data else "GET", endpoint, params, json=data, timeout=timeout
|
|
308
311
|
)
|
|
309
|
-
return self._parse_geocoding_response(response.json())
|
|
312
|
+
return self._parse_geocoding_response(response.json(), response=response)
|
|
310
313
|
|
|
311
314
|
# ──────────────────────────────────────────────────────────────────────────
|
|
312
315
|
# Internal helpers
|
|
@@ -350,6 +353,12 @@ class Geocodio:
|
|
|
350
353
|
logger.debug(f"Response headers: {resp.headers}")
|
|
351
354
|
logger.debug(f"Response body: {resp.content}")
|
|
352
355
|
|
|
356
|
+
# Keep the most recent rate limit state available to callers, including
|
|
357
|
+
# for responses that raise below.
|
|
358
|
+
rate_limit = RateLimit.from_headers(resp.headers)
|
|
359
|
+
if rate_limit is not None:
|
|
360
|
+
self.rate_limit = rate_limit
|
|
361
|
+
|
|
353
362
|
resp = self._handle_error_response(resp)
|
|
354
363
|
|
|
355
364
|
return resp
|
|
@@ -362,17 +371,30 @@ class Geocodio:
|
|
|
362
371
|
exception_mappings = self.get_status_exception_mappings()
|
|
363
372
|
# dump the type and content of the exception mappings for debugging
|
|
364
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
|
+
|
|
365
380
|
if resp.status_code in exception_mappings:
|
|
366
381
|
exception_class = exception_mappings[resp.status_code]
|
|
367
|
-
raise exception_class(resp.text)
|
|
382
|
+
raise exception_class(resp.text, warnings=warnings)
|
|
368
383
|
else:
|
|
369
384
|
raise GeocodioServerError(
|
|
370
|
-
f"Unrecognized status code {resp.status_code}: {resp.text}"
|
|
385
|
+
f"Unrecognized status code {resp.status_code}: {resp.text}",
|
|
386
|
+
warnings=warnings,
|
|
371
387
|
)
|
|
372
388
|
|
|
373
|
-
def _parse_geocoding_response(
|
|
389
|
+
def _parse_geocoding_response(
|
|
390
|
+
self,
|
|
391
|
+
response_json: dict,
|
|
392
|
+
response: Optional[httpx.Response] = None,
|
|
393
|
+
) -> GeocodingResponse:
|
|
374
394
|
logger.debug(f"Raw response: {response_json}")
|
|
375
395
|
|
|
396
|
+
rate_limit = RateLimit.from_headers(response.headers) if response else None
|
|
397
|
+
|
|
376
398
|
# Handle batch response format
|
|
377
399
|
if (
|
|
378
400
|
"results" in response_json
|
|
@@ -381,9 +403,14 @@ class Geocodio:
|
|
|
381
403
|
and "response" in response_json["results"][0]
|
|
382
404
|
):
|
|
383
405
|
results = []
|
|
406
|
+
batch_warnings: List[str] = parse_warnings(response_json)
|
|
384
407
|
for res in response_json["results"]:
|
|
385
408
|
query = res.get("query", "")
|
|
386
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
|
+
)
|
|
387
414
|
|
|
388
415
|
# Unmatched query (e.g. an unparseable address): keep an entry
|
|
389
416
|
# so the result list stays aligned with the submitted addresses,
|
|
@@ -398,6 +425,7 @@ class Geocodio:
|
|
|
398
425
|
accuracy_type="",
|
|
399
426
|
source="",
|
|
400
427
|
query=query,
|
|
428
|
+
warnings=query_warnings,
|
|
401
429
|
)
|
|
402
430
|
)
|
|
403
431
|
continue
|
|
@@ -416,9 +444,18 @@ class Geocodio:
|
|
|
416
444
|
query=query,
|
|
417
445
|
fields=self._parse_fields(top.get("fields")),
|
|
418
446
|
stable_address_key=top.get("stable_address_key"),
|
|
447
|
+
match_type=top.get("match_type"),
|
|
448
|
+
address_lines=top.get("address_lines"),
|
|
449
|
+
raw=top,
|
|
450
|
+
warnings=query_warnings + parse_warnings(top),
|
|
419
451
|
)
|
|
420
452
|
)
|
|
421
|
-
return GeocodingResponse(
|
|
453
|
+
return GeocodingResponse(
|
|
454
|
+
results=results,
|
|
455
|
+
raw=response_json,
|
|
456
|
+
rate_limit=rate_limit,
|
|
457
|
+
warnings=batch_warnings,
|
|
458
|
+
)
|
|
422
459
|
|
|
423
460
|
# Handle single response format
|
|
424
461
|
results = [
|
|
@@ -433,10 +470,19 @@ class Geocodio:
|
|
|
433
470
|
source=res.get("source", ""),
|
|
434
471
|
fields=self._parse_fields(res.get("fields")),
|
|
435
472
|
stable_address_key=res.get("stable_address_key"),
|
|
473
|
+
match_type=res.get("match_type"),
|
|
474
|
+
address_lines=res.get("address_lines"),
|
|
475
|
+
raw=res,
|
|
476
|
+
warnings=parse_warnings(res),
|
|
436
477
|
)
|
|
437
478
|
for res in response_json.get("results", [])
|
|
438
479
|
]
|
|
439
|
-
return GeocodingResponse(
|
|
480
|
+
return GeocodingResponse(
|
|
481
|
+
results=results,
|
|
482
|
+
raw=response_json,
|
|
483
|
+
rate_limit=rate_limit,
|
|
484
|
+
warnings=parse_warnings(response_json),
|
|
485
|
+
)
|
|
440
486
|
|
|
441
487
|
# ──────────────────────────────────────────────────────────────────────────
|
|
442
488
|
# List API methods
|
|
@@ -542,6 +588,7 @@ class Geocodio:
|
|
|
542
588
|
first_page_url=pagination_info.get("first_page_url"),
|
|
543
589
|
next_page_url=pagination_info.get("next_page_url"),
|
|
544
590
|
prev_page_url=pagination_info.get("prev_page_url"),
|
|
591
|
+
warnings=parse_warnings(pagination_info),
|
|
545
592
|
)
|
|
546
593
|
|
|
547
594
|
def get_list(self, list_id: str) -> ListResponse:
|
|
@@ -593,6 +640,7 @@ class Geocodio:
|
|
|
593
640
|
download_url=response_json.get("download_url"),
|
|
594
641
|
expires_at=response_json.get("expires_at"),
|
|
595
642
|
http_response=response,
|
|
643
|
+
warnings=parse_warnings(response_json),
|
|
596
644
|
)
|
|
597
645
|
|
|
598
646
|
@staticmethod
|
|
@@ -1341,6 +1389,7 @@ class Geocodio:
|
|
|
1341
1389
|
first_page_url=pagination_info.get("first_page_url"),
|
|
1342
1390
|
next_page_url=pagination_info.get("next_page_url"),
|
|
1343
1391
|
prev_page_url=pagination_info.get("prev_page_url"),
|
|
1392
|
+
warnings=parse_warnings(pagination_info),
|
|
1344
1393
|
)
|
|
1345
1394
|
|
|
1346
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
|
|