Coverage for src/usaspending/models/location.py: 97%
88 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-03 17:15 -0700
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-03 17:15 -0700
1from __future__ import annotations
2from typing import Dict, Any, Optional
3from titlecase import titlecase
4from ..utils.formatter import contracts_titlecase
5from .base_model import BaseModel
8class Location(BaseModel):
9 """Location model for USASpending data."""
11 def __init__(self, data: Dict[str, Any], client=None):
12 """Initialize Location. Client parameter is ignored for compatibility."""
13 super().__init__(data)
15 # simple direct fields --------------------------------------------------
16 @property
17 def address_line1(self) -> Optional[str]:
18 return self._format_location_string_property(self.get_value(["address_line1"]))
20 @property
21 def address_line2(self) -> Optional[str]:
22 return self._format_location_string_property(self.get_value(["address_line2"]))
24 @property
25 def address_line3(self) -> Optional[str]:
26 return self._format_location_string_property(self.get_value(["address_line3"]))
28 @property
29 def city_name(self) -> Optional[str]:
30 city_name = self.get_value(["city_name", "city"])
31 if not isinstance(city_name, str):
32 return None
33 return titlecase(city_name)
35 @property
36 def city(self) -> Optional[str]:
37 return self.city_name
39 @property
40 def state_name(self) -> Optional[str]:
41 state_name = titlecase(self.get_value(["state_name", "state"]))
42 if not isinstance(state_name, str):
43 return None
44 return titlecase(state_name)
46 @property
47 def country_name(self) -> Optional[str]:
48 country = self._format_location_string_property(
49 self.get_value(["country_name"])
50 )
51 if country and country.lower() == "usa":
52 country = "USA"
53 return country
55 @property
56 def zip4(self) -> Optional[str]:
57 return self.get_value(["zip4"])
59 @property
60 def county_name(self) -> Optional[str]:
61 county_name = titlecase(self.get_value(["county_name", "county"]))
62 if not isinstance(county_name, str):
63 return None
64 return county_name
66 @property
67 def county_code(self) -> Optional[str]:
68 return self.get_value(["county_code"])
70 @property
71 def congressional_code(self) -> Optional[str]:
72 return self.get_value(["congressional_code","district"])
74 @property
75 def foreign_province(self) -> Optional[str]:
76 return self.get_value(["foreign_province"])
78 @property
79 def foreign_postal_code(self) -> Optional[str]:
80 return self.get_value(["foreign_postal_code"])
82 # dual-source fields ----------------------------------------------------
83 @property
84 def state_code(self) -> Optional[str]:
85 return self.get_value(["state_code", "Place of Performance State Code"])
87 @property
88 def country_code(self) -> Optional[str]:
89 return self.get_value(
90 ["location_country_code", "Place of Performance Country Code"]
91 )
93 @property
94 def zip5(self) -> Optional[str]:
95 val = self.get_value(["zip5", "Place of Performance Zip5"])
96 return str(val) if val is not None else ""
98 # convenience -----------------------------------------------------------
99 @property
100 def district(self) -> Optional[str]:
101 pieces = [p for p in (self.state_code, self.congressional_code) if p]
102 return "-".join(pieces) or ""
104 @property
105 def formatted_address(self) -> Optional[str]:
106 lines: list[str] = [
107 line
108 for line in (self.address_line1, self.address_line2, self.address_line3)
109 if line
110 ]
111 trailing = [p for p in (self.city, self.state_code, self.zip5) if p]
112 if trailing:
113 lines.append(", ".join(trailing))
114 if self.country_name:
115 lines.append(self.country_name)
116 return "\n".join(lines) or None
118 def _format_location_string_property(self, text: str) -> Optional[str]:
119 """Format a location string with string check."""
120 if not isinstance(text, str):
121 return None
122 return contracts_titlecase(text.strip())
124 def __repr__(self) -> str:
125 return f"<Location {self.city or '?'} {self.state_code or ''} {self.country_code or ''}>"