Imported from archive.
[weather.git] / weather.py
index 3ea1f54..20c9d5c 100644 (file)
-# weather.py version 1.3, http://fungi.yuggoth.org/weather/
-# Copyright (c) 2006 Jeremy Stanley <fungi@yuggoth.org>, all rights reserved.
-# Licensed per terms in the LICENSE file distributed with this software.
+# weather.py version 1.4, http://fungi.yuggoth.org/weather/
+# Copyright (c) 2006-2008 Jeremy Stanley <fungi@yuggoth.org>.
+# Permission to use, copy, modify, and distribute this software is
+# granted under terms provided in the LICENSE file distributed with
+# this software.
 
 """Contains various object definitions needed by the weather utility."""
 
-version = "1.3"
+version = "1.4"
 
 class Selections:
-       """An object to contain selection data."""
-       def __init__(self):
-               """Store the config, options and arguments."""
-               self.config = get_config()
-               self.options, self.arguments = get_options(self.config)
-               if self.arguments:
-                       self.arguments = [(x.lower()) for x in self.arguments]
-               else: self.arguments = [ None ]
-       def get(self, option, argument=None):
-               """Retrieve data from the config or options."""
-               if not argument: return self.options.__dict__[option]
-               elif not self.config.has_section(argument):
-                       import sys
-                       sys.stderr.write("ERROR: no alias defined for " \
-                               + argument + "\n")
-                       sys.exit(1)
-               elif self.config.has_option(argument, option):
-                       return self.config.get(argument, option)
-               else: return self.options.__dict__[option]
-       def get_bool(self, option, argument=None):
-               """Get data and coerce to a boolean if necessary."""
-               return bool(self.get(option, argument))
+   """An object to contain selection data."""
+   def __init__(self):
+      """Store the config, options and arguments."""
+      self.config = get_config()
+      self.options, self.arguments = get_options(self.config)
+      if self.arguments:
+         self.arguments = [(x.lower()) for x in self.arguments]
+      else: self.arguments = [ None ]
+   def get(self, option, argument=None):
+      """Retrieve data from the config or options."""
+      if not argument: return self.options.__dict__[option]
+      elif not self.config.has_section(argument):
+         import sys
+         sys.stderr.write("weather: error: no alias defined for " \
+            + argument + "\n")
+         sys.exit(1)
+      elif self.config.has_option(argument, option):
+         return self.config.get(argument, option)
+      else: return self.options.__dict__[option]
+   def get_bool(self, option, argument=None):
+      """Get data and coerce to a boolean if necessary."""
+      return bool(self.get(option, argument))
 
 def bool(data):
-       """Coerce data to a boolean value."""
-       if type(data) is str:
-               if eval(data): return True
-               else: return False
-       else:
-               if data: return True
-               else: return False
+   """Coerce data to a boolean value."""
+   if type(data) is str:
+      if eval(data): return True
+      else: return False
+   else:
+      if data: return True
+      else: return False
 
 def quote(words):
-       """Wrap a string in quotes if it contains spaces."""
-       if words.find(" ") != -1: words = "\"" + words + "\""
-       return words
+   """Wrap a string in quotes if it contains spaces."""
+   if words.find(" ") != -1: words = "\"" + words + "\""
+   return words
 
 def sorted(data):
-       """Return a sorted copy of a list."""
-       new_copy = data[:]
-       new_copy.sort()
-       return new_copy
+   """Return a sorted copy of a list."""
+   new_copy = data[:]
+   new_copy.sort()
+   return new_copy
 
 def get_url(url):
