Skip to content

Commit 1e7d7ab

Browse files
fix(parser): handle new state legislative district response structure
The API now returns state_legislative_districts as a dict with house/senate keys containing legislator info, but the parser only checked for the "stateleg" key and expected a flat list. This caused all stateleg data to silently land in extras as None. Add _parse_stateleg helper that handles both the new dict format and legacy flat list format, and check for both "stateleg" and "state_legislative_districts" response keys.
1 parent 94629c1 commit 1e7d7ab

2 files changed

Lines changed: 141 additions & 10 deletions

File tree

src/geocodio/client.py

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,28 @@ def _parse_list_response(response_json: dict, response: httpx.Response = None) -
493493
)
494494

495495

496+
@staticmethod
497+
def _parse_stateleg(data) -> list:
498+
"""Parse state legislative district data.
499+
500+
Handles both formats:
501+
- Dict with house/senate keys: {house: [...], senate: [...]}
502+
- Flat list of district dicts (legacy)
503+
"""
504+
if isinstance(data, dict):
505+
districts = []
506+
for chamber, chamber_districts in data.items():
507+
if isinstance(chamber_districts, list):
508+
for district in chamber_districts:
509+
district_data = dict(district)
510+
if "chamber" not in district_data:
511+
district_data["chamber"] = chamber
512+
districts.append(StateLegislativeDistrict.from_api(district_data))
513+
return districts
514+
elif isinstance(data, list):
515+
return [StateLegislativeDistrict.from_api(d) for d in data]
516+
return []
517+
496518
def _parse_fields(self, fields_data: dict | None) -> GeocodioFields | None:
497519
"""
498520
Parse fields data from API response.
@@ -522,17 +544,15 @@ def _parse_fields(self, fields_data: dict | None) -> GeocodioFields | None:
522544

523545
state_legislative_districts = None
524546
if "stateleg" in fields_data:
525-
state_legislative_districts = [
526-
StateLegislativeDistrict.from_api(district)
527-
for district in fields_data["stateleg"]
528-
]
547+
state_legislative_districts = self._parse_stateleg(fields_data["stateleg"])
548+
elif "state_legislative_districts" in fields_data:
549+
state_legislative_districts = self._parse_stateleg(fields_data["state_legislative_districts"])
529550

530551
state_legislative_districts_next = None
531552
if "stateleg-next" in fields_data:
532-
state_legislative_districts_next = [
533-
StateLegislativeDistrict.from_api(district)
534-
for district in fields_data["stateleg-next"]
535-
]
553+
state_legislative_districts_next = self._parse_stateleg(fields_data["stateleg-next"])
554+
elif "state_legislative_districts_next" in fields_data:
555+
state_legislative_districts_next = self._parse_stateleg(fields_data["state_legislative_districts_next"])
536556

537557
# School districts - support both nested dict and flat list formats
538558
school_districts = None
@@ -713,7 +733,7 @@ def parse_census_data(data: dict) -> dict:
713733
# Collect all known field keys that were parsed
714734
parsed_keys = {
715735
"timezone", "cd", "congressional_districts",
716-
"stateleg", "stateleg-next",
736+
"stateleg", "stateleg-next", "state_legislative_districts", "state_legislative_districts_next",
717737
"school", "school_districts", # Both school formats
718738
"census", # Nested census structure
719739
"acs", # Nested ACS structure

tests/unit/test_geocode.py

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,4 +596,115 @@ def response_callback(request):
596596
# This will fail until we fix the parsing logic
597597
assert result.fields.census2024 is not None
598598
assert result.fields.census2024.tract == "960100"
599-
assert result.fields.census2024.block == "2004"
599+
assert result.fields.census2024.block == "2004"
600+
601+
602+
def test_geocode_with_stateleg_fields(client, httpx_mock):
603+
"""Test geocoding with state legislative district fields.
604+
605+
The API returns state_legislative_districts as a dict with house/senate keys,
606+
each containing a list of district objects with legislator info.
607+
"""
608+
def response_callback(request):
609+
assert request.url.params["fields"] == "stateleg"
610+
return httpx.Response(200, json={
611+
"input": {"formatted_address": "600 Santa Ray Ave, Oakland, CA 94610"},
612+
"results": [{
613+
"address_components": {
614+
"number": "600",
615+
"street": "Santa Ray",
616+
"suffix": "Ave",
617+
"city": "Oakland",
618+
"state": "CA",
619+
"zip": "94610",
620+
"country": "US"
621+
},
622+
"formatted_address": "600 Santa Ray Ave, Oakland, CA 94610",
623+
"location": {"lat": 37.811943, "lng": -122.240213},
624+
"accuracy": 1,
625+
"accuracy_type": "rooftop",
626+
"source": "Alameda",
627+
"fields": {
628+
"state_legislative_districts": {
629+
"house": [
630+
{
631+
"name": "Assembly District 18",
632+
"district_number": "18",
633+
"ocd_id": "ocd-division/country:us/state:ca/sldl:18",
634+
"is_upcoming_state_legislative_district": False,
635+
"proportion": 1,
636+
"current_legislators": [
637+
{
638+
"type": "representative",
639+
"bio": {
640+
"last_name": "Bonta",
641+
"first_name": "Mia",
642+
"party": "Democrat"
643+
}
644+
}
645+
]
646+
}
647+
],
648+
"senate": [
649+
{
650+
"name": "Senate District 7",
651+
"district_number": "7",
652+
"ocd_id": "ocd-division/country:us/state:ca/sldu:7",
653+
"is_upcoming_state_legislative_district": False,
654+
"proportion": 1,
655+
"current_legislators": [
656+
{
657+
"type": "senator",
658+
"bio": {
659+
"last_name": "Arreguin",
660+
"first_name": "Jesse",
661+
"party": "Democrat"
662+
}
663+
}
664+
]
665+
}
666+
]
667+
}
668+
}
669+
}]
670+
})
671+
672+
httpx_mock.add_callback(
673+
callback=response_callback,
674+
url=httpx.URL("https://api.test/v1.9/geocode", params={
675+
"q": "600 Santa Ray Ave, Oakland CA 94610",
676+
"fields": "stateleg"
677+
}),
678+
match_headers={"Authorization": "Bearer TEST_KEY"},
679+
)
680+
681+
# Act
682+
resp = client.geocode("600 Santa Ray Ave, Oakland CA 94610", fields=["stateleg"])
683+
684+
# Assert - state legislative districts should be parsed, not None
685+
fields = resp.results[0].fields
686+
assert fields.state_legislative_districts is not None
687+
assert len(fields.state_legislative_districts) == 2
688+
689+
# Check house district
690+
house = [d for d in fields.state_legislative_districts if d.chamber == "house"]
691+
assert len(house) == 1
692+
assert house[0].name == "Assembly District 18"
693+
assert house[0].district_number == "18"
694+
assert house[0].ocd_id == "ocd-division/country:us/state:ca/sldl:18"
695+
assert house[0].proportion == 1
696+
697+
# Check senate district
698+
senate = [d for d in fields.state_legislative_districts if d.chamber == "senate"]
699+
assert len(senate) == 1
700+
assert senate[0].name == "Senate District 7"
701+
assert senate[0].district_number == "7"
702+
assert senate[0].ocd_id == "ocd-division/country:us/state:ca/sldu:7"
703+
704+
# Check that legislator info is accessible via extras
705+
legislators = house[0].get_extra("current_legislators", [])
706+
assert len(legislators) == 1
707+
assert legislators[0]["bio"]["last_name"] == "Bonta"
708+
709+
# Ensure state_legislative_districts didn't leak into extras
710+
assert "state_legislative_districts" not in fields.extras

0 commit comments

Comments
 (0)