1 # weather.py version 1.4, http://fungi.yuggoth.org/weather/
2 # Copyright (c) 2006-2008 Jeremy Stanley <fungi@yuggoth.org>.
3 # Permission to use, copy, modify, and distribute this software is
4 # granted under terms provided in the LICENSE file distributed with
7 """Contains various object definitions needed by the weather utility."""
12 """An object to contain selection data."""
14 """Store the config, options and arguments."""
15 self.config = get_config()
16 self.options, self.arguments = get_options(self.config)
18 self.arguments = [(x.lower()) for x in self.arguments]
19 else: self.arguments = [ None ]
20 def get(self, option, argument=None):
21 """Retrieve data from the config or options."""
22 if not argument: return self.options.__dict__[option]
23 elif not self.config.has_section(argument):
25 sys.stderr.write("weather: error: no alias defined for " \
28 elif self.config.has_option(argument, option):
29 return self.config.get(argument, option)
30 else: return self.options.__dict__[option]
31 def get_bool(self, option, argument=None):
32 """Get data and coerce to a boolean if necessary."""
33 return bool(self.get(option, argument))
36 """Coerce data to a boolean value."""
38 if eval(data): return True
45 """Wrap a string in quotes if it contains spaces."""
46 if words.find(" ") != -1: words = "\"" + words + "\""
50 """Return a sorted copy of a list."""
56 """Return a string containing the results of a URL GET."""
58 try: return urllib2.urlopen(url).read()
59 except urllib2.URLError:
61 sys.stderr.write("weather: error: failed to retrieve\n " \
63 traceback.format_exception_only(sys.exc_type, sys.exc_value)[0])
66 def get_metar(id, verbose=False, quiet=False, headers=None, murl=None):
67 """Return a summarized METAR for the specified station."""
70 sys.stderr.write("weather: error: id required for conditions\n")
74 "http://weather.noaa.gov/pub/data/observations/metar/decoded/%ID%.TXT"
75 murl = murl.replace("%ID%", id.upper())
76 murl = murl.replace("%Id%", id.capitalize())
77 murl = murl.replace("%iD%", id)
78 murl = murl.replace("%id%", id.lower())
79 murl = murl.replace(" ", "_")
81 if verbose: return metar
83 lines = metar.split("\n")
86 "relative_humidity," \
87 + "precipitation_last_hour," \
92 headerlist = headers.lower().replace("_"," ").split(",")
95 output.append("Current conditions at " \
96 + lines[0].split(", ")[1] + " (" \
98 output.append("Last updated " + lines[1])
99 for header in headerlist:
101 if line.lower().startswith(header + ":"):
102 if line.endswith(":0"):
104 if quiet: output.append(line)
105 else: output.append(" " + line)
106 return "\n".join(output)
108 def get_forecast(city, st, verbose=False, quiet=False, flines="0", furl=None):
109 """Return the forecast for a specified city/st combination."""
110 if not city or not st:
112 sys.stderr.write("weather: error: city and st required for forecast\n")
115 furl = "http://weather.noaa.gov/pub/data/forecasts/city/%st%/%city%.txt"
116 furl = furl.replace("%CITY%", city.upper())
117 furl = furl.replace("%City%", city.capitalize())
118 furl = furl.replace("%citY%", city)
119 furl = furl.replace("%city%", city.lower())
120 furl = furl.replace("%ST%", st.upper())
121 furl = furl.replace("%St%", st.capitalize())
122 furl = furl.replace("%sT%", st)
123 furl = furl.replace("%st%", st.lower())
124 furl = furl.replace(" ", "_")
125 forecast = get_url(furl)
126 if verbose: return forecast
128 lines = forecast.split("\n")
130 if not quiet: output += lines[2:4]
132 if not flines: flines = len(lines) - 5
133 for line in lines[5:flines+5]:
134 if line.startswith("."):
135 if quiet: output.append(line.replace(".", "", 1))
136 else: output.append(line.replace(".", " ", 1))
137 return "\n".join(output)
139 def get_options(config):
140 """Parse the options passed on the command line."""
142 # for optparse's builtin -h/--help option
143 usage = "usage: %prog [ options ] [ alias [ alias [...] ] ]"
145 # for optparse's builtin --version option
146 verstring = "%prog " + version
150 option_parser = optparse.OptionParser(usage=usage, version=verstring)
152 # the -c/--city option
153 if config.has_option("default", "city"):
154 default_city = config.get("default", "city")
155 else: default_city = ""
156 option_parser.add_option("-c", "--city",
158 default=default_city,
159 help="the city name (ex: \"Raleigh Durham\")")
161 # the --flines option
162 if config.has_option("default", "flines"):
163 default_flines = config.get("default", "flines")
164 else: default_flines = "0"
165 option_parser.add_option("--flines",
167 default=default_flines,
168 help="maximum number of forecast lines to show")
170 # the -f/--forecast option
171 if config.has_option("default", "forecast"):
172 default_forecast = bool(config.get("default", "forecast"))
173 else: default_forecast = False
174 option_parser.add_option("-f", "--forecast",
177 default=default_forecast,
178 help="include a local forecast")
181 if config.has_option("default", "furl"):
182 default_furl = config.get("default", "furl")
185 "http://weather.noaa.gov/pub/data/forecasts/city/%st%/%city%.txt"
186 option_parser.add_option("--furl",
188 default=default_furl,
189 help="forecast URL (including %city% and %st%)")
191 # the --headers option
192 if config.has_option("default", "headers"):
193 default_headers = config.get("default", "headers")
197 + "relative_humidity," \
200 + "sky_conditions," \
201 + "precipitation_last_hour"
202 option_parser.add_option("--headers",
204 default=default_headers,
205 help="the conditions headers to display")
208 if config.has_option("default", "id"):
209 default_id = config.get("default", "id")
210 else: default_id = ""
211 option_parser.add_option("-i", "--id",
214 help="the METAR station ID (ex: KRDU)")
216 # the -l/--list option
217 option_parser.add_option("-l", "--list",
221 help="print a list of configured aliases")
224 if config.has_option("default", "murl"):
225 default_murl = config.get("default", "murl")
228 "http://weather.noaa.gov/pub/data/observations/metar/decoded/%ID%.TXT"
229 option_parser.add_option("--murl",
231 default=default_murl,
232 help="METAR URL (including %id%)")
234 # the -n/--no-conditions option
235 if config.has_option("default", "conditions"):
236 default_conditions = bool(config.get("default", "conditions"))
237 else: default_conditions = True
238 option_parser.add_option("-n", "--no-conditions",
240 action="store_false",
241 default=default_conditions,
242 help="disable output of current conditions (forces -f)")
244 # the -o/--omit-forecast option
245 option_parser.add_option("-o", "--omit-forecast",
247 action="store_false",
248 default=default_forecast,
249 help="omit the local forecast (cancels -f)")
251 # the -q/--quiet option
252 if config.has_option("default", "quiet"):
253 default_quiet = bool(config.get("default", "quiet"))
254 else: default_quiet = False
255 option_parser.add_option("-q", "--quiet",
258 default=default_quiet,
259 help="skip preambles and don't indent")
262 if config.has_option("default", "st"):
263 default_st = config.get("default", "st")
264 else: default_st = ""
265 option_parser.add_option("-s", "--st",
268 help="the state abbreviation (ex: NC)")
270 # the -v/--verbose option
271 if config.has_option("default", "verbose"):
272 default_verbose = bool(config.get("default", "verbose"))
273 else: default_verbose = False
274 option_parser.add_option("-v", "--verbose",
277 default=default_verbose,
278 help="show full decoded feeds (cancels -q)")
280 # separate options object from list of arguments and return both
281 options, arguments = option_parser.parse_args()
282 return options, arguments
285 """Parse the aliases and configuration."""
287 config = ConfigParser.ConfigParser()
291 os.path.expanduser("~/.weatherrc"),
295 for rcfile in rcfiles:
296 if os.access(rcfile, os.R_OK): config.read(rcfile)
297 for section in config.sections():
298 if section != section.lower():
299 if config.has_section(section.lower()):
300 config.remove_section(section.lower())
301 config.add_section(section.lower())
302 for option,value in config.items(section):
303 config.set(section.lower(), option, value)
306 def list_aliases(config):
307 """Return a formatted list of aliases defined in the config."""
309 for section in sorted(config.sections()):
310 if section.lower() not in sections and section != "default":
311 sections.append(section.lower())
312 output = "configured aliases..."
313 for section in sorted(sections):
317 + quote(config.get(section, "id")) \
319 + quote(config.get(section, "city")) \
321 + quote(config.get(section, "st"))