Version 1.0.2
[python-thefuckingweather.git] / thefuckingweather.py
blobfa4349c5478c2e20ac98c67853fc10984e81edc6
1 # coding: utf-8
3 # Python API for The Fucking Weather, version 1.0.2
4 # Copyright (C) 2012 Red Hat, Inc. and others
5 # https://github.com/ianweller/python-thefuckingweather
7 # This program is free software. It comes without any warranty, to
8 # the extent permitted by applicable law. You can redistribute it
9 # and/or modify it under the terms of the Do What The Fuck You Want
10 # To Public License, Version 2, as published by Sam Hocevar. See
11 # http://sam.zoy.org/wtfpl/COPYING for more details.
13 # Credits:
14 # - Colin Rice for fixing the regexps to allow for negative temperatures
16 """Scrapes data from www.thefuckingweather.com for a given location."""
18 from optparse import OptionParser
19 import re
20 import urllib
21 import urllib2
23 RE_WEATHER = """<div style="float: left;"><span class="small">(.*)</span></div>
24 <iframe id="fbook" .*>.*</iframe>
25 <div id="content"><div class="large" >(-?\d*)&deg;\?!<br />
26 <br />(.*)</div><div id="remark"><br />
27 <span>(.*)</span></div>"""
29 RE_FORECAST = """<div class="boxhead">
30 <h2>THE FUCKING FORECAST</h2>
31 </div>
32 <div class="boxbody">
33 <table><tr>
34 <td>DAY:</td>
35 <td class="center"><strong>(.{3})</strong></td>
36 <td class="center"><strong>(.{3})</strong></td>
37 </tr>
38 <tr>
39 <td>HIGH:</td><td class="center hot">(-?\d*)</td>\
40 <td class="center hot">(-?\d*)</td>
41 </tr>
42 <tr>
43 <td>LOW:</td><td class="center cold">(-?\d*)</td>\
44 <td class="center cold">(-?\d*)</td>
45 </tr>
46 <tr>
47 <td>FORECAST:</td><td class="center">(.*)</td><td class="center">(.*)</td></tr>
48 </table>
49 </div>"""
51 DEGREE_SYMBOL = "°"
54 class LocationError(StandardError):
55 """
56 The website reported a "WRONG FUCKING ZIP" error, which could mean either
57 the server has no clue what to do with your location or that it messed up.
58 """
60 def __init__(self):
61 StandardError.__init__(self, "WRONG FUCKING ZIP returned from website")
64 class ParseError(StandardError):
65 """
66 Something is wrong with the regexps or the site owner updated his template.
67 """
69 def __init__(self):
70 StandardError.__init__(
71 self, """Couldn't parse the website.
72 RE: %s
74 Please report what you did to get this error and this full Python traceback
75 to ian@ianweller.org. Thanks!""" % RE_WEATHER)
78 def get_weather(location, celsius=False):
79 """
80 Retrieves weather and forecast data for a given location.
82 Data is presented in a dict with three main elements: "location" (the
83 location presented by TFW), "current" (current weather data) and "forecast"
84 (a forecast of the next two days, with highs, lows, and what the weather
85 will be like).
87 "current" is a dictionary with three elements: "temperature" (an integer),
88 "weather" (a list of descriptive elements about the weather, e.g., "ITS
89 FUCKING HOT", which may be coupled with something such as "AND THUNDERING")
90 and "remark" (a string printed by the server which is meant to be witty but
91 is sometimes not. each to their own, I guess).
93 "forecast" is a dictionary with two elements, 0 and 1 (both integers). Each
94 of these is a dictionary which contains the keys "day" (a three-letter
95 string consisting of the day of week), "high" and "low" (integers
96 representing the relative extreme temperature of the day) and "weather" (a
97 basic description of the weather, such as "Scattered Thunderstorms").
99 The default is for temperatures to be in Fahrenheit. If you're so inclined,
100 you can pass True as a second variable and get temperatures in Celsius.
102 If you need a degree symbol, you can use thefuckingweather.DEGREE_SYMBOL.
104 # Retrieve yummy HTML
105 query = {"zipcode": location}
106 if celsius:
107 query["CELSIUS"] = "yes"
108 query_string = urllib.urlencode(query)
109 url = "http://www.thefuckingweather.com/?" + query_string
110 data = urllib2.urlopen(url).read()
111 # Check for an error report
112 if re.search("WRONG FUCKING ZIP", data):
113 raise LocationError()
114 # No error, so parse current weather data
115 return_val = {"current": {}, "forecast": {0: {}, 1: {}}}
116 weather_search = re.search(RE_WEATHER, data)
117 if not weather_search:
118 raise ParseError()
119 return_val["location"] = weather_search.group(1)
120 return_val["current"]["temperature"] = int(weather_search.group(2))
121 return_val["current"]["weather"] = weather_search.group(3).split(
122 "<br />")
123 return_val["current"]["remark"] = weather_search.group(4)
124 # Now parse the forecast data
125 forecast_search = re.search(RE_FORECAST, data)
126 if not forecast_search:
127 raise ParseError()
128 return_val["forecast"][0]["day"] = forecast_search.group(1)
129 return_val["forecast"][0]["high"] = int(forecast_search.group(3))
130 return_val["forecast"][0]["low"] = int(forecast_search.group(5))
131 return_val["forecast"][0]["weather"] = forecast_search.group(7)
132 return_val["forecast"][1]["day"] = forecast_search.group(2)
133 return_val["forecast"][1]["high"] = int(forecast_search.group(4))
134 return_val["forecast"][1]["low"] = int(forecast_search.group(6))
135 return_val["forecast"][1]["weather"] = forecast_search.group(8)
136 # I'm gonna have to jump!
137 return return_val
140 def main():
142 This function is run when the python file is run from the command line. It
143 prints content formatted somewhat like www.thefuckingweather.com. You can
144 use the -c (--celsius) switch to return temperatures in Celsius.
146 usage = "usage: %prog [-c] location"
147 parser = OptionParser(usage=usage)
148 parser.add_option("-c", "--celsius", dest="celsius", help="return temp" + \
149 "eratures in Celsius (Fahrenheit without this switch",
150 action="store_true", default=False)
151 (options, args) = parser.parse_args()
152 if len(args) == 1:
153 weather = get_weather(args[0], options.celsius)
154 weather_tuple = (weather["location"],
155 weather["current"]["temperature"],
156 DEGREE_SYMBOL,
157 "\n".join(weather["current"]["weather"]),
158 weather["current"]["remark"],
159 weather["forecast"][0]["day"],
160 weather["forecast"][0]["high"],
161 weather["forecast"][0]["low"],
162 weather["forecast"][0]["weather"],
163 weather["forecast"][1]["day"],
164 weather["forecast"][1]["high"],
165 weather["forecast"][1]["low"],
166 weather["forecast"][1]["weather"])
167 print """\
168 (%s)
169 %d%s?! %s
172 Forecast
173 Today (%s)
174 High: %d
175 Low: %d
176 Weather: %s
177 Tomorrow (%s)
178 High: %d
179 Low: %d
180 Weather: %s""" % weather_tuple
181 else:
182 parser.print_help()
184 if __name__ == "__main__":
185 main()