diff --git a/HW 4/__pycache__/hw4.cpython-314.pyc b/HW 4/__pycache__/hw4.cpython-314.pyc new file mode 100644 index 0000000..f170142 Binary files /dev/null and b/HW 4/__pycache__/hw4.cpython-314.pyc differ diff --git a/HW 4/hw4.py b/HW 4/hw4.py new file mode 100644 index 0000000..125f815 --- /dev/null +++ b/HW 4/hw4.py @@ -0,0 +1,251 @@ +""" +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()) diff --git a/modify.sh b/modify.sh new file mode 100644 index 0000000..a5b8e28 --- /dev/null +++ b/modify.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +BASE_DIR="$HOME/dcs211_exam" + +cd "$BASE_DIR"/data + +mv notes.txt lab_notes.txt + +mv "$BASE_DIR"/data/lab3.py "$BASE_DIR"/backup/lab3.py + +cp "$BASE_DIR"/data/lab1.py "$BASE_DIR"/scripts/lab1.py + +rm "$BASE_DIR"/data/lab2.py \ No newline at end of file diff --git a/setup_exam.sh b/setup_exam.sh new file mode 100644 index 0000000..cf3aaa4 --- /dev/null +++ b/setup_exam.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +BASE_DIR="$HOME/dcs211_exam" + +mkdir "$BASE_DIR"/data +mkdir "$BASE_DIR"/scripts +mkdir "$BASE_DIR"/backup + +cd "$BASE_DIR"/data + +touch lab1.py +touch lab2.py +touch lab3.py +touch notes.txt +touch image1.png +touch image2.jpg + +ls *.py + +ls *.png +ls *.jpg +ls *.gif \ No newline at end of file