Skip to content

Commit f8fd747

Browse files
authored
Add weather fetching and display script
This script fetches and displays weather information for a specified city using the OpenWeather API, with options for temperature units.
1 parent cb17b1e commit f8fd747

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)