Files
Climate-Mood-Analysis/scripts/download_gridmet_data.py
T
2026-06-11 14:50:33 -04:00

144 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""
Download gridMET daily NetCDF files into year-based folders.
Default layout:
data/gridmet/1991/sph.nc
data/gridmet/1991/rmax.nc
data/gridmet/1991/rmin.nc
gridMET source:
https://www.northwestknowledge.net/metdata/data/{variable}_{year}.nc
"""
from __future__ import annotations
import argparse
import sys
import tempfile
import urllib.error
import urllib.request
from pathlib import Path
DEFAULT_BASE_URL = "https://www.northwestknowledge.net/metdata/data"
DEFAULT_VARIABLES = ("sph", "rmax", "rmin")
def _parse_years(text: str) -> list[int]:
years: set[int] = set()
for part in text.split(","):
token = part.strip()
if not token:
continue
if "-" in token:
start_text, end_text = token.split("-", 1)
start = int(start_text)
end = int(end_text)
if start > end:
raise argparse.ArgumentTypeError(f"Invalid descending year range: {token}")
years.update(range(start, end + 1))
else:
years.add(int(token))
if not years:
raise argparse.ArgumentTypeError("At least one year is required.")
return sorted(years)
def _parse_variables(text: str) -> list[str]:
variables = [part.strip() for part in text.split(",") if part.strip()]
if not variables:
raise argparse.ArgumentTypeError("At least one variable is required.")
return variables
def _download_file(url: str, destination: Path, overwrite: bool) -> bool:
if destination.exists() and not overwrite:
print(f"skip existing {destination}")
return False
destination.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile(
prefix=destination.stem + ".",
suffix=".download",
dir=destination.parent,
delete=False,
) as handle:
temp_path = Path(handle.name)
try:
print(f"download {url}")
with urllib.request.urlopen(url) as response, temp_path.open("wb") as output:
total_text = response.headers.get("Content-Length")
total = int(total_text) if total_text and total_text.isdigit() else None
copied = 0
while True:
chunk = response.read(1024 * 1024)
if not chunk:
break
output.write(chunk)
copied += len(chunk)
if total:
percent = copied / total * 100
print(f"\r {copied / 1024 / 1024:,.1f} MiB / {total / 1024 / 1024:,.1f} MiB ({percent:5.1f}%)", end="")
if total:
print()
temp_path.replace(destination)
print(f"saved {destination}")
return True
except (urllib.error.URLError, urllib.error.HTTPError, OSError) as exc:
temp_path.unlink(missing_ok=True)
print(f"failed {url}: {exc}", file=sys.stderr)
raise
def main() -> int:
parser = argparse.ArgumentParser(
description="Download gridMET NetCDF files into data/gridmet/<year>/<variable>.nc."
)
parser.add_argument(
"--years",
type=_parse_years,
default=_parse_years("1991-2020"),
help="Comma-separated years/ranges. Default: 1991-2020.",
)
parser.add_argument(
"--variables",
type=_parse_variables,
default=list(DEFAULT_VARIABLES),
help="Comma-separated gridMET variables. Default: sph,rmax,rmin.",
)
parser.add_argument(
"--output-dir",
type=Path,
default=Path("data/gridmet"),
help="Root folder for downloaded files. Default: data/gridmet.",
)
parser.add_argument(
"--base-url",
default=DEFAULT_BASE_URL,
help=f"Base gridMET download URL. Default: {DEFAULT_BASE_URL}.",
)
parser.add_argument(
"--overwrite",
action="store_true",
help="Redownload files that already exist.",
)
args = parser.parse_args()
downloaded = 0
for year in args.years:
for variable in args.variables:
url = f"{args.base_url.rstrip('/')}/{variable}_{year}.nc"
destination = args.output_dir / str(year) / f"{variable}.nc"
if _download_file(url, destination, args.overwrite):
downloaded += 1
print(f"done; downloaded {downloaded} file(s)")
return 0
if __name__ == "__main__":
raise SystemExit(main())