Skip to content
This repository was archived by the owner on Jan 18, 2023. It is now read-only.

Commit a804c49

Browse files
committed
Add detail by location feature to adaptation and historic
1 parent 4caebe6 commit a804c49

File tree

7 files changed

+285
-7
lines changed

7 files changed

+285
-7
lines changed

firststreet/api/adaptation.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,52 @@ def get_detail(self, fsids, csv=False, limit=100, output_dir=None):
4242

4343
return product
4444

45+
def get_details_by_location(self, fsids, location_type, csv=False, limit=100, output_dir=None):
46+
"""Retrieves adaptation detail product data from the First Street Foundation API given a list of location
47+
FSIDs and returns a list of Adaptation Detail objects.
48+
49+
Args:
50+
fsids (list/file): A First Street Foundation IDs or a file of First Street Foundation IDs
51+
location_type (str): The location lookup type
52+
csv (bool): To output extracted data to a csv or not
53+
limit (int): max number of connections to make
54+
output_dir (str): The output directory to save the generated csvs
55+
Returns:
56+
A list of list of Adaptation Summary and Adaptation Detail
57+
Raises:
58+
InvalidArgument: The location provided is empty
59+
TypeError: The location provided is not a string
60+
"""
61+
62+
if not location_type:
63+
raise InvalidArgument(location_type)
64+
elif not isinstance(location_type, str):
65+
raise TypeError("location is not a string")
66+
elif location_type == 'property':
67+
raise InvalidArgument("Property is not a valid location type")
68+
69+
# Get data from api and create objects
70+
api_datas_summary = self.call_api(fsids, "adaptation", "summary", location_type, limit=limit)
71+
summary = [AdaptationSummary(api_data) for api_data in api_datas_summary]
72+
73+
fsids = list(set([adaptation for sum_adap in summary if sum_adap.adaptation for
74+
adaptation in sum_adap.adaptation]))
75+
76+
if fsids:
77+
api_datas_detail = self.call_api(fsids, "adaptation", "detail", None, limit=limit)
78+
79+
else:
80+
api_datas_detail = [{"adaptationId": None}]
81+
82+
detail = [AdaptationDetail(api_data) for api_data in api_datas_detail]
83+
84+
if csv:
85+
csv_format.to_csv([summary, detail], "adaptation", "summary_detail", location_type, output_dir=output_dir)
86+
87+
logging.info("Adaptation Summary Detail Data Ready.")
88+
89+
return [summary, detail]
90+
4591
def get_summary(self, fsids, location_type, csv=False, limit=100, output_dir=None):
4692
"""Retrieves adaptation summary product data from the First Street Foundation API given a list of FSIDs and
4793
returns a list of Adaptation Summary objects.
@@ -54,6 +100,9 @@ def get_summary(self, fsids, location_type, csv=False, limit=100, output_dir=Non
54100
output_dir (str): The output directory to save the generated csvs
55101
Returns:
56102
A list of Adaptation Summary
103+
Raises:
104+
InvalidArgument: The location provided is empty
105+
TypeError: The location provided is not a string
57106
"""
58107

59108
if not location_type:

firststreet/api/csv_format.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ def to_csv(data, product, product_subtype, location_type=None, output_dir=None):
4444
elif product_subtype == 'summary':
4545
df = format_adaptation_summary(data)
4646

47+
elif product_subtype == 'summary_detail':
48+
df = format_adaptation_summary_detail(data)
49+
4750
else:
4851
raise NotImplementedError
4952

@@ -83,6 +86,9 @@ def to_csv(data, product, product_subtype, location_type=None, output_dir=None):
8386
else:
8487
df = format_historic_summary(data)
8588

89+
elif product_subtype == 'summary_event':
90+
df = format_historic_summary_event(data)
91+
8692
else:
8793
raise NotImplementedError
8894

@@ -136,8 +142,7 @@ def to_csv(data, product, product_subtype, location_type=None, output_dir=None):
136142
raise NotImplementedError
137143

138144
# Export CSVs
139-
df.fillna(pd.NA, inplace=True)
140-
df.to_csv(output_dir + '/' + file_name, index=False)
145+
df.fillna(pd.NA).astype(str).to_csv(output_dir + '/' + file_name, index=False)
141146
logging.info("CSV generated to '{}'.".format(output_dir + '/' + file_name))
142147

