]>
Commit | Line | Data |
---|---|---|
685e346e A |
1 | # NOTE: You need to have the ZIP code database imported in order for Weather to perform ZIP code lookups |
2 | ||
3 | # Code note: In get_conditions/get_forecast, zipcode is used primarily for caching | |
4 | # While it would not strictly be necessary for the weather/etc querying to work, this code | |
5 | # assumes it is there if a lat/long is given as the 'loctation' input | |
6 | ||
7 | import datetime as dt | |
8 | from feed import get_json, FeedError | |
9 | ||
10 | ||
11 | def c_to_f(temp): | |
12 | """Convert given celcius temperature to farenheit.""" | |
13 | return ((9.0 / 5.0) * temp) + 32 | |
14 | ||
15 | ||
16 | class Weather(object): | |
17 | def __init__(self, key): | |
18 | self.API_KEY = key | |
19 | self.city_name_cache = {} # cache of names -> city id | |
20 | self.condition_cache = {} # cache of conditions, lasts 10 minutes | |
21 | self.forecast_cache = {} # cache of forecasts, lasts 30 minutes | |
22 | self.last_min_requests = [] | |
23 | self.REQ_LIMIT = 99 | |
24 | ||
25 | def api_request(self, url): | |
26 | """Perform an API request, including respecting sane request limits""" | |
27 | self.last_min_requests = [req for req in self.last_min_requests if (dt.datetime.now() - req).seconds < 60] | |
28 | ||
29 | if len(self.last_min_requests) >= self.REQ_LIMIT: | |
30 | raise WeatherException('weather data is temporarily unavailable. Try again later') | |
31 | ||
32 | self.last_min_requests.append(dt.datetime.now()) | |
33 | try: | |
34 | data = get_json(url) | |
35 | if data['cod'] == '404': | |
36 | raise WeatherException('No cities match your search query') | |
37 | return data | |
38 | except FeedError, e: | |
39 | if getattr(e, 'code', 0) == 512: # openweathermap returns this for no city, apparently. well, used to, that is | |
40 | raise WeatherException('No cities match your search query') | |
41 | else: | |
42 | raise e | |
43 | ||
44 | def get_conditions(self, location): | |
45 | """Return weather conditions for given location, using OpenWeatherMap. | |
46 | If location is string, evaluate it as city name. | |
47 | If location is list, evaluate as [latitude, longitude]. | |
48 | """ | |
49 | ||
50 | # id caching | |
51 | location_id = None | |
52 | ||
53 | if isinstance(location, str): | |
54 | if location.lower() in self.city_name_cache: | |
55 | location_id = self.city_name_cache[location.lower()] | |
56 | ||
57 | # check forecast cache, if we now have a valid ID | |
58 | if location_id is not None: | |
59 | # if we have it in our cache, and it's not expired, use that instead | |
60 | if location_id in self.condition_cache: | |
61 | weather_data = self.condition_cache.get(location_id, {'ts': 1}) | |
62 | if ((dt.datetime.now() - weather_data['ts']).seconds / 60) < 10: # 10 minutes | |
63 | return weather_data['conditions'] | |
64 | ||
65 | # we know that 'xxx' can't be a valid OpenWeatherMap key | |
66 | # and we wanna be nice to these guys, free and open and all | |
67 | if self.API_KEY == 'xxx': | |
68 | raise WeatherException('this key is not valid') | |
69 | ||
70 | if location_id is not None: | |
71 | api_data = self.api_request('http://api.openweathermap.org/data/2.5/weather?id={location_id}&APPID={key}&units=metric'.format(key=self.API_KEY, location_id=location_id)) | |
72 | elif isinstance(location, str): | |
73 | api_data = self.api_request('http://api.openweathermap.org/data/2.5/weather?q={location}&APPID={key}&units=metric'.format(key=self.API_KEY, location=location)) | |
74 | # elif type(location) == list or type(location) == tuple: | |
75 | # api_data = self.api_request('http://api.openweathermap.org/data/2.5/weather?lat={location[0]}&lon={location[1]}&APPID={key}&units=metric'.format(key=self.API_KEY, location=location)) | |
76 | else: | |
77 | raise Exception('weather: location type {} not supported'.format(type(location))) | |
78 | ||
79 | # set our location cache for next time | |
80 | if isinstance(location, str): | |
81 | self.city_name_cache[location.lower()] = api_data['id'] | |
82 | ||
83 | # silly hack | |
84 | api_data['weather'][0]['description'] = api_data['weather'][0]['description'].title() | |
85 | ||
86 | condition_data = { | |
87 | 'id': api_data['id'], | |
88 | 'city': api_data['name'] + u', ', | |
89 | 'country': api_data['sys']['country'], | |
90 | 'description': api_data['weather'][0]['description'].title().replace(' Is ', ' is '), | |
91 | 'temp_c': api_data['main']['temp'], | |
92 | 'temp_f': c_to_f(api_data['main']['temp']), # convert it manually | |
93 | 'pressure': api_data['main']['pressure'], | |
94 | 'humidity': api_data['main']['humidity'], | |
95 | 'rain': 'No Data Avaliable', | |
96 | } | |
97 | ||
98 | # make the country actually consistent | |
99 | if condition_data['country'] in ['United States of America', 'US']: | |
100 | condition_data['country'] = 'USA' | |
101 | ||
102 | if not api_data['name']: # this is a country, not a specific city | |
103 | condition_data['city'] = '' | |
104 | ||
105 | if 'rain' in api_data: # can be included or not, eg Japan | |
106 | rain_time = api_data['rain'].keys()[0] # since rain time can be '1h', '3h', etc | |
107 | condition_data['rain'] = '{}mm/{}'.format(api_data['rain'][rain_time], rain_time) | |
108 | ||
109 | # and set our data, with appropriate timeout | |
110 | self.condition_cache[api_data['id']] = { | |
111 | 'ts': dt.datetime.now(), | |
112 | 'conditions': condition_data, | |
113 | } | |
114 | ||
115 | return condition_data | |
116 | ||
117 | def get_forecast(self, location): | |
118 | """Return weather forecast for given location, using OpenWeatherMap. | |
119 | If location is string, evaluate it as city name. | |
120 | If location is list, evaluate as [latitude, longitude]. | |
121 | """ | |
122 | ||
123 | # id caching | |
124 | location_id = None | |
125 | ||
126 | if isinstance(location, str): | |
127 | if location.lower() in self.city_name_cache: | |
128 | location_id = self.city_name_cache[location.lower()] | |
129 | ||
130 | # check forecast cache, if we now have a valid ID | |
131 | if location_id is not None: | |
132 | # if we have it in our cache, and it's not expired, use that instead | |
133 | if location_id in self.forecast_cache: | |
134 | weather_data = self.forecast_cache.get(location_id, {'ts': 1}) | |
135 | if ((dt.datetime.now() - weather_data['ts']).seconds / 60) < 10: # 10 minutes | |
136 | return weather_data['forecast'] | |
137 | ||
138 | # we know that 'xxx' can't be a valid OpenWeatherMap key | |
139 | # and we wanna be nice to these guys, free and open and all | |
140 | if self.API_KEY == 'xxx': | |
141 | raise WeatherException('this key is not valid') | |
142 | ||
143 | if location_id is not None: | |
144 | api_data = self.api_request('http://api.openweathermap.org/data/2.5/forecast?id={location_id}&APPID={key}&units=metric'.format(key=self.API_KEY, location_id=location_id)) | |
145 | elif isinstance(location, str): | |
146 | api_data = self.api_request('http://api.openweathermap.org/data/2.5/forecast?q={location}&APPID={key}&units=metric'.format(key=self.API_KEY, location=location)) | |
147 | else: | |
148 | raise Exception('get_conditions: location type {} not supported'.format(type(location))) | |
149 | ||
150 | # set our location cache for next time | |
151 | if isinstance(location, str): | |
152 | self.city_name_cache[location.lower()] = api_data['city']['id'] | |
153 | ||
154 | forecast_data = { | |
155 | 'id': api_data['city']['id'], | |
156 | 'city': api_data['city']['name'] + u', ', | |
157 | 'country': api_data['city']['country'], | |
158 | 'days': [], | |
159 | } | |
160 | ||
161 | # make the country actually consistent | |
162 | if forecast_data['country'] in ['United States of America', 'US']: | |
163 | forecast_data['country'] = 'USA' | |
164 | ||
165 | if not api_data['city']['name']: # this is a country, not a specific city | |
166 | forecast_data['city'] = '' | |
167 | ||
168 | # assemble and insert specific day data | |
169 | today = dt.datetime.now() | |
170 | i = 0 | |
171 | ||
172 | for day in api_data['list'][:4]: | |
173 | day_data = { | |
174 | 'name': (today + dt.timedelta(days=i)).strftime('%A'), | |
175 | 'description': day['weather'][0]['description'].title().replace(' Is ', ' is '), | |
176 | 'min_c': int(day['main']['temp_min']), | |
177 | 'min_f': int(c_to_f(day['main']['temp_min'])), | |
178 | 'max_c': int(day['main']['temp_max']), | |
179 | 'max_f': int(c_to_f(day['main']['temp_max'])), | |
180 | } | |
181 | ||
182 | forecast_data['days'].append(day_data) | |
183 | i += 1 | |
184 | ||
185 | # and set our data, with appropriate timeout | |
186 | self.forecast_cache[api_data['city']['id']] = { | |
187 | 'ts': dt.datetime.now(), | |
188 | 'forecast': forecast_data, | |
189 | } | |
190 | ||
191 | return forecast_data | |
192 | ||
193 | ||
194 | class WeatherException(Exception): | |
195 | def __init__(self, msg): | |
196 | self.msg = msg | |
197 | ||
198 | def __str__(self): | |
199 | return str(self.msg) |