geocodio-library-python 1.3.0__tar.gz → 1.4.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.4.0}/CHANGELOG.md +8 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/PKG-INFO +59 -1
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/README.md +58 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/pyproject.toml +1 -1
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/src/geocodio/_version.py +1 -1
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/src/geocodio/client.py +29 -5
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/src/geocodio/models.py +129 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/e2e/test_api.py +56 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/test_geocode.py +213 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/test_models.py +107 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/uv.lock +1 -1
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/.github/workflows/ci.yml +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/.github/workflows/publish.yml +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/.github/workflows/test-act-event-ci.json +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/.github/workflows/test-act-event-publish.json +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/.gitignore +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/LICENSE +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/requirements-dev.txt +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/setup.cfg +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/smoke.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/smoke_lists.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/src/geocodio/__init__.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/src/geocodio/distance.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/src/geocodio/exceptions.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/test_pypi_release.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/__init__.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/conftest.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/e2e/__init__.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/e2e/test_batch_reverse.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/e2e/test_distance.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/e2e/test_lists_api.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/test_workflows.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/__init__.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/test_client.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/test_distance.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/test_errors.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/test_exceptions.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/test_geocoding.py +0 -0
- {geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/test_reverse.py +0 -0
|
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.4.0] - 2026-08-28
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- 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.
|
|
14
|
+
- `GeocodingResult.match_type` and `GeocodingResult.address_lines`, both of which the API returns on every result and the model previously dropped.
|
|
15
|
+
- 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.
|
|
16
|
+
- 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).
|
|
17
|
+
|
|
10
18
|
## [1.3.0] - 2026-08-25
|
|
11
19
|
|
|
12
20
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: geocodio-library-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.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,64 @@ 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
|
+
|
|
152
210
|
### Address components
|
|
153
211
|
|
|
154
212
|
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,64 @@ 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
|
+
|
|
117
175
|
### Address components
|
|
118
176
|
|
|
119
177
|
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,
|
|
@@ -117,6 +118,7 @@ class Geocodio:
|
|
|
117
118
|
self._http = httpx.Client(
|
|
118
119
|
base_url=f"https://{self.hostname}", verify=verify_ssl
|
|
119
120
|
)
|
|
121
|
+
self.rate_limit: Optional[RateLimit] = None
|
|
120
122
|
|
|
121
123
|
# ──────────────────────────────────────────────────────────────────────────
|
|
122
124
|
# Public methods
|
|
@@ -226,7 +228,7 @@ class Geocodio:
|
|
|
226
228
|
response = self._request(
|
|
227
229
|
"POST" if data else "GET", endpoint, params, json=data, timeout=timeout
|
|
228
230
|
)
|
|
229
|
-
return self._parse_geocoding_response(response.json())
|
|
231
|
+
return self._parse_geocoding_response(response.json(), response=response)
|
|
230
232
|
|
|
231
233
|
def reverse(
|
|
232
234
|
self,
|
|
@@ -306,7 +308,7 @@ class Geocodio:
|
|
|
306
308
|
response = self._request(
|
|
307
309
|
"POST" if data else "GET", endpoint, params, json=data, timeout=timeout
|
|
308
310
|
)
|
|
309
|
-
return self._parse_geocoding_response(response.json())
|
|
311
|
+
return self._parse_geocoding_response(response.json(), response=response)
|
|
310
312
|
|
|
311
313
|
# ──────────────────────────────────────────────────────────────────────────
|
|
312
314
|
# Internal helpers
|
|
@@ -350,6 +352,12 @@ class Geocodio:
|
|
|
350
352
|
logger.debug(f"Response headers: {resp.headers}")
|
|
351
353
|
logger.debug(f"Response body: {resp.content}")
|
|
352
354
|
|
|
355
|
+
# Keep the most recent rate limit state available to callers, including
|
|
356
|
+
# for responses that raise below.
|
|
357
|
+
rate_limit = RateLimit.from_headers(resp.headers)
|
|
358
|
+
if rate_limit is not None:
|
|
359
|
+
self.rate_limit = rate_limit
|
|
360
|
+
|
|
353
361
|
resp = self._handle_error_response(resp)
|
|
354
362
|
|
|
355
363
|
return resp
|
|
@@ -370,9 +378,15 @@ class Geocodio:
|
|
|
370
378
|
f"Unrecognized status code {resp.status_code}: {resp.text}"
|
|
371
379
|
)
|
|
372
380
|
|
|
373
|
-
def _parse_geocoding_response(
|
|
381
|
+
def _parse_geocoding_response(
|
|
382
|
+
self,
|
|
383
|
+
response_json: dict,
|
|
384
|
+
response: Optional[httpx.Response] = None,
|
|
385
|
+
) -> GeocodingResponse:
|
|
374
386
|
logger.debug(f"Raw response: {response_json}")
|
|
375
387
|
|
|
388
|
+
rate_limit = RateLimit.from_headers(response.headers) if response else None
|
|
389
|
+
|
|
376
390
|
# Handle batch response format
|
|
377
391
|
if (
|
|
378
392
|
"results" in response_json
|
|
@@ -416,9 +430,14 @@ class Geocodio:
|
|
|
416
430
|
query=query,
|
|
417
431
|
fields=self._parse_fields(top.get("fields")),
|
|
418
432
|
stable_address_key=top.get("stable_address_key"),
|
|
433
|
+
match_type=top.get("match_type"),
|
|
434
|
+
address_lines=top.get("address_lines"),
|
|
435
|
+
raw=top,
|
|
419
436
|
)
|
|
420
437
|
)
|
|
421
|
-
return GeocodingResponse(
|
|
438
|
+
return GeocodingResponse(
|
|
439
|
+
results=results, raw=response_json, rate_limit=rate_limit
|
|
440
|
+
)
|
|
422
441
|
|
|
423
442
|
# Handle single response format
|
|
424
443
|
results = [
|
|
@@ -433,10 +452,15 @@ class Geocodio:
|
|
|
433
452
|
source=res.get("source", ""),
|
|
434
453
|
fields=self._parse_fields(res.get("fields")),
|
|
435
454
|
stable_address_key=res.get("stable_address_key"),
|
|
455
|
+
match_type=res.get("match_type"),
|
|
456
|
+
address_lines=res.get("address_lines"),
|
|
457
|
+
raw=res,
|
|
436
458
|
)
|
|
437
459
|
for res in response_json.get("results", [])
|
|
438
460
|
]
|
|
439
|
-
return GeocodingResponse(
|
|
461
|
+
return GeocodingResponse(
|
|
462
|
+
results=results, raw=response_json, rate_limit=rate_limit
|
|
463
|
+
)
|
|
440
464
|
|
|
441
465
|
# ──────────────────────────────────────────────────────────────────────────
|
|
442
466
|
# List API methods
|
|
@@ -5,6 +5,7 @@ Dataclass representations of Geocodio API responses and related objects.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import copy
|
|
8
9
|
from dataclasses import dataclass, field
|
|
9
10
|
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar
|
|
10
11
|
|
|
@@ -398,6 +399,48 @@ class GeocodioFields:
|
|
|
398
399
|
# Internal storage for census data (all years dynamically accessible)
|
|
399
400
|
_census: Dict[str, CensusData] = field(default_factory=dict, repr=False)
|
|
400
401
|
|
|
402
|
+
@property
|
|
403
|
+
def census(self) -> Optional[CensusData]:
|
|
404
|
+
"""
|
|
405
|
+
The census append, exposed like every other append on this object.
|
|
406
|
+
|
|
407
|
+
Geocodio keys census data by vintage (``census2020``, ``census2023``,
|
|
408
|
+
…). Requesting a single census field – the common case – makes
|
|
409
|
+
``fields.census`` that field. When several vintages are present the
|
|
410
|
+
most recent one is returned; use :meth:`get_census` to pick one.
|
|
411
|
+
"""
|
|
412
|
+
return self.get_census()
|
|
413
|
+
|
|
414
|
+
@property
|
|
415
|
+
def census_years(self) -> List[int]:
|
|
416
|
+
"""Census vintages present on this result, oldest first."""
|
|
417
|
+
return sorted(int(name[6:]) for name in self._census if name[6:].isdigit())
|
|
418
|
+
|
|
419
|
+
@property
|
|
420
|
+
def census_data(self) -> Dict[str, CensusData]:
|
|
421
|
+
"""All census vintages, keyed as requested (``{"census2023": ...}``)."""
|
|
422
|
+
return dict(self._census)
|
|
423
|
+
|
|
424
|
+
def get_census(self, year: Optional[Any] = None) -> Optional[CensusData]:
|
|
425
|
+
"""
|
|
426
|
+
Return the census append for ``year``.
|
|
427
|
+
|
|
428
|
+
``year`` accepts the vintage in any form the API or a caller might use
|
|
429
|
+
– ``2023``, ``"2023"`` or ``"census2023"``. Omit it to get the most
|
|
430
|
+
recent vintage on this result. Returns ``None`` when the requested
|
|
431
|
+
vintage was not appended.
|
|
432
|
+
"""
|
|
433
|
+
if year is None:
|
|
434
|
+
years = self.census_years
|
|
435
|
+
if not years:
|
|
436
|
+
return None
|
|
437
|
+
return self._census.get(f"census{years[-1]}")
|
|
438
|
+
|
|
439
|
+
key = str(year)
|
|
440
|
+
if not key.startswith("census"):
|
|
441
|
+
key = f"census{key}"
|
|
442
|
+
return self._census.get(key)
|
|
443
|
+
|
|
401
444
|
def __getattr__(self, name: str):
|
|
402
445
|
"""
|
|
403
446
|
Dynamic attribute access for census years (census2020, census2025, etc.).
|
|
@@ -419,6 +462,62 @@ class GeocodioFields:
|
|
|
419
462
|
)
|
|
420
463
|
|
|
421
464
|
|
|
465
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
466
|
+
# Rate limiting
|
|
467
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@dataclass(slots=True, frozen=True)
|
|
471
|
+
class RateLimit:
|
|
472
|
+
"""
|
|
473
|
+
Rate limit state as reported by the ``X-RateLimit-*`` response headers.
|
|
474
|
+
|
|
475
|
+
Attributes:
|
|
476
|
+
limit: Requests allowed in the current window.
|
|
477
|
+
remaining: Requests left in the current window.
|
|
478
|
+
reset: Unix timestamp when the window resets, when the API sends it.
|
|
479
|
+
period: Length of the window in seconds, when the API sends it.
|
|
480
|
+
headers: The raw ``x-ratelimit-*`` headers, lowercased.
|
|
481
|
+
"""
|
|
482
|
+
|
|
483
|
+
limit: Optional[int] = None
|
|
484
|
+
remaining: Optional[int] = None
|
|
485
|
+
reset: Optional[int] = None
|
|
486
|
+
period: Optional[int] = None
|
|
487
|
+
headers: Dict[str, str] = field(default_factory=dict, repr=False)
|
|
488
|
+
|
|
489
|
+
@classmethod
|
|
490
|
+
def from_headers(cls, headers: Any) -> Optional["RateLimit"]:
|
|
491
|
+
"""
|
|
492
|
+
Build a RateLimit from response headers, or None when the response
|
|
493
|
+
carries no rate limit information.
|
|
494
|
+
"""
|
|
495
|
+
if headers is None:
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
raw = {
|
|
499
|
+
key.lower(): value
|
|
500
|
+
for key, value in headers.items()
|
|
501
|
+
if key.lower().startswith("x-ratelimit-")
|
|
502
|
+
}
|
|
503
|
+
if not raw:
|
|
504
|
+
return None
|
|
505
|
+
|
|
506
|
+
def as_int(name: str) -> Optional[int]:
|
|
507
|
+
try:
|
|
508
|
+
return int(raw[f"x-ratelimit-{name}"])
|
|
509
|
+
except (KeyError, TypeError, ValueError):
|
|
510
|
+
return None
|
|
511
|
+
|
|
512
|
+
return cls(
|
|
513
|
+
limit=as_int("limit"),
|
|
514
|
+
remaining=as_int("remaining"),
|
|
515
|
+
reset=as_int("reset"),
|
|
516
|
+
period=as_int("period"),
|
|
517
|
+
headers=raw,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
|
|
422
521
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
423
522
|
# Distance API models
|
|
424
523
|
# ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -691,20 +790,50 @@ class GeocodingResult:
|
|
|
691
790
|
fields: Optional[GeocodioFields] = None
|
|
692
791
|
query: str = ""
|
|
693
792
|
stable_address_key: Optional[str] = None
|
|
793
|
+
match_type: Optional[str] = None
|
|
794
|
+
address_lines: Optional[List[str]] = None
|
|
795
|
+
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
694
796
|
|
|
695
797
|
@property
|
|
696
798
|
def matched(self) -> bool:
|
|
697
799
|
"""True when the API returned coordinates for this query."""
|
|
698
800
|
return self.location is not None
|
|
699
801
|
|
|
802
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
803
|
+
"""
|
|
804
|
+
The untouched JSON object the API returned for this result.
|
|
805
|
+
|
|
806
|
+
Unlike ``dataclasses.asdict()`` this keeps every key the API sent,
|
|
807
|
+
including any the models do not (yet) map. Empty for a query the API
|
|
808
|
+
returned no match for – the full payload is still on
|
|
809
|
+
``GeocodingResponse.raw``.
|
|
810
|
+
"""
|
|
811
|
+
return copy.deepcopy(self.raw)
|
|
812
|
+
|
|
700
813
|
|
|
701
814
|
@dataclass(slots=True, frozen=True)
|
|
702
815
|
class GeocodingResponse:
|
|
703
816
|
"""
|
|
704
817
|
Top‑level structure returned by client.geocode() / client.reverse().
|
|
818
|
+
|
|
819
|
+
Attributes:
|
|
820
|
+
results: Flat list of results, one per submitted query.
|
|
821
|
+
raw: The untouched JSON payload as returned by the API.
|
|
822
|
+
rate_limit: Rate limit state from the response headers, when present.
|
|
705
823
|
"""
|
|
706
824
|
|
|
707
825
|
results: List[GeocodingResult] = field(default_factory=list)
|
|
826
|
+
raw: Dict[str, Any] = field(default_factory=dict, repr=False)
|
|
827
|
+
rate_limit: Optional[RateLimit] = None
|
|
828
|
+
|
|
829
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
830
|
+
"""
|
|
831
|
+
The untouched JSON payload as returned by the API.
|
|
832
|
+
|
|
833
|
+
Cache this to avoid re-fetching (and re-paying for) a lookup when a
|
|
834
|
+
new derived column is needed later.
|
|
835
|
+
"""
|
|
836
|
+
return copy.deepcopy(self.raw)
|
|
708
837
|
|
|
709
838
|
|
|
710
839
|
@dataclass(slots=True, frozen=True)
|
|
@@ -700,3 +700,59 @@ def test_integration_with_congressional_district_variants(client):
|
|
|
700
700
|
assert district.congress_number is not None
|
|
701
701
|
if district.ocd_id:
|
|
702
702
|
assert isinstance(district.ocd_id, str)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
def test_integration_census_public_accessor(client):
|
|
706
|
+
"""Census appends are reachable without touching private attributes."""
|
|
707
|
+
response = client.geocode(
|
|
708
|
+
"1109 N Highland St, Arlington, VA", fields=["census2023"]
|
|
709
|
+
)
|
|
710
|
+
fields = response.results[0].fields
|
|
711
|
+
|
|
712
|
+
assert fields is not None
|
|
713
|
+
assert fields.census is not None
|
|
714
|
+
assert fields.census.census_year == 2023
|
|
715
|
+
assert fields.census.full_fips is not None
|
|
716
|
+
assert fields.census_years == [2023]
|
|
717
|
+
assert fields.get_census(2023) is fields.census
|
|
718
|
+
|
|
719
|
+
# Backward compatible access paths still work
|
|
720
|
+
assert fields.census2023 is fields.census
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def test_integration_match_type_and_address_lines(client):
|
|
724
|
+
"""match_type and address_lines are carried through from the API."""
|
|
725
|
+
response = client.geocode("1109 N Highland St, Arlington, VA")
|
|
726
|
+
result = response.results[0]
|
|
727
|
+
|
|
728
|
+
assert result.address_lines is not None
|
|
729
|
+
assert result.address_lines[0] == "1109 N Highland St"
|
|
730
|
+
# match_type is nullable, but the attribute must exist and match the payload
|
|
731
|
+
assert result.match_type == result.raw.get("match_type")
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def test_integration_raw_payload(client):
|
|
735
|
+
"""The untouched API payload is available on the response and results."""
|
|
736
|
+
response = client.geocode(
|
|
737
|
+
"1109 N Highland St, Arlington, VA", fields=["census2023"]
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
assert "results" in response.raw
|
|
741
|
+
raw_result = response.raw["results"][0]
|
|
742
|
+
assert response.results[0].raw == raw_result
|
|
743
|
+
|
|
744
|
+
# Nothing the API sent is dropped from the raw payload
|
|
745
|
+
assert "match_type" in raw_result
|
|
746
|
+
assert "address_lines" in raw_result
|
|
747
|
+
assert raw_result["fields"]["census"]["2023"]["full_fips"] is not None
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def test_integration_rate_limit_headers(client):
|
|
751
|
+
"""Rate limit headers are exposed on the response and the client."""
|
|
752
|
+
response = client.geocode("1109 N Highland St, Arlington, VA")
|
|
753
|
+
|
|
754
|
+
assert response.rate_limit is not None
|
|
755
|
+
assert response.rate_limit.limit is not None
|
|
756
|
+
assert response.rate_limit.remaining is not None
|
|
757
|
+
assert response.rate_limit.limit >= response.rate_limit.remaining
|
|
758
|
+
assert client.rate_limit == response.rate_limit
|
|
@@ -915,6 +915,7 @@ def test_geocode_batch_with_unmatched_address(client, httpx_mock):
|
|
|
915
915
|
assert unmatched.query == "qwertyuiop asdfghjkl zxcvbnm"
|
|
916
916
|
assert unmatched.formatted_address == ""
|
|
917
917
|
|
|
918
|
+
|
|
918
919
|
def test_geocode_batch_exposes_stable_address_key(client, httpx_mock):
|
|
919
920
|
"""stable_address_key is parsed from the nested batch response format."""
|
|
920
921
|
addresses = ["1109 N Highland St, Arlington, VA"]
|
|
@@ -1003,3 +1004,215 @@ def test_geocode_stable_address_key_absent_is_none(client, httpx_mock):
|
|
|
1003
1004
|
resp = client.geocode("1109 N Highland St, Arlington, VA")
|
|
1004
1005
|
|
|
1005
1006
|
assert resp.results[0].stable_address_key is None
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def test_geocode_nested_census_public_accessor(client, httpx_mock):
|
|
1010
|
+
"""The nested census structure is reachable via the public accessor."""
|
|
1011
|
+
|
|
1012
|
+
def response_callback(request):
|
|
1013
|
+
assert request.url.params["fields"] == "census2023"
|
|
1014
|
+
return httpx.Response(
|
|
1015
|
+
200,
|
|
1016
|
+
json={
|
|
1017
|
+
"results": [
|
|
1018
|
+
{
|
|
1019
|
+
"address_components": {
|
|
1020
|
+
"number": "1109",
|
|
1021
|
+
"predirectional": "N",
|
|
1022
|
+
"street": "Highland",
|
|
1023
|
+
"suffix": "St",
|
|
1024
|
+
"city": "Arlington",
|
|
1025
|
+
"state_province": "VA",
|
|
1026
|
+
"postal_code": "22201",
|
|
1027
|
+
"country": "US",
|
|
1028
|
+
},
|
|
1029
|
+
"formatted_address": "1109 N Highland St, Arlington, VA 22201",
|
|
1030
|
+
"location": {"lat": 38.886672, "lng": -77.094735},
|
|
1031
|
+
"accuracy": 1,
|
|
1032
|
+
"accuracy_type": "rooftop",
|
|
1033
|
+
"source": "Arlington",
|
|
1034
|
+
"fields": {
|
|
1035
|
+
"census": {
|
|
1036
|
+
"2023": {
|
|
1037
|
+
"census_year": 2023,
|
|
1038
|
+
"state_fips": "51",
|
|
1039
|
+
"county_fips": "51013",
|
|
1040
|
+
"tract_code": "101801",
|
|
1041
|
+
"block_code": "2004",
|
|
1042
|
+
"block_group": "2",
|
|
1043
|
+
"full_fips": "510131018012004",
|
|
1044
|
+
"source": "US Census Bureau",
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
},
|
|
1048
|
+
}
|
|
1049
|
+
]
|
|
1050
|
+
},
|
|
1051
|
+
)
|
|
1052
|
+
|
|
1053
|
+
httpx_mock.add_callback(
|
|
1054
|
+
callback=response_callback,
|
|
1055
|
+
url=httpx.URL(
|
|
1056
|
+
"https://api.test/v2/geocode",
|
|
1057
|
+
params={
|
|
1058
|
+
"q": "1109 N Highland St, Arlington, VA",
|
|
1059
|
+
"fields": "census2023",
|
|
1060
|
+
},
|
|
1061
|
+
),
|
|
1062
|
+
match_headers={"Authorization": "Bearer TEST_KEY"},
|
|
1063
|
+
)
|
|
1064
|
+
|
|
1065
|
+
resp = client.geocode("1109 N Highland St, Arlington, VA", fields=["census2023"])
|
|
1066
|
+
fields = resp.results[0].fields
|
|
1067
|
+
|
|
1068
|
+
# New public accessor
|
|
1069
|
+
assert fields.census is not None
|
|
1070
|
+
assert fields.census.full_fips == "510131018012004"
|
|
1071
|
+
assert fields.census.census_year == 2023
|
|
1072
|
+
assert fields.get_census(2023).full_fips == "510131018012004"
|
|
1073
|
+
assert fields.census_years == [2023]
|
|
1074
|
+
|
|
1075
|
+
# Legacy field name mapping still applies through the accessor
|
|
1076
|
+
assert fields.census.tract == "101801"
|
|
1077
|
+
assert fields.census.block == "2004"
|
|
1078
|
+
|
|
1079
|
+
# Backward compatibility
|
|
1080
|
+
assert fields.census2023.full_fips == "510131018012004"
|
|
1081
|
+
assert fields._census["census2023"].full_fips == "510131018012004"
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def test_geocode_exposes_match_type_address_lines_and_raw(client, httpx_mock):
|
|
1085
|
+
"""match_type, address_lines and the raw payload survive parsing."""
|
|
1086
|
+
payload = {
|
|
1087
|
+
"results": [
|
|
1088
|
+
{
|
|
1089
|
+
"stable_address_key": "gcod_abc123",
|
|
1090
|
+
"address_components": {
|
|
1091
|
+
"number": "1109",
|
|
1092
|
+
"predirectional": "N",
|
|
1093
|
+
"street": "Highland",
|
|
1094
|
+
"suffix": "St",
|
|
1095
|
+
"city": "Arlington",
|
|
1096
|
+
"state_province": "VA",
|
|
1097
|
+
"postal_code": "22201",
|
|
1098
|
+
"country": "US",
|
|
1099
|
+
},
|
|
1100
|
+
"address_lines": [
|
|
1101
|
+
"1109 N Highland St",
|
|
1102
|
+
"",
|
|
1103
|
+
"Arlington, VA 22201",
|
|
1104
|
+
],
|
|
1105
|
+
"formatted_address": "1109 N Highland St, Arlington, VA 22201",
|
|
1106
|
+
"location": {"lat": 38.886672, "lng": -77.094735},
|
|
1107
|
+
"accuracy": 1,
|
|
1108
|
+
"accuracy_type": "rooftop",
|
|
1109
|
+
"match_type": "building_centroid",
|
|
1110
|
+
"source": "Arlington",
|
|
1111
|
+
"some_future_key": {"not": "modelled"},
|
|
1112
|
+
}
|
|
1113
|
+
]
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
httpx_mock.add_callback(
|
|
1117
|
+
callback=lambda request: httpx.Response(
|
|
1118
|
+
200,
|
|
1119
|
+
json=payload,
|
|
1120
|
+
headers={
|
|
1121
|
+
"X-RateLimit-Limit": "1000",
|
|
1122
|
+
"X-RateLimit-Remaining": "998",
|
|
1123
|
+
"X-RateLimit-Period": "60",
|
|
1124
|
+
},
|
|
1125
|
+
),
|
|
1126
|
+
url=httpx.URL(
|
|
1127
|
+
"https://api.test/v2/geocode",
|
|
1128
|
+
params={"q": "1109 N Highland St, Arlington, VA"},
|
|
1129
|
+
),
|
|
1130
|
+
match_headers={"Authorization": "Bearer TEST_KEY"},
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
resp = client.geocode("1109 N Highland St, Arlington, VA")
|
|
1134
|
+
result = resp.results[0]
|
|
1135
|
+
|
|
1136
|
+
assert result.match_type == "building_centroid"
|
|
1137
|
+
assert result.address_lines == [
|
|
1138
|
+
"1109 N Highland St",
|
|
1139
|
+
"",
|
|
1140
|
+
"Arlington, VA 22201",
|
|
1141
|
+
]
|
|
1142
|
+
|
|
1143
|
+
# Raw payloads keep everything, including keys the models do not map
|
|
1144
|
+
assert resp.raw == payload
|
|
1145
|
+
assert resp.to_dict() == payload
|
|
1146
|
+
assert result.raw == payload["results"][0]
|
|
1147
|
+
assert result.to_dict()["some_future_key"] == {"not": "modelled"}
|
|
1148
|
+
|
|
1149
|
+
# Rate limit headers are exposed on the response and the client
|
|
1150
|
+
assert resp.rate_limit is not None
|
|
1151
|
+
assert resp.rate_limit.limit == 1000
|
|
1152
|
+
assert resp.rate_limit.remaining == 998
|
|
1153
|
+
assert resp.rate_limit.period == 60
|
|
1154
|
+
assert client.rate_limit == resp.rate_limit
|
|
1155
|
+
|
|
1156
|
+
|
|
1157
|
+
def test_geocode_batch_exposes_match_type_and_raw(client, httpx_mock):
|
|
1158
|
+
"""Batch results carry match_type, address_lines and their raw payload."""
|
|
1159
|
+
payload = {
|
|
1160
|
+
"results": [
|
|
1161
|
+
{
|
|
1162
|
+
"query": "1109 N Highland St, Arlington VA",
|
|
1163
|
+
"response": {
|
|
1164
|
+
"results": [
|
|
1165
|
+
{
|
|
1166
|
+
"address_components": {
|
|
1167
|
+
"number": "1109",
|
|
1168
|
+
"street": "Highland",
|
|
1169
|
+
"suffix": "St",
|
|
1170
|
+
"city": "Arlington",
|
|
1171
|
+
"state_province": "VA",
|
|
1172
|
+
"postal_code": "22201",
|
|
1173
|
+
"country": "US",
|
|
1174
|
+
},
|
|
1175
|
+
"address_lines": [
|
|
1176
|
+
"1109 N Highland St",
|
|
1177
|
+
"",
|
|
1178
|
+
"Arlington, VA 22201",
|
|
1179
|
+
],
|
|
1180
|
+
"formatted_address": (
|
|
1181
|
+
"1109 N Highland St, Arlington, VA 22201"
|
|
1182
|
+
),
|
|
1183
|
+
"location": {"lat": 38.886672, "lng": -77.094735},
|
|
1184
|
+
"accuracy": 1,
|
|
1185
|
+
"accuracy_type": "rooftop",
|
|
1186
|
+
"match_type": "building_centroid",
|
|
1187
|
+
"source": "Arlington",
|
|
1188
|
+
}
|
|
1189
|
+
]
|
|
1190
|
+
},
|
|
1191
|
+
},
|
|
1192
|
+
{
|
|
1193
|
+
"query": "zzzzzz nowhere",
|
|
1194
|
+
"response": {"results": []},
|
|
1195
|
+
},
|
|
1196
|
+
]
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
httpx_mock.add_callback(
|
|
1200
|
+
callback=lambda request: httpx.Response(200, json=payload),
|
|
1201
|
+
url=httpx.URL("https://api.test/v2/geocode"),
|
|
1202
|
+
method="POST",
|
|
1203
|
+
match_headers={"Authorization": "Bearer TEST_KEY"},
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
resp = client.geocode(["1109 N Highland St, Arlington VA", "zzzzzz nowhere"])
|
|
1207
|
+
|
|
1208
|
+
matched, unmatched = resp.results
|
|
1209
|
+
assert matched.match_type == "building_centroid"
|
|
1210
|
+
assert matched.address_lines[0] == "1109 N Highland St"
|
|
1211
|
+
assert matched.raw == payload["results"][0]["response"]["results"][0]
|
|
1212
|
+
|
|
1213
|
+
# Unmatched queries keep their slot; the full payload stays on the response
|
|
1214
|
+
assert unmatched.matched is False
|
|
1215
|
+
assert unmatched.match_type is None
|
|
1216
|
+
assert unmatched.address_lines is None
|
|
1217
|
+
assert unmatched.raw == {}
|
|
1218
|
+
assert resp.raw == payload
|
|
@@ -14,6 +14,7 @@ from geocodio.models import (
|
|
|
14
14
|
GeocodioFields,
|
|
15
15
|
Housing,
|
|
16
16
|
Location,
|
|
17
|
+
RateLimit,
|
|
17
18
|
SchoolDistrict,
|
|
18
19
|
Social,
|
|
19
20
|
StateLegislativeDistrict,
|
|
@@ -397,3 +398,109 @@ def test_ffiec_data():
|
|
|
397
398
|
data = {"extra_field": "extra value"}
|
|
398
399
|
ffiec = FFIECData.from_api(data)
|
|
399
400
|
assert ffiec.get_extra("extra_field") == "extra value"
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def test_census_accessors():
|
|
404
|
+
"""fields.census exposes the census append like every other append."""
|
|
405
|
+
fields = GeocodioFields(
|
|
406
|
+
_census={
|
|
407
|
+
"census2020": CensusData.from_api(
|
|
408
|
+
{"census_year": 2020, "full_fips": "500110960100000"}
|
|
409
|
+
),
|
|
410
|
+
"census2023": CensusData.from_api(
|
|
411
|
+
{"census_year": 2023, "full_fips": "500110960102004"}
|
|
412
|
+
),
|
|
413
|
+
}
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
# The public accessor returns the most recent vintage
|
|
417
|
+
assert fields.census is not None
|
|
418
|
+
assert fields.census.census_year == 2023
|
|
419
|
+
assert fields.census.full_fips == "500110960102004"
|
|
420
|
+
|
|
421
|
+
# A specific vintage can be requested in any form
|
|
422
|
+
assert fields.get_census(2020).census_year == 2020
|
|
423
|
+
assert fields.get_census("2020").census_year == 2020
|
|
424
|
+
assert fields.get_census("census2020").census_year == 2020
|
|
425
|
+
assert fields.get_census(1999) is None
|
|
426
|
+
|
|
427
|
+
# Supporting accessors
|
|
428
|
+
assert fields.census_years == [2020, 2023]
|
|
429
|
+
assert sorted(fields.census_data) == ["census2020", "census2023"]
|
|
430
|
+
|
|
431
|
+
# Backward compatibility: dynamic attributes and the private dict
|
|
432
|
+
assert fields.census2023.full_fips == "500110960102004"
|
|
433
|
+
assert fields._census["census2023"].full_fips == "500110960102004"
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def test_census_accessors_without_census_data():
|
|
437
|
+
"""fields.census is None when no census field was requested."""
|
|
438
|
+
fields = GeocodioFields()
|
|
439
|
+
|
|
440
|
+
assert fields.census is None
|
|
441
|
+
assert fields.get_census() is None
|
|
442
|
+
assert fields.get_census(2023) is None
|
|
443
|
+
assert fields.census_years == []
|
|
444
|
+
assert fields.census_data == {}
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def test_rate_limit_from_headers():
|
|
448
|
+
"""Rate limit state is parsed from the X-RateLimit-* headers."""
|
|
449
|
+
rate_limit = RateLimit.from_headers(
|
|
450
|
+
{
|
|
451
|
+
"Content-Type": "application/json",
|
|
452
|
+
"X-RateLimit-Limit": "1000",
|
|
453
|
+
"X-RateLimit-Remaining": "999",
|
|
454
|
+
"X-RateLimit-Period": "60",
|
|
455
|
+
}
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
assert rate_limit is not None
|
|
459
|
+
assert rate_limit.limit == 1000
|
|
460
|
+
assert rate_limit.remaining == 999
|
|
461
|
+
assert rate_limit.period == 60
|
|
462
|
+
assert rate_limit.reset is None
|
|
463
|
+
assert rate_limit.headers == {
|
|
464
|
+
"x-ratelimit-limit": "1000",
|
|
465
|
+
"x-ratelimit-remaining": "999",
|
|
466
|
+
"x-ratelimit-period": "60",
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def test_rate_limit_from_headers_without_rate_limit_headers():
|
|
471
|
+
"""No rate limit headers means no RateLimit object."""
|
|
472
|
+
assert RateLimit.from_headers({"Content-Type": "application/json"}) is None
|
|
473
|
+
assert RateLimit.from_headers(None) is None
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def test_rate_limit_from_headers_with_unparseable_values():
|
|
477
|
+
"""Non-numeric header values are ignored rather than raising."""
|
|
478
|
+
rate_limit = RateLimit.from_headers(
|
|
479
|
+
{"X-RateLimit-Limit": "unlimited", "X-RateLimit-Remaining": "5"}
|
|
480
|
+
)
|
|
481
|
+
|
|
482
|
+
assert rate_limit is not None
|
|
483
|
+
assert rate_limit.limit is None
|
|
484
|
+
assert rate_limit.remaining == 5
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_geocoding_result_raw_defaults():
|
|
488
|
+
"""raw defaults to an empty dict and to_dict() returns a copy."""
|
|
489
|
+
result = GeocodingResult(
|
|
490
|
+
address_components=AddressComponents.from_api({}),
|
|
491
|
+
formatted_address="",
|
|
492
|
+
location=None,
|
|
493
|
+
accuracy=0.0,
|
|
494
|
+
accuracy_type="",
|
|
495
|
+
source="",
|
|
496
|
+
raw={"formatted_address": "", "match_type": None},
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
assert result.to_dict() == {"formatted_address": "", "match_type": None}
|
|
500
|
+
result.to_dict()["formatted_address"] = "mutated"
|
|
501
|
+
assert result.raw["formatted_address"] == ""
|
|
502
|
+
|
|
503
|
+
response = GeocodingResponse()
|
|
504
|
+
assert response.raw == {}
|
|
505
|
+
assert response.to_dict() == {}
|
|
506
|
+
assert response.rate_limit is None
|
|
File without changes
|
{geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/.github/workflows/publish.yml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/e2e/test_batch_reverse.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/test_exceptions.py
RENAMED
|
File without changes
|
{geocodio_library_python-1.3.0 → geocodio_library_python-1.4.0}/tests/unit/test_geocoding.py
RENAMED
|
File without changes
|
|
File without changes
|