143148

@@ -157,7 +162,7 @@ def format_adaptation_detail(data):
157162

158163

159164
def format_adaptation_summary(data):
160-
"""Reformat the list of data to Adaptation Summary format
165+
"""Reformat the list of data to Adaptation Summary Detail format
161166
162167
Args:
163168
data (list): A list of FSF object
@@ -166,9 +171,28 @@ def format_adaptation_summary(data):
166171
"""
167172
df = pd.DataFrame([vars(o) for o in data]).explode('adaptation').reset_index(drop=True)
168173
df['fsid'] = df['fsid'].apply(str)
174+
df['adaptation'] = df['adaptation'].astype('Int64').apply(str)
169175
return df[['fsid', 'adaptation', 'properties']]
170176

171177

178+
def format_adaptation_summary_detail(data):
179+
"""Reformat the list of data to Adaptation Summary format
180+
181+
Args:
182+
data (list): A list of FSF object
183+
Returns:
184+
A pandas formatted DataFrame
185+
"""
186+
187+
summary = format_adaptation_summary(data[0])
188+
detail = format_adaptation_detail(data[1])
189+
190+
df = pd.merge(summary, detail, left_on='adaptation', right_on='adaptationId', how='left')\
191+
.drop('adaptationId', axis=1)
192+
193+
return df
194+
195+
172196
def format_probability_chance(data):
173197
"""Reformat the list of data to Probability Chance format
174198
@@ -245,9 +269,6 @@ def format_probability_count_summary(data):
245269
Returns:
246270
A pandas formatted DataFrame
247271
"""
248-
# listed_data = [{'fsid': attr.fsid, 'location': [{'state': attr.state}, {'city': attr.city}, {'zcta': attr.zcta},
249-
# {'neighborhood': attr.neighborhood}, {'tract': attr.tract},
250-
# {'county': attr.county}, {'cd': attr.cd}]} for attr in data]
251272
listed_data = [{'fsid': attr.fsid, 'location': [{'location': 'state', 'data': attr.state},
252273
{'location': 'city', 'data': attr.city},
253274
{'location': 'zcta', 'data': attr.zcta},
@@ -462,6 +483,21 @@ def format_historic_summary(data):
462483
return df[['fsid', 'eventId', 'name', 'type', 'bin', 'count']]
463484

464485

486+
def format_historic_summary_event(data):
487+
"""Reformat the list of data to Historic Summary Event format
488+
489+
Args:
490+
data (list): A list of FSF object
491+
Returns:
492+
A pandas formatted DataFrame
493+
"""
494+
495+
summary = format_historic_summary(data[0])
496+
event = format_historic_event(data[1])
497+
498+
return pd.merge(summary, event, on='eventId', how='left')
499+
500+
465501
def format_location_detail_property(data):
466502
"""Reformat the list of data to Location Detail format for property
467503

firststreet/api/historic.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,52 @@ def get_event(self, fsids, csv=False, limit=100, output_dir=None):
4343

4444
return product
4545