-       """Return a string containing the results of a URL GET."""
-       import urllib
-       return urllib.urlopen(url).read()
-
-def get_metar(id, verbose=False):
-       """Return a summarized METAR for the specified station."""
-       metar = get_url(
-               "http://weather.noaa.gov/pub/data/observations/metar/decoded/" \
-                       + id.upper() + ".TXT")
-       if verbose: return metar
-       else:
-               lines = metar.split("\n")
-               headings = [
-                       "Relative Humidity",
-                       "Precipitation last hour",
-                       "Sky conditions",
-                       "Temperature",
-                       "Weather",
-                       "Wind" 
-                       ]
-               output = []
-               output.append("Current conditions at " \
-                       + lines[0].split(", ")[1] + " (" \
-                       + id.upper() +")")
-               output.append("Last updated " + lines[1])
-               for line in lines:
-                       for heading in headings:
-                               if line.startswith(heading + ":"):
-                                       if line.endswith(":0"):
-                                               line = line[:-2]
-                                       output.append("   " + line)
-               return "\n".join(output)
-
-def get_forecast(city, st, verbose=False):
-       """Return the forecast for a specified city/st combination."""
-       forecast = get_url("http://weather.noaa.gov/pub/data/forecasts/city/" \
-               + st.lower() + "/" + city.lower().replace(" ", "_") \
-               + ".txt")
-       if verbose: return forecast
-       else:
-               lines = forecast.split("\n")
-               output = []
-               output.append(lines[2])
-               output.append(lines[3])
-               for line in lines:
-                       if line.startswith("."):
-                               output.append(line.replace(".", "   ", 1))
-               return "\n".join(output)
+   """Return a string containing the results of a URL GET."""
+   import urllib2
+   try: return urllib2.urlopen(url).read()
+   except urllib2.URLError:
+      import sys, traceback
+      sys.stderr.write("weather: error: failed to retrieve\n   " \
+         + url + "\n   " + \
+         traceback.format_exception_only(sys.exc_type, sys.exc_value)[0])
+      sys.exit(1)
+
+def get_metar(id, verbose=False, quiet=False, headers=None, murl=None):
+   """Return a summarized METAR for the specified station."""
+   if not id:
+      import sys
+      sys.stderr.write("weather: error: id required for conditions\n")
+      sys.exit(1)
+   if not murl:
+      murl = \
+         "http://weather.noaa.gov/pub/data/observations/metar/decoded/%ID%.TXT"
+   murl = murl.replace("%ID%", id.upper())
+   murl = murl.replace("%Id%", id.capitalize())
+   murl = murl.replace("%iD%", id)
+   murl = murl.replace("%id%", id.lower())
+   murl = murl.replace(" ", "_")
+   metar = get_url(murl)
+   if verbose: return metar
+   else:
+      lines = metar.split("\n")
+      if not headers:
+         headers = \
+            "relative_humidity," \
+            + "precipitation_last_hour," \
+            + "sky conditions," \
+            + "temperature," \
+            + "weather," \
+            + "wind"
+      headerlist = headers.lower().replace("_"," ").split(",")
+      output = []
+      if not quiet:
+         output.append("Current conditions at " \
+            + lines[0].split(", ")[1] + " (" \
+            + id.upper() +")")
+         output.append("Last updated " + lines[1])
+      for header in headerlist:
+         for line in lines:
+            if line.lower().startswith(header + ":"):
+               if line.endswith(":0"):
+                  line = line[:-2]
+               if quiet: output.append(line)
+               else: output.append("   " + line)
+      return "\n".join(output)
+
+def get_forecast(city, st, verbose=False, quiet=False, flines="0", furl=None):
+   """Return the forecast for a specified city/st combination."""
+   if not city or not st:
+      import sys
+      sys.stderr.write("weather: error: city and st required for forecast\n")
+      sys.exit(1)
+   if not furl:
+      furl = "http://weather.noaa.gov/pub/data/forecasts/city/%st%/%city%.txt"
+   furl = furl.replace("%CITY%", city.upper())
+   furl = furl.replace("%City%", city.capitalize())
+   furl = furl.replace("%citY%", city)
+   furl = furl.replace("%city%", city.lower())
+   furl = furl.replace("%ST%", st.upper())
+   furl = furl.replace("%St%", st.capitalize())
+   furl = furl.replace("%sT%", st)
+   furl = furl.replace("%st%", st.lower())
+   furl = furl.replace(" ", "_")
+   forecast = get_url(furl)
+   if verbose: return forecast
+   else:
+      lines = forecast.split("\n")
+      output = []
+      if not quiet: output += lines[2:4]
+      flines = int(flines)
+      if not flines: flines = len(lines) - 5
+      for line in lines[5:flines+5]:
+         if line.startswith("."):
+            if quiet: output.append(line.replace(".", "", 1))
+            else: output.append(line.replace(".", "   ", 1))
+      return "\n".join(output)
 
 def get_options(config):
