20c9d5cbd2081662ce2fb9e1c1b0f0c74e2893a8
[weather.git] / weather.py
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
5 # this software.
6
7 """Contains various object definitions needed by the weather utility."""
8
9 version = "1.4"
10
11 class Selections:
12    """An object to contain selection data."""
13    def __init__(self):
14       """Store the config, options and arguments."""
15       self.config = get_config()
16       self.options, self.arguments = get_options(self.config)
17       if self.arguments:
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):
24          import sys
25          sys.stderr.write("weather: error: no alias defined for " \
26             + argument + "\n")
27          sys.exit(1)
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))
34
35 def bool(data):
36    """Coerce data to a boolean value."""
37    if type(data) is str:
38       if eval(data): return True
39       else: return False
40    else:
41       if data: return True
42       else: return False
43
44 def quote(words):
45    """Wrap a string in quotes if it contains spaces."""
46    if words.find(" ") != -1: words = "\"" + words + "\""
47    return words
48
49 def sorted(data):
50    """Return a sorted copy of a list."""
51    new_copy = data[:]
52    new_copy.sort()
53    return new_copy
54
55 def get_url(url):
56    """Return a string containing the results of a URL GET."""
57    import urllib2
58    try: return urllib2.urlopen(url).read()
59    except urllib2.URLError:
60       import sys, traceback
61       sys.stderr.write("weather: error: failed to retrieve\n   " \
62          + url + "\n   " + \
63          traceback.format_exception_only(sys.exc_type, sys.exc_value)[0])
64       sys.exit(1)
65
66 def get_metar(id, verbose=False, quiet=False, headers=None, murl=None):
67    """Return a summarized METAR for the specified station."""
68    if not id:
69       import sys
70       sys.stderr.write("weather: error: id required for conditions\n")
71       sys.exit(1)
72    if not murl:
73       murl = \
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(" ", "_")
80    metar = get_url(murl)
81    if verbose: return metar
82    else:
83       lines = metar.split("\n")
84       if not headers:
85          headers = \
86             "relative_humidity," \
87             + "precipitation_last_hour," \
88             + "sky conditions," \
89             + "temperature," \
90             + "weather," \
91             + "wind"
92       headerlist = headers.lower().replace("_"," ").split(",")
93       output = []
94       if not quiet:
95          output.append("Current conditions at " \
96             + lines[0].split(", ")[1] + " (" \
97             + id.upper() +")")
98          output.append("Last updated " + lines[1])
99       for header in headerlist:
100          for line in lines:
101             if line.lower().startswith(header + ":"):
102                if line.endswith(":0"):
103                   line = line[:-2]
104                if quiet: output.append(line)
105                else: output.append("   " + line)
106       return "\n".join(output)
107
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:
111       import sys
112       sys.stderr.write("weather: error: city and st required for forecast\n")
113       sys.exit(1)
114    if not furl:
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
127    else:
128       lines = forecast.split("\n")
129       output = []
130       if not quiet: output += lines[2:4]
131       flines = int(flines)
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)
138
139 def get_options(config):
140    """Parse the options passed on the command line."""
141
142    # for optparse's builtin -h/--help option
143    usage = "usage: %prog [ options ] [ alias [ alias [...] ] ]"
144
145    # for optparse's builtin --version option
146    verstring = "%prog " + version
147
148    # create the parser
149    import optparse
150    option_parser = optparse.OptionParser(usage=usage, version=verstring)
151
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",
157       dest="city",
158       default=default_city,
159       help="the city name (ex: \"Raleigh Durham\")")
160
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",
166       dest="flines",
167       default=default_flines,
168       help="maximum number of forecast lines to show")
169
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",
175       dest="forecast",
176       action="store_true",
177       default=default_forecast,
178       help="include a local forecast")
179
180    # the --furl option
181    if config.has_option("default", "furl"):
182       default_furl = config.get("default", "furl")
183    else:
184       default_furl = \
185          "http://weather.noaa.gov/pub/data/forecasts/city/%st%/%city%.txt"
186    option_parser.add_option("--furl",
187       dest="furl",
188       default=default_furl,
189       help="forecast URL (including %city% and %st%)")
190
191    # the --headers option
192    if config.has_option("default", "headers"):
193       default_headers = config.get("default", "headers")
194    else:
195       default_headers = \
196          "temperature," \
197          + "relative_humidity," \
198          + "wind," \
199          + "weather," \
200          + "sky_conditions," \
201          + "precipitation_last_hour"
202    option_parser.add_option("--headers",
203       dest="headers",
204       default=default_headers,
205       help="the conditions headers to display")
206
207    # the -i/--id option
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",
212       dest="id",
213       default=default_id,
214       help="the METAR station ID (ex: KRDU)")
215
216    # the -l/--list option
217    option_parser.add_option("-l", "--list",
218       dest="list",
219       action="store_true",
220       default=False,
221       help="print a list of configured aliases")
222
223    # the --murl option
224    if config.has_option("default", "murl"):
225       default_murl = config.get("default", "murl")
226    else:
227       default_murl = \
228          "http://weather.noaa.gov/pub/data/observations/metar/decoded/%ID%.TXT"
229    option_parser.add_option("--murl",
230       dest="murl",
231       default=default_murl,
232       help="METAR URL (including %id%)")
233
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",
239       dest="conditions",
240       action="store_false",
241       default=default_conditions,
242       help="disable output of current conditions (forces -f)")
243
244    # the -o/--omit-forecast option
245    option_parser.add_option("-o", "--omit-forecast",
246       dest="forecast",
247       action="store_false",
248       default=default_forecast,
249       help="omit the local forecast (cancels -f)")
250
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",
256       dest="quiet",
257       action="store_true",
258       default=default_quiet,
259       help="skip preambles and don't indent")
260
261    # the -s/--st option
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",
266       dest="st",
267       default=default_st,
268       help="the state abbreviation (ex: NC)")
269
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",
275       dest="verbose",
276       action="store_true",
277       default=default_verbose,
278       help="show full decoded feeds (cancels -q)")
279
280    # separate options object from list of arguments and return both
281    options, arguments = option_parser.parse_args()
282    return options, arguments
283
284 def get_config():
285    """Parse the aliases and configuration."""
286    import ConfigParser
287    config = ConfigParser.ConfigParser()
288    import os.path
289    rcfiles = [
290       "/etc/weatherrc",
291       os.path.expanduser("~/.weatherrc"),
292       "weatherrc"
293       ]
294    import os
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)
304    return config
305
306 def list_aliases(config):
307    """Return a formatted list of aliases defined in the config."""
308    sections = []
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):
314       output += "\n   " \
315          + section \
316          + ": --id=" \
317          + quote(config.get(section, "id")) \
318          + " --city=" \
319          + quote(config.get(section, "city")) \
320          + " --st=" \
321          + quote(config.get(section, "st"))
322    return output
323