Skip to content

Commit 32eb637

Browse files
authored
Add weather fetching script using OpenWeather API
This script fetches and displays weather information for a specified city using the OpenWeather API. It supports both metric and imperial temperature units.
1 parent cb17b1e commit 32eb637

1 file changed

Lines changed: 189 additions & 0 deletions

File tree

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

0 commit comments

Comments
 (0)