Skip to content

Commit 8eee00f

Browse files
authored
Merge pull request #235 from realpython/weather-app
Add tutorial code for Weather App SbSP
2 parents a832a7d + e269cce commit 8eee00f

File tree

17 files changed

+717
-0
lines changed

17 files changed

+717
-0
lines changed

weather-app/README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# Weather CLI
2+
3+
> See the world's weather 🌤 at home through your CLI window.
4+
5+
View weather and temperature information for different cities around the world right inside your CLI:
6+
7+
![CLI interactions using the weather CLI app](weather.gif)
8+
9+
Follow the tutorial to [Build This Weather CLI App with Python](https://realpython.com/build-a-python-weather-app-cli/) using only modules from Python's standard library.
10+
11+
## Requirements
12+
13+
To run this CLI app, you need a modern installation of [Python](https://www.python.org/) >= 3.6.
14+
15+
**Note:** This app was developed on macOS for the [Zsh](https://www.zsh.org) shell. Your mileage with some of the color display and emojis may vary depending on your setup. On Windows, [PowerShell](https://docs.microsoft.com/en-us/powershell/) on [Windows Terminal](https://github.com/microsoft/terminal) displays the formatting reasonably well.
16+
17+
You also need an [API key for OpenWeather's API](https://openweathermap.org/appid). After signing up and generating your API key, add it to the `secrets.ini` config file:
18+
19+
```
20+
[openweathermap]
21+
api_key = <YOUR-OPENWEATHERAPP-API-KEY>
22+
```
23+
24+
**Note:** Make sure that `secrets.ini` is not tracked by your version control system, to avoid leaking your API key to the public.
25+
26+
For more information on the OpenWeather API used in this CLI app, check out the [documentation on the city name endpoint](https://openweathermap.org/current#name).
27+
28+
## Usage example
29+
30+
Once you're set up, you can call the weather CLI by running:
31+
32+
```bash
33+
$ python weather.py <CITY_NAME> [-i]
34+
```
35+
36+
For example, to get the current weather in Vienna, Austria, you can run the following command:
37+
38+
```bash
39+
$ python weather.py Vienna
40+
```
41+
42+
To display the temperature in Fahrenheit, you can add the optional `-i` or `--imperial` flag.
43+
44+
_For more examples and usage, please refer to the [associated Real Python tutorial](https://realpython.com/build-a-python-weather-app-cli/)._
45+
46+
## Development setup
47+
48+
To run and continue development on this CLI project, you need to have **Python>=3.6** installed on your system.
49+
50+
## Release History
51+
52+
- 0.1.0
53+
- The first proper release
54+
55+
## Meta
56+
57+
Martin Breuss – [@martinbreuss](https://twitter.com/martinbreuss)[email protected]
58+
59+
Distributed under the [MIT License](https://opensource.org/licenses/MIT).
60+
61+
[https://github.com/martin-martin](https://github.com/martin-martin/)
62+
63+
## Contributing
64+
65+
1. Fork it (<https://github.com/realpython/materials/fork>)
66+
2. Create your feature branch (`git checkout -b feature/fooBar`)
67+
3. Commit your changes (`git commit -am 'Add some fooBar'`)
68+
4. Push to the branch (`git push origin feature/fooBar`)
69+
5. Create a new Pull Request
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
secrets.ini
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import sys
2+
3+
4+
PADDING = 20
5+
6+
RED = "\033[1;31m"
7+
BLUE = "\033[1;34m"
8+
CYAN = "\033[1;36m"
9+
GREEN = "\033[0;32m"
10+
YELLOW = "\033[33m"
11+
WHITE = "\033[37m"
12+
13+
REVERSE = "\033[;7m"
14+
RESET = "\033[0m"
15+
16+
17+
def change_color(color):
18+
sys.stdout.write(color)
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
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)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
There is no code here!
2+
3+
This step is all about obtaining your API key.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
secrets.ini
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
from configparser import ConfigParser
2+
3+
4+
def _get_api_key():
5+
"""Fetch the API key from your configuration file.
6+
7+
Expects a configuration file named "secrets.ini" with structure:
8+
9+
[openweather]
10+
api_key=<YOUR-OPENWEATHER-API-KEY>
11+
"""
12+
config = ConfigParser()
13+
config.read("secrets.ini")
14+
return config["openweather"]["api_key"]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
secrets.ini
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import argparse
2+
from configparser import ConfigParser
3+
4+
5+
def read_user_cli_args():
6+
"""Handles the CLI user interactions.
7+
8+
Returns:
9+
argparse.Namespace: Populated namespace object
10+
"""
11+
parser = argparse.ArgumentParser(
12+
description="gets weather and temperature information for a city"
13+
)
14+
parser.add_argument(
15+
"City", metavar="city", nargs="+", type=str, help="enter the city name"
16+
)
17+
parser.add_argument(
18+
"-i",
19+
"--imperial",
20+
action="store_true",
21+
help="display the temperature in imperial units",
22+
)
23+
return parser.parse_args()
24+
25+
26+
def _get_api_key():
27+
"""Fetch the API key from your configuration file.
28+
29+
Expects a configuration file named "secrets.ini" with structure:
30+
31+
[openweather]
32+
api_key=<YOUR-OPENWEATHER-API-KEY>
33+
"""
34+
config = ConfigParser()
35+
config.read("secrets.ini")
36+
return config["openweather"]["api_key"]
37+
38+
39+
if __name__ == "__main__":
40+
user_args = read_user_cli_args()
41+
print(user_args.City, user_args.imperial)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
secrets.ini

0 commit comments

Comments
 (0)