Skip to content

Commit 9378982

Browse files
committed
feat: improve Canadian field support - Split CanadianRiding into FederalRiding and ProvincialRiding models, add complete StatisticsCanadaData model, update tests, fix dataclass field ordering
1 parent beca3c2 commit 9378982

File tree

3 files changed

+394
-3
lines changed

3 files changed

+394
-3
lines changed

src/geocodio/client.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414
GeocodingResponse, GeocodingResult, AddressComponents,
1515
Location, GeocodioFields, Timezone, CongressionalDistrict,
1616
CensusData, ACSSurveyData, StateLegislativeDistrict, SchoolDistrict,
17-
Demographics, Economics, Families, Housing
17+
Demographics, Economics, Families, Housing, Social,
18+
FederalRiding, ProvincialRiding, StatisticsCanadaData
1819
)
1920
from .exceptions import InvalidRequestError, AuthenticationError, GeocodioServerError
2021

@@ -40,12 +41,18 @@ def geocode(
4041
address: Union[str, Dict[str, str], List[Union[str, Dict[str, str]]], Dict[str, Union[str, Dict[str, str]]]],
4142
fields: Optional[List[str]] = None,
4243
limit: Optional[int] = None,
44+
country: Optional[str] = None,
45+
format: Optional[str] = None,
4346
) -> GeocodingResponse:
4447
params: Dict[str, Union[str, int]] = {"api_key": self.api_key}
4548
if fields:
4649
params["fields"] = ",".join(fields)
4750
if limit:
4851
params["limit"] = int(limit)
52+
if country:
53+
params["country"] = country
54+
if format:
55+
params["format"] = format
4956

5057
endpoint: str
5158
data: Union[List, Dict] | None
@@ -54,7 +61,20 @@ def geocode(
5461
if isinstance(address, dict) and not any(isinstance(v, dict) for v in address.values()):
5562
# Single structured address
5663
endpoint = f"{self.BASE_PATH}/geocode"
57-
params.update(address)
64+
# Map our parameter names to API parameter names
65+
param_map = {
66+
"street": "street",
67+
"street2": "street2",
68+
"city": "city",
69+
"county": "county",
70+
"state": "state",
71+
"postal_code": "postal_code",
72+
"country": "country",
73+
}
74+
# Only include parameters that are present in the input
75+
for key, value in address.items():
76+
if key in param_map and value:
77+
params[param_map[key]] = value
5878
data = None
5979
elif isinstance(address, list):
6080
# Batch addresses - send list directly
@@ -244,6 +264,32 @@ def _parse_fields(self, fields_data: dict | None) -> GeocodioFields | None:
244264
if "acs-housing" in fields_data else None
245265
)
246266

267+
social = (
268+
Social.from_api(fields_data["acs-social"])
269+
if "acs-social" in fields_data else None
270+
)
271+
272+
# Canadian fields
273+
riding = (
274+
FederalRiding.from_api(fields_data["riding"])
275+
if "riding" in fields_data else None
276+
)
277+
278+
provriding = (
279+
ProvincialRiding.from_api(fields_data["provriding"])
280+
if "provriding" in fields_data else None
281+
)
282+
283+
provriding_next = (
284+
ProvincialRiding.from_api(fields_data["provriding-next"])
285+
if "provriding-next" in fields_data else None
286+
)
287+
288+
statcan = (
289+
StatisticsCanadaData.from_api(fields_data["statcan"])
290+
if "statcan" in fields_data else None
291+
)
292+
247293
return GeocodioFields(
248294
timezone=timezone,
249295
congressional_districts=congressional_districts,
@@ -258,4 +304,9 @@ def _parse_fields(self, fields_data: dict | None) -> GeocodioFields | None:
258304
economics=economics,
259305
families=families,
260306
housing=housing,
307+
social=social,
308+
riding=riding,
309+
provriding=provriding,
310+
provriding_next=provriding_next,
311+
statcan=statcan,
261312
)

src/geocodio/models.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,76 @@ class Housing(_HasExtras, ApiModelMixin):
194194
extras: Dict[str, Any] = field(default_factory=dict, repr=False)
195195

196196