46+
def get_events_by_location(self, fsids, location_type, csv=False, limit=100, output_dir=None):
47+
"""Retrieves historic summary product data from the First Street Foundation API given a list of location
48+
FSIDs and returns a list of Historic Summary objects.
49+
50+
Args:
51+
fsids (list/file): A First Street Foundation IDs or a file of First Street Foundation IDs
52+
location_type (str): The location lookup type
53+
csv (bool): To output extracted data to a csv or not
54+
limit (int): max number of connections to make
55+
output_dir (str): The output directory to save the generated csvs
56+
Returns:
57+
A list of Historic Event
58+
Raises:
59+
InvalidArgument: The location provided is empty
60+
TypeError: The location provided is not a string
61+
"""
62+
63+
if not location_type:
64+
raise InvalidArgument(location_type)
65+
elif not isinstance(location_type, str):
66+
raise TypeError("location is not a string")
67+
elif location_type == 'property':
68+
raise InvalidArgument("Property is not a valid location type")
69+
70+
# Get data from api and create objects
71+
api_datas = self.call_api(fsids, "historic", "summary", location_type, limit)
72+
summary = [HistoricSummary(api_data) for api_data in api_datas]
73+
74+
fsids = list(set([event.get("eventId") for sum_hist in summary if sum_hist.historic for
75+
event in sum_hist.historic]))
76+
77+
if fsids:
78+
api_datas_event = self.call_api(fsids, "historic", "event", None, limit)
79+
80+
else:
81+
api_datas_event = [{"eventId": None}]
82+
83+
event = [HistoricEvent(api_data) for api_data in api_datas_event]
84+
85+
if csv:
86+
csv_format.to_csv([summary, event], "historic", "summary_event", location_type, output_dir=output_dir)
87+
88+
logging.info("Historic Summary Event Data Ready.")
89+
90+
return [summary, event]
91+
4692
def get_summary(self, fsids, location_type, csv=False, limit=100, output_dir=None):
4793
"""Retrieves historic summary product data from the First Street Foundation API given a list of FSIDs and
4894
returns a list of Historic Summary objects.
@@ -55,6 +101,9 @@ def get_summary(self, fsids, location_type, csv=False, limit=100, output_dir=Non
55101
output_dir (str): The output directory to save the generated csvs
56102
Returns:
57103
A list of Historic Summary
104+
Raises:
105+
InvalidArgument: The location provided is empty
106+
TypeError: The location provided is not a string
58107
"""
59108

60109
if not location_type:

firststreet/models/geometry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class Geometry:
1515
def __init__(self, geometry):
1616

1717
if geometry:
18-
self.polygon = shape(geometry.get('polygon'))
18+
self.polygon = shape(geometry.get('polygon')) if geometry.get('polygon') else None
1919
self.center = shape(geometry.get('center'))
2020
if geometry.get('bbox'):
2121
self.bbox = shape(geometry.get('bbox'))

tests/api/test_adaptation.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,68 @@ def test_mixed_invalid(self):
105105
def test_mixed_invalid_csv(self, tmpdir):
106106
adaptation = fs.adaptation.get_summary([2739, 0000], "property", csv=True, output_dir=tmpdir)
107107
assert len(adaptation) == 2
108+
109+
110+
class TestAdaptationSummaryDetail:
111+
112+
def test_empty(self):
113+
with pytest.raises(InvalidArgument):
114+
fs.adaptation.get_details_by_location([], "")
115+
116+
def test_empty_fsid(self):
117+
with pytest.raises(InvalidArgument):
118+
fs.adaptation.get_details_by_location([], "property")
119+
120+
def test_empty_type(self):
121+
with pytest.raises(InvalidArgument):
122+
fs.adaptation.get_details_by_location([1935265], "")
123+
124+
def test_wrong_fsid_type(self):
125+
with pytest.raises(InvalidArgument):
126+
fs.adaptation.get_details_by_location(190836953, "city")
127+
128+
def test_wrong_fsid_number(self):
129+
adaptation = fs.adaptation.get_details_by_location([11], "city")
130+
assert len(adaptation[0]) == 1
131+
assert len(adaptation[1]) == 1
132+
assert adaptation[0][0].adaptation is None
133+
134+
def test_incorrect_lookup_type(self, tmpdir):
135+
adaptation = fs.adaptation.get_details_by_location([1935265], "state", csv=True, output_dir=tmpdir)
136+
assert len(adaptation[0]) == 1
137+
assert len(adaptation[1]) == 1
138+
assert adaptation[0][0].adaptation is None
139+
140+
def test_wrong_adaptation_type(self):
141+
with pytest.raises(TypeError):
142+
fs.adaptation.get_details_by_location([1935265], 190)
143+
144+
def test_single(self):
145+
adaptation = fs.adaptation.get_details_by_location([1935265], "city")
146+
assert len(adaptation[0]) == 1
147+
assert len(adaptation[1]) == 2
148+
149+
def test_multiple(self):
150+
adaptation = fs.adaptation.get_details_by_location([1935265, 1714000], "city")
151+
assert len(adaptation[0]) == 2
152+
assert len(adaptation[1]) == 5
153+
154+
def test_single_csv(self, tmpdir):
155+
adaptation = fs.adaptation.get_details_by_location([1935265], "city", csv=True, output_dir=tmpdir)
156+
assert len(adaptation[0]) == 1
157+
assert len(adaptation[1]) == 2
158+
159+
def test_multiple_csv(self, tmpdir):
160+
adaptation = fs.adaptation.get_details_by_location([1935265, 1714000], "city", csv=True, output_dir=tmpdir)
161+
assert len(adaptation[0]) == 2
162+
assert len(adaptation[1]) == 5
163+
164+
def test_mixed_invalid(self):
165+
adaptation = fs.adaptation.get_details_by_location([1935265, 000000000], "city")
166+
assert len(adaptation[0]) == 2
167+
assert len(adaptation[1]) == 2
168+
169+
def test_mixed_invalid_csv(self, tmpdir):
170+
adaptation = fs.adaptation.get_details_by_location([1935265, 000000000], "city", csv=True, output_dir=tmpdir)
171+
assert len(adaptation[0]) == 2
172+
assert len(adaptation[1]) == 2

