|
| 1 | +import argparse |
| 2 | +import json |
| 3 | +import sys |
| 4 | +from configparser import ConfigParser |
| 5 | +from urllib import error, parse, request |
| 6 | + |
| 7 | +import style |
| 8 | + |
| 9 | +BASE_WEATHER_API_URL = "http://api.openweathermap.org/data/2.5/weather" |
| 10 | + |
| 11 | +# Weather Condition Codes |
| 12 | +# https://openweathermap.org/weather-conditions#Weather-Condition-Codes-2 |
| 13 | +THUNDERSTORM = range(200, 300) |
| 14 | +DRIZZLE = range(300, 400) |
| 15 | +RAIN = range(500, 600) |
| 16 | +SNOW = range(600, 700) |
| 17 | +ATMOSPHERE = range(700, 800) |
| 18 | +CLEAR = range(800, 801) |
| 19 | +CLOUDY = range(801, 900) |
| 20 | + |
| 21 | + |
| 22 | +def read_user_cli_args(): |
| 23 | + """Handles the CLI user interactions. |
| 24 | +
|
| 25 | + Returns: |
| 26 | + argparse.Namespace: Populated namespace object |
| 27 | + """ |
| 28 | + parser = argparse.ArgumentParser( |
| 29 | + description="gets weather and temperature information for a city" |
| 30 | + ) |
| 31 | + parser.add_argument( |
| 32 | + "City", metavar="city", nargs="+", type=str, help="enter the city name" |
| 33 | + ) |
| 34 | + parser.add_argument( |
| 35 | + "-i", |
| 36 | + "--imperial", |
| 37 | + action="store_true", |
| 38 | + help="display the temperature in imperial units", |
| 39 | + ) |
| 40 | + return parser.parse_args() |
| 41 | + |
| 42 | + |
| 43 | +def build_weather_query(city_input, imperial=False): |
| 44 | + """Builds the URL for an API request to OpenWeather's Weather API. |
| 45 | +
|
| 46 | + Args: |
| 47 | + city_input (List[str]): Name of a city as collected by argparse |
| 48 | + imperial (bool): Whether or not to use imperial units for temperature |
| 49 | +
|
| 50 | + Returns: |
| 51 | + str: URL formatted for a call to OpenWeather's city name endpoint |
| 52 | + """ |
| 53 | + api_key = _get_api_key() |
| 54 | + city_name = " ".join(city_input) |
| 55 | + url_encoded_city_name = parse.quote_plus(city_name) |
| 56 | + units = "imperial" if imperial else "metric" |
| 57 | + url = ( |
| 58 | + f"{BASE_WEATHER_API_URL}?q={url_encoded_city_name}" |
| 59 | + f"&units={units}&appid={api_key}" |
| 60 | + ) |
| 61 | + return url |
| 62 | + |
| 63 | + |
| 64 | +def _get_api_key(): |
| 65 | + """Fetch the API key from your configuration file. |
| 66 | +
|
| 67 | + Expects a configuration file named "secrets.ini" with structure: |
| 68 | +
|
| 69 | + [openweather] |
| 70 | + api_key=<YOUR-OPENWEATHER-API-KEY> |
| 71 | + """ |
| 72 | + config = ConfigParser() |
| 73 | + config.read("secrets.ini") |
| 74 | + return config["openweather"]["api_key"] |
| 75 | + |
| 76 | + |
| 77 | +def get_weather_data(query_url): |
| 78 | + """Makes an API request to a URL and returns the data as a Python object. |
| 79 | +
|
| 80 | + Args: |
| 81 | + query_url (str): URL formatted for OpenWeather's city name endpoint |
| 82 | +
|
| 83 | + Returns: |
| 84 | + dict: Weather information for a specific city |
| 85 | + """ |
| 86 | + try: |
| 87 | + response = request.urlopen(query_url) |
| 88 | + except error.HTTPError as http_error: |
| 89 | + if http_error.code == 401: # 401 - Unauthorized |
| 90 | + sys.exit("Access denied. Check your API key.") |
| 91 | + elif http_error.code == 404: # 404 - Not Found |
| 92 | + sys.exit("Can't find weather data for this city.") |
| 93 | + else: |
| 94 | + sys.exit(f"Something went wrong... ({http_error.code})") |
| 95 | + |
| 96 | + data = response.read() |
| 97 | + |
| 98 | + try: |
| 99 | + return json.loads(data) |
| 100 | + except json.JSONDecodeError: |
| 101 | + sys.exit("Couldn't read the server response.") |
| 102 | + |
| 103 | + |
| 104 | +def display_weather_info(weather_data, imperial=False): |
| 105 | + """Prints formatted weather information about a city. |
| 106 | +
|
| 107 | + Args: |
| 108 | + weather_data (dict): API response from OpenWeather by city name |
| 109 | + imperial (bool): Whether or not to use imperial units for temperature |
| 110 | +
|
| 111 | + More information at https://openweathermap.org/current#name |
| 112 | + """ |
| 113 | + city = weather_data["name"] |
| 114 | + weather_id = weather_data["weather"][0]["id"] |
| 115 | + weather_description = weather_data["weather"][0]["description"] |
| 116 | + temperature = weather_data["main"]["temp"] |
| 117 | + |
| 118 | + style.change_color(style.REVERSE) |
| 119 | + print(f"{city:^{style.PADDING}}", end="") |
| 120 | + style.change_color(style.RESET) |
| 121 | + |
| 122 | + weather_symbol, color = _select_weather_display_params(weather_id) |
| 123 | + style.change_color(color) |
| 124 | + |
| 125 | + print(f"\t{weather_symbol}", end=" ") |
| 126 | + print( |
| 127 | + f"\t{weather_description.capitalize():^{style.PADDING}}", |
| 128 | + end=" ", |
| 129 | + ) |
| 130 | + style.change_color(style.RESET) |
| 131 | + |
| 132 | + print(f"({temperature}°{'F' if imperial else 'C'})") |
| 133 | + |
| 134 | + |
| 135 | +def _select_weather_display_params(weather_id): |
| 136 | + """Selects a weather symbol and a display color for a weather state. |
| 137 | +
|
| 138 | + Args: |
| 139 | + weather_id (int): Weather condition code from the OpenWeather API |
| 140 | +
|
| 141 | + Returns: |
| 142 | + tuple[str]: Contains a weather symbol and a display color |
| 143 | + """ |
| 144 | + if weather_id in THUNDERSTORM: |
| 145 | + display_params = ("💥", style.RED) |
| 146 | + elif weather_id in DRIZZLE: |
| 147 | + display_params = ("💧", style.CYAN) |
| 148 | + elif weather_id in RAIN: |
| 149 | + display_params = ("💦", style.BLUE) |
| 150 | + elif weather_id in SNOW: |
| 151 | + display_params = ("⛄️", style.WHITE) |
| 152 | + elif weather_id in ATMOSPHERE: |
| 153 | + display_params = ("🌀", style.BLUE) |
| 154 | + elif weather_id in CLEAR: |
| 155 | + display_params = ("🔆", style.YELLOW) |
| 156 | + elif weather_id in CLOUDY: |
| 157 | + display_params = ("💨", style.WHITE) |
| 158 | + else: # In case the API adds new weather codes |
| 159 | + display_params = ("🌈", style.RESET) |
| 160 | + return display_params |
| 161 | + |
| 162 | + |
| 163 | +if __name__ == "__main__": |
| 164 | + user_args = read_user_cli_args() |
| 165 | + query_url = build_weather_query(user_args.City, user_args.imperial) |
| 166 | + weather_data = get_weather_data(query_url) |
| 167 | + display_weather_info(weather_data, user_args.imperial) |
0 commit comments