-       """Parse the options passed on the command line."""
-       import optparse
-       usage = "usage: %prog [ options ] [ alias [ alias [...] ] ]"
-       verstring = "%prog " + version
-       option_parser = optparse.OptionParser(usage=usage, version=verstring)
-       if config.has_option("default", "city"):
-               default_city = config.get("default", "city")
-       else: default_city = "Raleigh Durham"
-       option_parser.add_option("-c", "--city",
-               dest="city",
-               default=default_city,
-               help="the city name (ex: \"Raleigh Durham\")")
-       if config.has_option("default", "forecast"):
-               default_forecast = bool(config.get("default", "forecast"))
-       else: default_forecast = False
-       option_parser.add_option("-f", "--forecast",
-               dest="forecast",
-               action="store_true",
-               default=default_forecast,
-               help="include a local forecast")
-       if config.has_option("default", "id"):
-               default_id = config.get("default", "id")
-       else: default_id = "KRDU"
-       option_parser.add_option("-i", "--id",
-               dest="id",
-               default=default_id,
-               help="the METAR station ID (ex: KRDU)")
-       option_parser.add_option("-l", "--list",
-               dest="list",
-               action="store_true",
-               default=False,
-               help="print a list of configured aliases")
-       if config.has_option("default", "conditions"):
-               default_conditions = bool(config.get("default", "conditions"))
-       else: default_conditions = True
-       option_parser.add_option("-n", "--no-conditions",
-               dest="conditions",
-               action="store_false",
-               default=default_conditions,
-               help="disable output of current conditions (forces -f)")
-       option_parser.add_option("-o", "--omit-forecast",
-               dest="forecast",
-               action="store_false",
-               default=default_forecast,
-               help="omit the local forecast (cancels -f)")
-       if config.has_option("default", "st"):
-               default_st = config.get("default", "st")
-       else: default_st = "NC"
-       option_parser.add_option("-s", "--st",
-               dest="st",
-               default=default_st,
-               help="the state abbreviation (ex: NC)")
-       if config.has_option("default", "verbose"):
-               default_verbose = bool(config.get("default", "verbose"))
-       else: default_verbose = False
-       option_parser.add_option("-v", "--verbose",
-               dest="verbose",
-               action="store_true",
-               default=default_verbose,
-               help="show full decoded feeds")
-       options, arguments = option_parser.parse_args()
-       return options, arguments
+   """Parse the options passed on the command line."""
+
+   # for optparse's builtin -h/--help option
+   usage = "usage: %prog [ options ] [ alias [ alias [...] ] ]"
+
+   # for optparse's builtin --version option
+   verstring = "%prog " + version
+
+   # create the parser
+   import optparse
+   option_parser = optparse.OptionParser(usage=usage, version=verstring)
+
+   # the -c/--city option
+   if config.has_option("default", "city"):
+      default_city = config.get("default", "city")
+   else: default_city = ""
+   option_parser.add_option("-c", "--city",
+      dest="city",
+      default=default_city,
+      help="the city name (ex: \"Raleigh Durham\")")
+
+   # the --flines option
+   if config.has_option("default", "flines"):
+      default_flines = config.get("default", "flines")
+   else: default_flines = "0"
+   option_parser.add_option("--flines",
+      dest="flines",
+      default=default_flines,
+      help="maximum number of forecast lines to show")
+
+   # the -f/--forecast option
+   if config.has_option("default", "forecast"):
+      default_forecast = bool(config.get("default", "forecast"))
+   else: default_forecast = False
+   option_parser.add_option("-f", "--forecast",
+      dest="forecast",
+      action="store_true",
+      default=default_forecast,
+      help="include a local forecast")
+
+   # the --furl option
+   if config.has_option("default", "furl"):
+      default_furl = config.get("default", "furl")
+   else:
+      default_furl = \
+         "http://weather.noaa.gov/pub/data/forecasts/city/%st%/%city%.txt"
+   option_parser.add_option("--furl",
+      dest="furl",
+      default=default_furl,
+      help="forecast URL (including %city% and %st%)")
+
+   # the --headers option
+   if config.has_option("default", "headers"):
+      default_headers = config.get("default", "headers")
+   else:
+      default_headers = \
+         "temperature," \
+         + "relative_humidity," \
+         + "wind," \
+         + "weather," \
+         + "sky_conditions," \
+         + "precipitation_last_hour"
+   option_parser.add_option("--headers",
+      dest="headers",
+      default=default_headers,
+      help="the conditions headers to display")
+
+   # the -i/--id option
+   if config.has_option("default", "id"):
+      default_id = config.get("default", "id")
+   else: default_id = ""
+   option_parser.add_option("-i", "--id",
+      dest="id",
+      default=default_id,
+      help="the METAR station ID (ex: KRDU)")
+
+   # the -l/--list option
+   option_parser.add_option("-l", "--list",
+      dest="list",
+      action="store_true",
+      default=False,
+      help="print a list of configured aliases")
+
+   # the --murl option
+   if config.has_option("default", "murl"):
+      default_murl = config.get("default", "murl")
+   else:
+      default_murl = \
+         "http://weather.noaa.gov/pub/data/observations/metar/decoded/%ID%.TXT"
+   option_parser.add_option("--murl",
+      dest="murl",
+      default=default_murl,
+      help="METAR URL (including %id%)")
+
+   # the -n/--no-conditions option
+   if config.has_option("default", "conditions"):
+      default_conditions = bool(config.get("default", "conditions"))
+   else: default_conditions = True
+   option_parser.add_option("-n", "--no-conditions",
+      dest="conditions",
+      action="store_false",
+      default=default_conditions,
+      help="disable output of current conditions (forces -f)")
+
+   # the -o/--omit-forecast option
+   option_parser.add_option("-o", "--omit-forecast",
+      dest="forecast",
+      action="store_false",
+      default=default_forecast,
+      help="omit the local forecast (cancels -f)")
+
+   # the -q/--quiet option
+   if config.has_option("default", "quiet"):
+      default_quiet = bool(config.get("default", "quiet"))
+   else: default_quiet = False
+   option_parser.add_option("-q", "--quiet",
+      dest="quiet",
+      action="store_true",
+      default=default_quiet,
+      help="skip preambles and don't indent")
+
+   # the -s/--st option
+   if config.has_option("default", "st"):
+      default_st = config.get("default", "st")
+   else: default_st = ""
+   option_parser.add_option("-s", "--st",
+      dest="st",
+      default=default_st,
+      help="the state abbreviation (ex: NC)")
+
+   # the -v/--verbose option
+   if config.has_option("default", "verbose"):
+      default_verbose = bool(config.get("default", "verbose"))
+   else: default_verbose = False
+   option_parser.add_option("-v", "--verbose",
+      dest="verbose",
+      action="store_true",
+      default=default_verbose,
+      help="show full decoded feeds (cancels -q)")
+
+   # separate options object from list of arguments and return both
+   options, arguments = option_parser.parse_args()
+   return options, arguments
 
 def get_config():
