Skip to content

Commit 9e53422

Browse files
committed
Issue #43: Add Python script to download and extract data files.
1 parent 7418df5 commit 9e53422

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

bin/download_data.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env python3
2+
import asyncio
3+
import sys
4+
import zipfile
5+
from pathlib import Path
6+
7+
import httpx
8+
9+
# Data files to download
10+
DOWNLOADS = [
11+
'http://download.geonames.org/export/dump/cities500.zip',
12+
'http://download.geonames.org/export/dump/cities1000.zip',
13+
'http://download.geonames.org/export/dump/cities5000.zip',
14+
'http://download.geonames.org/export/dump/cities15000.zip',
15+
'http://download.geonames.org/export/dump/countryInfo.txt',
16+
'https://www2.census.gov/geo/docs/reference/codes2020/national_county2020.txt'
17+
]
18+
19+
20+
async def download_file(client: httpx.AsyncClient, filename: str, url: str, data_dir: Path) -> bool:
21+
"""Download a single file."""
22+
23+
print(f'Downloading {filename}...')
24+
25+
try:
26+
response = await client.get(url, follow_redirects=True)
27+
response.raise_for_status()
28+
file_path = data_dir / filename
29+
30+
# Write the file
31+
file_path.write_bytes(response.content)
32+
print(f'✓ Downloaded {filename}')
33+
return True
34+
35+
except httpx.RequestError as e:
36+
print(f'✗ Network error downloading {filename}: {e}')
37+
return False
38+
except httpx.HTTPStatusError as e:
39+
print(f'✗ HTTP error downloading {filename}: {e.response.status_code}')
40+
return False
41+
except Exception as e:
42+
print(f'✗ Unexpected error downloading {filename}: {e}')
43+
return False
44+
45+
46+
def extract_zip(zip_path: Path, extract_to: Path) -> bool:
47+
"""Extract a zip file and remove the zip afterwards."""
48+
49+
try:
50+
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
51+
zip_ref.extractall(extract_to)
52+
53+
# Remove the zip file
54+
zip_path.unlink()
55+
print(f'✓ Extracted and removed {zip_path.name}')
56+
return True
57+
58+
except zipfile.BadZipFile:
59+
print(f'✗ Bad zip file: {zip_path}')
60+
return False
61+
except Exception as e:
62+
print(f'✗ Error extracting {zip_path}: {e}')
63+
return False
64+
65+
66+
async def download_all_files(data_dir: Path = Path('data')) -> bool:
67+
"""Download all required data files."""
68+
69+
# Create data directory
70+
data_dir.mkdir(exist_ok=True)
71+
print(f'Using data directory: {data_dir.absolute()}')
72+
73+
success_count = 0
74+
75+
# Use httpx client with reasonable timeouts
76+
timeout = httpx.Timeout(30, connect=10)
77+
async with httpx.AsyncClient(timeout=timeout, verify=False) as client:
78+
79+
# Download all files concurrently
80+
tasks = []
81+
for url in DOWNLOADS:
82+
filename = url.split('/')[-1]
83+
84+
# Keep existing files
85+
if data_dir.joinpath(filename.replace('.zip', '.txt')).exists():
86+
print(f'✓ Already exists: {filename}')
87+
success_count += 1
88+
continue
89+
90+
task = download_file(client, filename, url, data_dir)
91+
tasks.append((task, filename))
92+
93+
# Wait for all downloads to complete
94+
results = await asyncio.gather(*[task for task, _ in tasks], return_exceptions=True)
95+
96+
# Process results and handle zip extraction
97+
for (_, filename), result in zip(tasks, results, strict=False):
98+
if isinstance(result, Exception):
99+
print(f'✗ Failed to download {filename}: {result}')
100+
continue
101+
elif not result:
102+
continue
103+
104+
success_count += 1
105+
106+
# Extract zip files if needed
107+
if filename.endswith('.zip'):
108+
zip_path = data_dir / filename
109+
if zip_path.exists():
110+
extract_zip(zip_path, data_dir)
111+
112+
print(f'\nCompleted: {success_count}/{len(DOWNLOADS)} files downloaded successfully')
113+
return success_count == len(DOWNLOADS)
114+
115+
116+
def main():
117+
try:
118+
success = asyncio.run(download_all_files(Path('data')))
119+
sys.exit(0 if success else 1)
120+
except KeyboardInterrupt:
121+
sys.exit('\n✗ Download interrupted by user')
122+
except Exception as e:
123+
sys.exit(f'✗ Unexpected error: {e}')
124+
125+
126+
if __name__ == '__main__':
127+
main()

0 commit comments

Comments
 (0)