/
google_usps_validator.py
236 lines (186 loc) · 7.53 KB
/
google_usps_validator.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
import requests
from django_project import settings
from pyusps import address_information
FAILED = 'FAILED'
UNSUBMITTED = 'UNSUBMITTED'
MAPPED = 'MAPPED'
MAPPED_PARTIAL = 'MAPPED_PARTIAL'
MATCHED = 'MATCHED'
MATCHED_PARTIAL = 'MATCHED_PARTIAL'
class GoogleUspsValidator(object):
# TODO: The instance variables got out of control here.
# I'd like to refactor this to pass around return values instead of holding
# instance variables.
def __init__(self, user_input):
self.key = settings.GOOGLE_API_KEY
self.status = ''
self.message = ''
self.user_input = user_input
# Expect this to be the big messy dict returned by Google API.
self.mapped_address = {}
self.mapped_lines = ()
self.matched_street = ''
self.matched_city = ''
self.matched_state = ''
self.matched_zip5 = ''
self.matched_zip4 = ''
def validate(self):
self.google_geocode()
if self.status in [MAPPED, MAPPED_PARTIAL]:
self.extract_lines_from_google_address()
self.usps_validate()
def mapped(self, mapped_address):
# Found a good match with Google Maps, but haven't done USPS yet.
self.status = MAPPED
self.mapped_address = mapped_address
def failed(self, message):
# The user_input is not going to be resolvable as entered.
self.status = FAILED
self.message = message
def unsubmitted(self, message):
# We haven't had a valid gmaps response about this user_input yet.
# Most likely an API call failed entirely.
self.status = UNSUBMITTED
self.message = message
def mapped_partial(self, message, mapped_address):
# Partial match from the Google Maps API.
self.status = MAPPED_PARTIAL
self.message = message
self.mapped_address = mapped_address
def matched_partial(self, result, message):
self.matched(result, message)
self.status = MATCHED_PARTIAL
def matched(self, result, message):
self.matched_street = result['address'].title()
self.matched_city = result['city'].title()
self.matched_state = result['state']
self.matched_zip5 = result['zip5']
self.matched_zip4 = result['zip4'] or ''
self.message = message
self.status = MATCHED
def google_geocode(self):
params = {
'address': self.user_input,
'key': self.key,
}
url = 'https://maps.googleapis.com/maps/api/geocode/json'
# noinspection PyBroadException
try:
response = requests.get(url, params=params).json()
except:
self.unsubmitted('Failed to connect to Google API. Possibly try '
'again later.')
return
if response['status'] == 'OK':
results = response['results']
# ROOFTOP results are the most specific. Prefer them.
rooftops = [result for result in results
if result['geometry']['location_type'] == 'ROOFTOP']
if rooftops:
results = rooftops
# Too many results
if len(results) > 1:
self.failed('Multiple results found with the Google Maps API. '
'Try being more specific.')
return
# One result
else:
address = results[0]
if ('partial_match' in address or
address['geometry']['location_type'] in
['RANGE_INTERPOLATED', 'APPROXIMATE']):
self.mapped_partial(
'Inexact match. Be sure to check the result address '
'carefully.',
address)
return
elif address['geometry']['location_type'] in \
['GEOMETRIC_CENTER']:
self.failed('Address given was too broad. Try adding more '
'specific detail.')
return
else:
assert (
('partial_match' not in address) and
(address['geometry']['location_type'] == 'ROOFTOP'))
# Finally!
self.mapped(address)
return
# Bad status
else:
if response['status'] == 'ZERO_RESULTS':
self.failed('No results found with the Google Maps API.')
return
else: # Other error code
message = ('Google API call failed with status {}. ({})'.format(
response['status'],
response.get('error_message',
'No additional error message.'))
)
self.unsubmitted(message)
return
def extract_lines_from_google_address(self):
relevant_types = {'street_number', # Street name
'route', # Street name
'subpremise', # Apartment number
'administrative_area_level_1', # State
'locality', # City
'postal_code', # ZIP
}
my_components = {}
for gmap_component in self.mapped_address['address_components']:
component_types = gmap_component['types']
my_types = relevant_types.intersection(component_types)
if my_types:
# If this isn't true then I'm making bad assumptions about
# the API:
assert len(my_types) == 1
my_type = my_types.pop()
my_components[my_type] = gmap_component['long_name']
# USPS honors letter suite numbers when they're preceded by #.
if my_components.get('subpremise'):
my_components['subpremise'] = '# ' + my_components['subpremise']
# Concatenate only the elements which are not empty. Can't predict
# which ones the Google API will return.
line_1 = ' '.join(
filter(
lambda x: x is not None,
[
my_components.get('street_number'),
my_components.get('route'),
my_components.get('subpremise'),
]
)
)
line_2 = ' '.join(
filter(
lambda x: x is not None,
[
my_components.get('locality'),
my_components.get('administrative_area_level_1'),
my_components.get('postal_code'),
]
)
)
self.mapped_lines = (line_1, line_2)
def usps_validate(self):
line_1, line_2 = self.mapped_lines
key = settings.USPS_API_KEY
data = {
'address': line_1,
'city': line_2
}
result = {}
try:
result = address_information.verify(key, data)
except ValueError as e:
self.failed(e.message)
return
if result.get('returntext'):
self.matched_partial(result, "(USPS): " + result.get('returntext'))
# Good USPS match, partial Google match
elif self.status == MAPPED_PARTIAL:
self.matched_partial(result, self.message)
else:
assert (self.status == MAPPED) # Belt and suspenders
self.matched(result, "Address is fully matched and is deliverable.")