tests/api/test_historic.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,68 @@ def test_mixed_invalid(self):
105105
def test_mixed_invalid_csv(self, tmpdir):
106106
historic = fs.historic.get_summary([190836953, 000000000], "property", csv=True, output_dir=tmpdir)
107107
assert len(historic) == 2
108+
109+
110+
class TestHistoricSummaryDetail:
111+
112+
def test_empty(self):
113+
with pytest.raises(InvalidArgument):
114+
fs.historic.get_events_by_location([], "")
115+
116+
def test_empty_fsid(self):
117+
with pytest.raises(InvalidArgument):
118+
fs.historic.get_events_by_location([], "property")
119+
120+
def test_empty_type(self):
121+
with pytest.raises(InvalidArgument):
122+
fs.historic.get_events_by_location([190836953], "")
123+
124+
def test_wrong_fsid_type(self):
125+
with pytest.raises(InvalidArgument):
126+
fs.historic.get_events_by_location(190836953, "city")
127+
128+
def test_wrong_fsid_number(self):
129+
historic = fs.historic.get_events_by_location([11], "city")
130+
assert len(historic[0]) == 1
131+
assert len(historic[1]) == 1
132+
assert historic[0][0].historic is None
133+
134+
def test_incorrect_lookup_type(self, tmpdir):
135+
historic = fs.historic.get_events_by_location([1982200], "state", csv=True, output_dir=tmpdir)
136+
assert len(historic[0]) == 1
137+
assert len(historic[1]) == 1
138+
assert historic[0][0].historic is None
139+
140+
def test_wrong_historic_type(self):
141+
with pytest.raises(TypeError):
142+
fs.historic.get_events_by_location([1982200], 190)
143+
144+
def test_single(self):
145+
historic = fs.historic.get_events_by_location([1982200], "city")
146+
assert len(historic[0]) == 1
147+
assert len(historic[1]) == 1
148+
149+
def test_multiple(self):
150+
historic = fs.historic.get_events_by_location([1982200, 3905074], "city")
151+
assert len(historic[0]) == 2
152+
assert len(historic[1]) == 2
153+
154+
def test_single_csv(self, tmpdir):
155+
historic = fs.historic.get_events_by_location([1982200], "city", csv=True, output_dir=tmpdir)
156+
assert len(historic[0]) == 1
157+
assert len(historic[1]) == 1
158+
159+
def test_multiple_csv(self, tmpdir):
160+
historic = fs.historic.get_events_by_location([1982200, 3905074], "city", csv=True, output_dir=tmpdir)
161+
assert len(historic[0]) == 2
162+
assert len(historic[1]) == 2
163+
164+
def test_mixed_invalid(self):
165+
historic = fs.historic.get_events_by_location([1982200, 000000000], "city")
166+
assert len(historic[0]) == 2
167+
assert len(historic[1]) == 1
168+
169+
def test_mixed_invalid_csv(self, tmpdir):
170+
historic = fs.historic.get_events_by_location([1982200, 000000000], "city", csv=True, output_dir=tmpdir)
171+
assert len(historic[0]) == 2
172+
assert len(historic[1]) == 1

0 commit comments

Comments
 (0)