197+
@dataclass(slots=True, frozen=True)
198+
class Social(_HasExtras, ApiModelMixin):
199+
"""
200+
American Community Survey social data.
201+
"""
202+
high_school_graduate_or_higher: Optional[int] = None
203+
bachelors_degree_or_higher: Optional[int] = None
204+
graduate_degree_or_higher: Optional[int] = None
205+
veterans: Optional[int] = None
206+
veterans_percentage: Optional[float] = None
207+
extras: Dict[str, Any] = field(default_factory=dict, repr=False)
208+
209+
210+
@dataclass(slots=True, frozen=True)
211+
class ZIP4Data(_HasExtras, ApiModelMixin):
212+
"""USPS ZIP+4 code and delivery information."""
213+
zip4: str
214+
delivery_point: str
215+
carrier_route: str
216+
extras: Dict[str, Any] = field(default_factory=dict, repr=False)
217+
218+
219+
@dataclass(slots=True, frozen=True)
220+
class FederalRiding(_HasExtras, ApiModelMixin):
221+
"""Canadian federal electoral district information."""
222+
code: str
223+
name_english: str
224+
name_french: str
225+
ocd_id: str
226+
year: int
227+
source: str
228+
extras: Dict[str, Any] = field(default_factory=dict, repr=False)
229+
230+
231+
@dataclass(slots=True, frozen=True)
232+
class ProvincialRiding(_HasExtras, ApiModelMixin):
233+
"""Canadian provincial electoral district information."""
234+
name_english: str
235+
name_french: str
236+
ocd_id: str
237+
is_upcoming_district: bool
238+
source: str
239+
extras: Dict[str, Any] = field(default_factory=dict, repr=False)
240+
241+
242+
@dataclass(slots=True, frozen=True)
243+
class StatisticsCanadaData(_HasExtras, ApiModelMixin):
244+
"""Canadian statistical boundaries from Statistics Canada."""
245+
division: Dict[str, Any]
246+
consolidated_subdivision: Dict[str, Any]
247+
subdivision: Dict[str, Any]
248+
economic_region: str
249+
statistical_area: Dict[str, Any]
250+
cma_ca: Dict[str, Any]
251+
tract: str
252+
population_centre: Dict[str, Any]
253+
dissemination_area: Dict[str, Any]
254+
dissemination_block: Dict[str, Any]
255+
census_year: int
256+
designated_place: Optional[Dict[str, Any]] = None
257+
extras: Dict[str, Any] = field(default_factory=dict, repr=False)
258+
259+
260+
@dataclass(slots=True, frozen=True)
261+
class FFIECData(_HasExtras, ApiModelMixin):
262+
"""FFIEC CRA/HMDA Data (Beta)."""
263+
# Add FFIEC specific fields as they become available
264+
extras: Dict[str, Any] = field(default_factory=dict, repr=False)
265+
266+
197267
@dataclass(slots=True, frozen=True)
198268
class GeocodioFields:
199269
"""
@@ -205,14 +275,42 @@ class GeocodioFields:
205275
state_legislative_districts: Optional[List[StateLegislativeDistrict]] = None
206276
state_legislative_districts_next: Optional[List[StateLegislativeDistrict]] = None
207277
school_districts: Optional[List[SchoolDistrict]] = None
278+
279+
# Census data for all available years
280+
census2000: Optional[CensusData] = None
208281
census2010: Optional[CensusData] = None
282+
census2011: Optional[CensusData] = None
283+
census2012: Optional[CensusData] = None
284+
census2013: Optional[CensusData] = None
285+
census2014: Optional[CensusData] = None
286+
census2015: Optional[CensusData] = None
287+
census2016: Optional[CensusData] = None
288+
census2017: Optional[CensusData] = None
289+
census2018: Optional[CensusData] = None
290+
census2019: Optional[CensusData] = None
209291
census2020: Optional[CensusData] = None
292+
census2021: Optional[CensusData] = None
293+
census2022: Optional[CensusData] = None
210294
census2023: Optional[CensusData] = None
295+
census2024: Optional[CensusData] = None
296+
297+
# ACS data
211298
acs: Optional[ACSSurveyData] = None
212299
demographics: Optional[Demographics] = None
213300
economics: Optional[Economics] = None
214301
families: Optional[Families] = None
215302
housing: Optional[Housing] = None
303+
social: Optional[Social] = None
304+
305+
# New fields
306+
zip4: Optional[ZIP4Data] = None
307+
ffiec: Optional[FFIECData] = None
308+
309+
# Canadian fields
310+
riding: Optional[FederalRiding] = None
311+
provriding: Optional[ProvincialRiding] = None
312+
provriding_next: Optional[ProvincialRiding] = None
313+
statcan: Optional[StatisticsCanadaData] = None
216314

217315

218316
# ──────────────────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)