Imported from archive.

* Release 1.4.

* (all): Updated the copyright years for 2008 on some of the files
in the current release and added a copyright statement to any files
previously lacking one.

* LICENSE: Replaced the previous BSD-like license with the one used
by the OpenBSD project (modeled after the Internet Software
Consortium's, a two-clause BSD license removing language made
unnecessary by the Berne convention); this new license is
functionally identical to the old one, just more terse and openly
recognized.

* weather: Clarified function parameters in calls from the wrapper
script to ease future ABI changes in the underlying module.

* weather, weather.py: Some extra comments were added to the source,
indentation style was updated from tab characters to three-space,
and lines longer than 79 columns were refactored or otherwise split.

* weather.1, weather.5, weather.py: Added an flines option to allow
the maximum number of forecast output lines to be shortened. Added
furl and murl options to allow overriding of the default current
conditions and forecast data retrieval URLs. Added a headers option
to allow overriding the default list of header names for current
conditions data filtering. Added a quiet option to suppress the
preamble lines and indentation for both current conditions and
forecast output.

* weather.py: Replaced the hardcoded fallback default METAR station
ID and forecast city/state abbreviation with error messages to
minimize confusion when necessary values are omitted. Adjusted a
couple of hard-coded error message strings to be consistent with the
output format of the option_parser module. Switched from urllib to
urllib2 for retrieving data, providing a simpler means to detect and
report retrieval errors. Upped the version to 1.4.
This commit is contained in:
Jeremy Stanley
2008-07-13 07:49:02 +00:00
parent eb249a0ae2
commit 8349654b7c
9 changed files with 434 additions and 263 deletions

View File

@@ -1,206 +1,323 @@
# 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()
"""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):
"""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_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):
"""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)
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