-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcli.py
More file actions
executable file
·235 lines (202 loc) · 7.35 KB
/
cli.py
File metadata and controls
executable file
·235 lines (202 loc) · 7.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#!/usr/bin/env python
import os
import re
import click
import requests
from string import Template
class ApiKey(click.ParamType):
name = 'api-key'
def convert(self, value, param, ctx):
found = re.match(r'[0-9a-f]{32}', value)
if not found:
self.fail(
f'{value} is not a 32-character hexadecimal string',
param,
ctx,
)
return value
def degrees_to_cardinal(degrees):
'''
Return the cardinal direction representing a given 'degrees'
'''
cardinal = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE',
'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']
cardinal_len = len(cardinal)
ix = round(degrees / (360.0 / cardinal_len))
return cardinal[ix % cardinal_len]
def current_weather(location, api_key, units):
url = 'http://api.openweathermap.org/data/2.5/weather'
query_params = {
# If using City ID, set the 'id' parameter
#'id': location,
# Otherwise, we are using City names, 'q' parameter
# TODO: Make the above into a flag so the user can choose
# between City ID or City Name
'q': location,
'units': units,
'appid': api_key,
}
response = requests.get(url, params=query_params)
# Check the HTTP response. If we receive a 4XX or 5XX error, raise
# an exception so it may be dealt with.
response.raise_for_status()
return response.json()
@click.group()
@click.option(
'--api-key', '-a',
type=ApiKey(),
help='Your OpenWeatherMap API key')
@click.option(
'--config-file', '-c',
type=click.Path(),
default='~/.weather.cfg')
@click.pass_context
def main(ctx, api_key, config_file):
'''
A simple weather tool that shows the current weather condition in a
LOCATION of your choice. Provide the city name, and optionally a two-digit
country code. If the city name contains spaces, it should be enclosed in
quotation marks. Examples:
1. "New York City,US"
2. Berlin
You need a valid API key from OpenWeatherMap for this tool to work. You
can sign up for a free account at https://openweathermap.org/appid.
'''
filename = os.path.expanduser(config_file)
if not api_key and os.path.exists(filename):
with open(filename) as cfg:
api_key = cfg.read()
ctx.obj = {
'api_key': api_key,
'config_file': filename,
}
@main.command()
@click.pass_context
def config(ctx):
'''
Store the OpenWeatherMap API key in configuration file.
'''
config_file = ctx.obj['config_file']
api_key = click.prompt(
'Please enter your OpenWeatherMap API key',
default=ctx.obj.get('api_key', '')
)
with open(config_file, 'w') as cfg:
cfg.write(api_key)
# TODO: More flags to add -- not necessarily all, but these look good
# - sunrise/sunset
# - air pressure
# - visibility
# - data updated time
# - cloud cover
@main.command()
@click.argument('location')
@click.option('--units', '-u',
type=click.Choice(['standard', 'metric', 'imperial'],
case_sensitive=False),
default='standard',
help='Units for displayed data')
@click.option('--pretty', '-p',
type=click.Choice(['short', 'long', 'verbose',],
case_sensitive=False),
default='short',
help='Enable extra output text, and formatting')
@click.option('--conditions', '-C', is_flag=True,
help='Display current conditions text')
@click.option('--temperature', '-t', is_flag=True,
help='Show current temperature (includes high/low)')
@click.option('--humidity', '-h', is_flag=True,
help='Show current relative humidity')
@click.option('--wind', '-w', is_flag=True,
help='Show current wind speed and direction')
@click.pass_context
def current(ctx, location, units, pretty, conditions, temperature, humidity,\
wind):
'''
Show the current weather for a location using OpenWeatherMap data.
'''
api_key = ctx.obj['api_key']
try:
weather = current_weather(location, api_key, units)
except requests.HTTPError as error:
print(f'HTTP Error!\n{error}')
return
except requests.ConnectionError as error:
print(f'Connection Error!\n{error}')
return
except requests.Timeout as error:
print(f'Timed Out!\n{error}')
return
current_conditions = {'location': location,}
# Choose an output template based on the chosen option
if pretty == 'short':
# Default, short & to the point
template = Template(
'''$conditions | $temperature | $humidity | $wind_dir at $wind_speed'''
)
elif pretty == 'long':
# Longer form, additional formatting
template = Template(
'''Current conditions for ${location} are
$conditions at $temperature
${humidity}, winds $wind_dir at $wind_speed'''
)
elif pretty == 'verbose':
# Describe the full output
# TODO: Pretty print into a table!
template = Template(
'''Location: $location
Conditions: $conditions
Temperature (High, Current, Low): $temperature
Relative Humidity: $humidity
Wind (Direction, Speed): $wind_dir, $wind_speed'''
)
if conditions:
if 'weather' in weather:
current_conditions['conditions'] = \
weather['weather'][0]['description'].capitalize()
else:
current_conditions['conditions'] = ''
if temperature:
if 'temp_max' in weather['main'] and \
'temp_min' in weather['main'] and \
'temp' in weather['main']:
temp_units = {
'imperial': 'F',
'metric': 'C',
'standard': 'K',
}
# TODO: Should we break these values out individually too?
current_conditions['temperature'] = '↑' + \
str(weather['main']['temp_max']) + f'°{temp_units[units]}, ' +\
str(weather['main']['temp']) + f'°{temp_units[units]}, ↓' + \
str(weather['main']['temp_min']) + f'°{temp_units[units]}'
else:
current_conditions['temperature'] = ''
if humidity:
if 'humidity' in weather['main']:
current_conditions['humidity'] = \
str(weather['main']['humidity']) + '%RH'
else:
current_conditions['humidity'] = ''
if wind:
if 'wind' in weather:
if 'speed' in weather['wind'] and 'deg' in weather['wind']:
wind_units = {
'imperial': 'mph',
'metric': 'm/s',
'standard': 'm/s',
}
wind_cardinal = degrees_to_cardinal(weather['wind']['deg'])
current_conditions['wind_dir'] = wind_cardinal
current_conditions['wind_speed'] = \
str(weather['wind']['speed']) + f'{wind_units[units]}'
else:
current_conditions['wind_dir'] = ''
current_conditions['wind_speed'] = ''
# Pass our conditions dictionary into the 'pretty' template and
# substitue our weather data in for display.
output_text = template.safe_substitute(**current_conditions)
print(output_text)
if __name__ == '__main__':
main()