Skip to content

Commit 36f64b9

Browse files
committed
Download script for USFEMA_FloodInsuranceClaims
1 parent 822c6ec commit 36f64b9

1 file changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import os
16+
import sys
17+
import shutil
18+
import time
19+
import requests
20+
from absl import logging
21+
22+
script_dir = os.path.dirname(os.path.abspath(__file__))
23+
data_dir = os.path.dirname(os.path.dirname(script_dir))
24+
if data_dir not in sys.path:
25+
sys.path.insert(0, data_dir)
26+
27+
from util.download_util_script import download_file
28+
29+
def get_total_records(api_url):
30+
"""
31+
Makes a preliminary API call to get the total number of records.
32+
33+
This is necessary because the main download utility and pagination logic
34+
are not guaranteed to be robust for all API behaviors (e.g., an empty
35+
final page).
36+
37+
Args:
38+
api_url (str): The base URL of the API endpoint.
39+
40+
Returns:
41+
int: The total number of records, or None if the request fails.
42+
"""
43+
count_url = f"{api_url}?$count=true"
44+
logging.info("Getting total record count from: %s", count_url)
45+
try:
46+
# Use requests for this simple JSON query, as the download_file
47+
# utility is for large file downloads and may not be suitable.
48+
response = requests.get(count_url, timeout=30)
49+
response.raise_for_status()
50+
data = response.json()
51+
total_count = int(data.get('metadata', {}).get('count'))
52+
logging.info("Found a total of %s records.", total_count)
53+
return total_count
54+
except requests.exceptions.RequestException as e:
55+
logging.error("Failed to get total record count: %s", e)
56+
return None
57+
except (ValueError, KeyError, TypeError) as e:
58+
logging.error("Failed to parse the total record count from the response: %s", e)
59+
return None
60+
61+
def download_fema_csv(api_url, filename="fema_nfip_claims.csv"):
62+
"""
63+
Downloads a complete CSV file from the FEMA API by handling pagination.
64+
65+
This function uses a provided utility to download paginated chunks
66+
and merges them into a single file.
67+
68+
Args:
69+
api_url (str): The base URL of the API endpoint.
70+
filename (str): The name of the final merged file.
71+
"""
72+
logging.set_verbosity(logging.INFO)
73+
74+
# Define the page size for each API request.
75+
PAGE_SIZE = 1000
76+
skip_count = 0
77+
records_downloaded = 0
78+
temp_dir = "temp_fema_data"
79+
final_filepath = filename
80+
81+
# Get the total number of records from the API for a reliable failsafe.
82+
total_records = get_total_records(api_url)
83+
if total_records is None:
84+
logging.fatal("Could not get the total record count. Cannot proceed.")
85+
return
86+
87+
try:
88+
# Create a temporary directory for downloaded chunks.
89+
if os.path.exists(temp_dir):
90+
shutil.rmtree(temp_dir)
91+
os.makedirs(temp_dir)
92+
93+
logging.info("Starting download to file: %s", final_filepath)
94+
95+
# The main download loop for pagination
96+
while records_downloaded < total_records:
97+
csv_url = f"{api_url}?$format=csv&$skip={skip_count}"
98+
logging.info("Requesting data from: %s", csv_url)
99+
100+
101+
util_output_filename = "FimaNfipClaims.xlsx"
102+
util_output_path = os.path.join(temp_dir, util_output_filename)
103+
104+
# Then, define the unique filename our script will use for processing.
105+
chunk_filename = f"FimaNfipClaims_{skip_count}.xlsx"
106+
chunk_filepath = os.path.join(temp_dir, chunk_filename)
107+
108+
download_success = download_file(
109+
url=csv_url,
110+
output_folder=temp_dir,
111+
unzip=False,
112+
tries=3,
113+
delay=5,
114+
backoff=2
115+
)
116+
117+
# Check for download success and if the utility's output file exists.
118+
if not download_success or not os.path.exists(util_output_path):
119+
logging.error("Failed to download chunk or file not found. Exiting.")
120+
break
121+
122+
# Rename the downloaded file to a unique name so the utility
123+
# downloads a fresh copy on the next iteration.
124+
os.rename(util_output_path, chunk_filepath)
125+
126+
# Read the downloaded chunk from its new unique path.
127+
with open(chunk_filepath, 'rb') as f_chunk:
128+
content = f_chunk.read()
129+
130+
# Append content to the final file, handling the header.
131+
with open(final_filepath, 'ab') as f_final:
132+
if skip_count == 0:
133+
# For the first chunk, write the entire content (including header).
134+
f_final.write(content)
135+
else:
136+
# For subsequent chunks, strip the header before writing.
137+
content_without_header = content.split(b'\n', 1)[1]
138+
f_final.write(content_without_header)
139+
140+
# Check how many records were in the chunk
141+
num_records_in_chunk = len(content.split(b'\n')) - 1
142+
records_downloaded += num_records_in_chunk
143+
144+
logging.info("Downloaded %s of %s records.", records_downloaded, total_records)
145+
146+
# If the number of records is less than the page size, it's the last page.
147+
if num_records_in_chunk < PAGE_SIZE:
148+
logging.info("Reached the end of the dataset. All records have been downloaded.")
149+
break
150+
151+
# Increment the skip counter for the next request.
152+
skip_count += PAGE_SIZE
153+
154+
logging.info("Total download complete. All available records saved to: %s", final_filepath)
155+
156+
except IOError as e:
157+
logging.error("An error occurred while writing the file: %s", e)
158+
finally:
159+
# Clean up the temporary directory.
160+
if os.path.exists(temp_dir):
161+
shutil.rmtree(temp_dir)
162+
163+
if __name__ == "__main__":
164+
api_url = "https://www.fema.gov/api/open/v2/FimaNfipClaims"
165+
download_fema_csv(api_url)
166+

0 commit comments

Comments
 (0)