-       """Parse the aliases and configuration."""
-       import ConfigParser
-       config = ConfigParser.ConfigParser()
-       import os.path
-       rcfiles = [
-               "/etc/weatherrc",
-               os.path.expanduser("~/.weatherrc"),
-               "weatherrc"
-               ]
-       import os
-       for rcfile in rcfiles:
-               if os.access(rcfile, os.R_OK): config.read(rcfile)
-       for section in config.sections():
-               if section != section.lower():
-                       if config.has_section(section.lower()):
-                               config.remove_section(section.lower())
-                       config.add_section(section.lower())
-                       for option,value in config.items(section):
-                               config.set(section.lower(), option, value)
-       return config
+   """Parse the aliases and configuration."""
+   import ConfigParser
+   config = ConfigParser.ConfigParser()
+   import os.path
+   rcfiles = [
+      "/etc/weatherrc",
+      os.path.expanduser("~/.weatherrc"),
+      "weatherrc"
+      ]
+   import os
+   for rcfile in rcfiles:
+      if os.access(rcfile, os.R_OK): config.read(rcfile)
+   for section in config.sections():
+      if section != section.lower():
+         if config.has_section(section.lower()):
+            config.remove_section(section.lower())
+         config.add_section(section.lower())
+         for option,value in config.items(section):
+            config.set(section.lower(), option, value)
+   return config
 
 def list_aliases(config):
-       """Return a formatted list of aliases defined in the config."""
-       sections = []
-       for section in sorted(config.sections()):
-               if section.lower() not in sections and section != "default":
-                       sections.append(section.lower())
-       output = "configured aliases..."
-       for section in sorted(sections):
-               output += "\n   " \
-                       + section \
-                       + ": --id=" \
-                       + quote(config.get(section, "id")) \
-                       + " --city=" \
-                       + quote(config.get(section, "city")) \
-                       + " --st=" \
-                       + quote(config.get(section, "st"))
-       return output
+   """Return a formatted list of aliases defined in the config."""
+   sections = []
+   for section in sorted(config.sections()):
+      if section.lower() not in sections and section != "default":
+         sections.append(section.lower())
+   output = "configured aliases..."
+   for section in sorted(sections):
+      output += "\n   " \
+         + section \
+         + ": --id=" \
+         + quote(config.get(section, "id")) \
+         + " --city=" \
+         + quote(config.get(section, "city")) \
+         + " --st=" \
+         + quote(config.get(section, "st"))
+   return output