252 lines
7.7 KiB
Python
252 lines
7.7 KiB
Python
"""
|
|
HW 4: Open-Meteo Weather Utility
|
|
Benjamin Adovasio
|
|
API documentation: https://open-meteo.com/en/docs
|
|
|
|
Description:
|
|
This utility fetches current weather conditions from the Open-Meteo
|
|
Forecast API. You provide a latitude and longitude on the command line,
|
|
and the script prints a small weather report for that location.
|
|
|
|
How to run:
|
|
python3 hw4.py --lat 44.1004 --lon -70.2148
|
|
python3 hw4.py --lat 44.1004 --lon -70.2148 --temp-unit fahrenheit --wind-unit mph
|
|
python3 hw4.py --lat 44.1004 --lon -70.2148 --timezone America/New_York
|
|
"""
|
|
|
|
import sys
|
|
from typing import Dict, List, Optional
|
|
|
|
|
|
BASE_URL = "https://api.open-meteo.com/v1/forecast"
|
|
CURRENT_FIELDS = [
|
|
"temperature_2m",
|
|
"relative_humidity_2m",
|
|
"apparent_temperature",
|
|
"is_day",
|
|
"precipitation",
|
|
"weather_code",
|
|
"cloud_cover",
|
|
"wind_speed_10m",
|
|
"wind_direction_10m",
|
|
]
|
|
|
|
#weather codes coppied from api docs
|
|
WEATHER_CODES = {
|
|
0: "Clear sky",
|
|
1: "Mainly clear",
|
|
2: "Partly cloudy",
|
|
3: "Overcast",
|
|
45: "Fog",
|
|
48: "Depositing rime fog",
|
|
51: "Light drizzle",
|
|
53: "Moderate drizzle",
|
|
55: "Dense drizzle",
|
|
56: "Light freezing drizzle",
|
|
57: "Dense freezing drizzle",
|
|
61: "Slight rain",
|
|
63: "Moderate rain",
|
|
65: "Heavy rain",
|
|
66: "Light freezing rain",
|
|
67: "Heavy freezing rain",
|
|
71: "Slight snow fall",
|
|
73: "Moderate snow fall",
|
|
75: "Heavy snow fall",
|
|
77: "Snow grains",
|
|
80: "Slight rain showers",
|
|
81: "Moderate rain showers",
|
|
82: "Violent rain showers",
|
|
85: "Slight snow showers",
|
|
86: "Heavy snow showers",
|
|
95: "Thunderstorm",
|
|
96: "Thunderstorm with slight hail",
|
|
99: "Thunderstorm with heavy hail",
|
|
}
|
|
|
|
|
|
def print_help() -> None:
|
|
"""Print a help message describing how to use the utility."""
|
|
print(
|
|
"""Open-Meteo Weather Utility
|
|
|
|
Usage:
|
|
python3 hw4.py --lat LATITUDE --lon LONGITUDE [options]
|
|
|
|
Required arguments:
|
|
--lat LATITUDE Latitude for the weather lookup (-90 to 90)
|
|
--lon LONGITUDE Longitude for the weather lookup (-180 to 180)
|
|
|
|
Optional arguments found via api docs:
|
|
--temp-unit UNIT celsius or fahrenheit (default: celsius)
|
|
--wind-unit UNIT kmh, ms, mph, or kn (default: kmh)
|
|
--timezone ZONE Time zone name or auto (default: auto)
|
|
--help Show this help message
|
|
"""
|
|
)
|
|
|
|
|
|
def parse_args(argv: List[str]) -> Optional[Dict[str, str]]:
|
|
"""Parse command-line arguments using sys.argv-style input."""
|
|
if not argv or "--help" in argv:
|
|
print_help()
|
|
return None
|
|
|
|
option_map = {
|
|
"--lat": "latitude",
|
|
"--lon": "longitude",
|
|
"--temp-unit": "temperature_unit",
|
|
"--wind-unit": "wind_speed_unit",
|
|
"--timezone": "timezone",
|
|
}
|
|
|
|
parsed = {
|
|
"temperature_unit": "celsius",
|
|
"wind_speed_unit": "kmh",
|
|
"timezone": "auto",
|
|
}
|
|
|
|
index = 0
|
|
while index < len(argv):
|
|
flag = argv[index]
|
|
|
|
if flag not in option_map:
|
|
raise ValueError(f"Unknown argument: {flag}")
|
|
|
|
if index + 1 >= len(argv):
|
|
raise ValueError(f"Missing value after {flag}")
|
|
|
|
parsed[option_map[flag]] = argv[index + 1]
|
|
index += 2
|
|
|
|
if "latitude" not in parsed or "longitude" not in parsed:
|
|
raise ValueError("You must provide both --lat and --lon.")
|
|
|
|
try:
|
|
latitude = float(parsed["latitude"])
|
|
longitude = float(parsed["longitude"])
|
|
except ValueError as err:
|
|
raise ValueError("Latitude and longitude must be numbers.") from err
|
|
|
|
if not -90 <= latitude <= 90:
|
|
raise ValueError("Latitude must be between -90 and 90.")
|
|
|
|
if not -180 <= longitude <= 180:
|
|
raise ValueError("Longitude must be between -180 and 180.")
|
|
|
|
if parsed["temperature_unit"] not in {"celsius", "fahrenheit"}:
|
|
raise ValueError("Temperature unit must be celsius or fahrenheit.")
|
|
|
|
if parsed["wind_speed_unit"] not in {"kmh", "ms", "mph", "kn"}:
|
|
raise ValueError("Wind unit must be kmh, ms, mph, or kn.")
|
|
|
|
parsed["latitude"] = str(latitude)
|
|
parsed["longitude"] = str(longitude)
|
|
return parsed
|
|
|
|
|
|
def weather_description(code: int) -> str:
|
|
"""Return a readable weather description for an Open-Meteo WMO code."""
|
|
return WEATHER_CODES.get(code, "Unknown weather condition")
|
|
|
|
|
|
def fetch_weather(query_options: Dict[str, str]) -> Dict:
|
|
"""Send the GET request to Open-Meteo and return the JSON response."""
|
|
try:
|
|
import requests
|
|
except ModuleNotFoundError as err:
|
|
raise RuntimeError(
|
|
"The requests library is not installed. Install it with: "
|
|
"python3 -m pip install requests"
|
|
) from err
|
|
|
|
# Open-Meteo parameters used in this utility:
|
|
# - latitude / longitude: required coordinates for the requested location.
|
|
# - current: tells the API which current-condition fields to include.
|
|
# - temperature_unit: lets the user choose Celsius or Fahrenheit.
|
|
# - wind_speed_unit: lets the user choose km/h, m/s, mph, or knots.
|
|
# - timezone: returns times in the requested zone, or auto-detects the zone.
|
|
params = {
|
|
"latitude": query_options["latitude"],
|
|
"longitude": query_options["longitude"],
|
|
"current": ",".join(CURRENT_FIELDS),
|
|
"temperature_unit": query_options["temperature_unit"],
|
|
"wind_speed_unit": query_options["wind_speed_unit"],
|
|
"timezone": query_options["timezone"],
|
|
}
|
|
|
|
try:
|
|
response = requests.get(BASE_URL, params=params, timeout=10)
|
|
response.raise_for_status()
|
|
except requests.exceptions.RequestException as err:
|
|
raise ConnectionError(f"Request failed: {err}") from err
|
|
|
|
payload = response.json()
|
|
if payload.get("error"):
|
|
raise ValueError(payload.get("reason", "The API returned an unknown error."))
|
|
|
|
return payload
|
|
|
|
|
|
def display_weather_report(payload: Dict) -> None:
|
|
"""Print a readable weather report from the API response."""
|
|
current = payload["current"]
|
|
units = payload["current_units"]
|
|
code = current["weather_code"]
|
|
daylight = "Day" if current["is_day"] == 1 else "Night"
|
|
|
|
print("Open-Meteo Weather Report")
|
|
print(f"Coordinates: {payload['latitude']}, {payload['longitude']}")
|
|
print(f"Timezone: {payload['timezone']}")
|
|
print(f"Observed at: {current['time']}")
|
|
print(f"Condition: {weather_description(code)} (WMO code {code})")
|
|
print(f"Day/Night: {daylight}")
|
|
print(f"Temperature: {current['temperature_2m']} {units['temperature_2m']}")
|
|
print(
|
|
f"Feels like: {current['apparent_temperature']} "
|
|
f"{units['apparent_temperature']}"
|
|
)
|
|
print(
|
|
f"Humidity: {current['relative_humidity_2m']} "
|
|
f"{units['relative_humidity_2m']}"
|
|
)
|
|
print(f"Precipitation: {current['precipitation']} {units['precipitation']}")
|
|
print(f"Cloud cover: {current['cloud_cover']} {units['cloud_cover']}")
|
|
print(
|
|
f"Wind: {current['wind_speed_10m']} {units['wind_speed_10m']} "
|
|
f"at {current['wind_direction_10m']} {units['wind_direction_10m']}"
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
"""Coordinate argument parsing, API access, and report output."""
|
|
try:
|
|
query_options = parse_args(sys.argv[1:])
|
|
if query_options is None:
|
|
return 0
|
|
|
|
payload = fetch_weather(query_options)
|
|
display_weather_report(payload)
|
|
return 0
|
|
|
|
except ValueError as err:
|
|
print(f"Error: {err}")
|
|
print()
|
|
print_help()
|
|
return 1
|
|
|
|
except RuntimeError as err:
|
|
print(err)
|
|
return 1
|
|
|
|
except ConnectionError as err:
|
|
print(err)
|
|
return 1
|
|
|
|
except KeyError as err:
|
|
print(f"Unexpected API response. Missing field: {err}")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|