Imported from archive.
* Release 1.5. * (all): Updated copyright notices for 2010. * FAQ, INSTALL, LICENSE, README: Reformatted as ReStructuredText. * FAQ: Updated to mention alternative sources for NOAA's stations list, in case the recommended one is unavailable (thanks Celejar!). * NEWS: Renamed to ChangeLog and refactored into GNU format. * weather: Added some comment padding between the shebang line and the copyright, so that distributions wishing to carry patches which modify the interpreter path don't have to refresh them every year when the copyright line changes in their context. * weather, weather.1, weatherrc.5, weather.py: Added experimental alert, atypes, aurl and zones options to support retrieval, filtering and formatting of unexpired NWS severe weather advisories. * weather.1, weatherrc.5: Minor cosmetic fixes to option descriptions. * weather.1, weatherrc.5, weather.py: Added imperial and metric options to filter/convert display units (thanks to Andrew Carter for this suggestion!). * weather.py: Fixed a METAR parsing error which would trigger an IndexError exception if the NWS didn't have a station description on file (thanks to Celejar for reporting the bug!). Fixed METAR title line parsing to look for human-readable city and state in the first line--previous code stopped showing the city name after NWS made slight format mods. Upped the version to 1.5. * weatherrc: Additional PIE (Saint Petersburg, FL), PNC (Ponca City, OK), and PNS (Pensacola, FL) aliases.
This commit is contained in:
279
weather.py
279
weather.py
@@ -1,12 +1,10 @@
|
||||
# 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.
|
||||
# Copyright (c) 2006-2010 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.4"
|
||||
version = "1.5"
|
||||
|
||||
class Selections:
|
||||
"""An object to contain selection data."""
|
||||
@@ -46,24 +44,121 @@ def quote(words):
|
||||
if words.find(" ") != -1: words = "\"" + words + "\""
|
||||
return words
|
||||
|
||||
def titlecap(words):
|
||||
"""Perform English-language title capitalization."""
|
||||
words = words.lower().strip()
|
||||
for separator in [" ", "-", "'"]:
|
||||
newwords = []
|
||||
wordlist = words.split(separator)
|
||||
for word in wordlist:
|
||||
if word:
|
||||
newwords.append(word[0].upper() + word[1:])
|
||||
words = separator.join(newwords)
|
||||
end = len(words)
|
||||
for prefix in ["Mac", "Mc"]:
|
||||
position = 0
|
||||
offset = len(prefix)
|
||||
while position < end:
|
||||
position = words.find(prefix, position)
|
||||
if position == -1:
|
||||
position = end
|
||||
position += offset
|
||||
import string
|
||||
if position < end and words[position] in string.letters:
|
||||
words = words[:position] \
|
||||
+ words[position].upper() \
|
||||
+ words[position+1:]
|
||||
return words
|
||||
|
||||
def filter_units(line, units="imperial"):
|
||||
"""Filter or convert units in a line of text between US/UK and metric."""
|
||||
import re
|
||||
# filter lines with both pressures in the form of "X inches (Y hPa)" or
|
||||
# "X in. Hg (Y hPa)"
|
||||
dual_p = re.match(
|
||||
"(.* )(\d*(\.\d+)? (inches|in\. Hg)) \((\d*(\.\d+)? hPa)\)(.*)",
|
||||
line
|
||||
)
|
||||
if dual_p:
|
||||
preamble, in_hg, i_fr, i_un, hpa, h_fr, trailer = dual_p.groups()
|
||||
if units == "imperial": line = preamble + in_hg + trailer
|
||||
elif units == "metric": line = preamble + hpa + trailer
|
||||
# filter lines with both temperatures in the form of "X F (Y C)"
|
||||
dual_t = re.match(
|
||||
"(.* )(\d*(\.\d+)? F) \((\d*(\.\d+)? C)\)(.*)",
|
||||
line
|
||||
)
|
||||
if dual_t:
|
||||
preamble, fahrenheit, f_fr, celsius, c_fr, trailer = dual_t.groups()
|
||||
if units == "imperial": line = preamble + fahrenheit + trailer
|
||||
elif units == "metric": line = preamble + celsius + trailer
|
||||
# if metric is desired, convert distances in the form of "X mile(s)" to
|
||||
# "Y kilometer(s)"
|
||||
if units == "metric":
|
||||
imperial_d = re.match(
|
||||
"(.* )(\d+)( mile\(s\))(.*)",
|
||||
line
|
||||
)
|
||||
if imperial_d:
|
||||
preamble, mi, m_u, trailer = imperial_d.groups()
|
||||
line = preamble + str(int(round(int(mi)*1.609344))) \
|
||||
+ " kilometer(s)" + trailer
|
||||
# filter speeds in the form of "X MPH (Y KT)" to just "X MPH"; if metric is
|
||||
# desired, convert to "Z KPH"
|
||||
imperial_s = re.match(
|
||||
"(.* )(\d+)( MPH)( \(\d+ KT\))(.*)",
|
||||
line
|
||||
)
|
||||
if imperial_s:
|
||||
preamble, mph, m_u, kt, trailer = imperial_s.groups()
|
||||
if units == "imperial": line = preamble + mph + m_u + trailer
|
||||
elif units == "metric":
|
||||
line = preamble + str(int(round(int(mph)*1.609344))) + " KPH" + \
|
||||
trailer
|
||||
# if imperial is desired, qualify given forcast temperatures like "X F"; if
|
||||
# metric is desired, convert to "Y C"
|
||||
imperial_t = re.match(
|
||||
"(.* )(High |high |Low |low )(\d+)(\.|,)(.*)",
|
||||
line
|
||||
)
|
||||
if imperial_t:
|
||||
preamble, parameter, fahrenheit, sep, trailer = imperial_t.groups()
|
||||
if units == "imperial":
|
||||
line = preamble + parameter + fahrenheit + " F" + sep + trailer
|
||||
elif units == "metric":
|
||||
line = preamble + parameter \
|
||||
+ str(int(round((int(fahrenheit)-32)*5/9))) + " C" + sep + trailer
|
||||
# hand off the resulting line
|
||||
return line
|
||||
|
||||
def sorted(data):
|
||||
"""Return a sorted copy of a list."""
|
||||
new_copy = data[:]
|
||||
new_copy.sort()
|
||||
return new_copy
|
||||
|
||||
def get_url(url):
|
||||
def get_url(url, ignore_fail=False):
|
||||
"""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)
|
||||
if ignore_fail: return ""
|
||||
else:
|
||||
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):
|
||||
def get_metar(
|
||||
id,
|
||||
verbose=False,
|
||||
quiet=False,
|
||||
headers=None,
|
||||
murl=None,
|
||||
imperial=False,
|
||||
metric=False
|
||||
):
|
||||
"""Return a summarized METAR for the specified station."""
|
||||
if not id:
|
||||
import sys
|
||||
@@ -92,20 +187,92 @@ def get_metar(id, verbose=False, quiet=False, headers=None, murl=None):
|
||||
headerlist = headers.lower().replace("_"," ").split(",")
|
||||
output = []
|
||||
if not quiet:
|
||||
output.append("Current conditions at " \
|
||||
+ lines[0].split(", ")[1] + " (" \
|
||||
+ id.upper() +")")
|
||||
title = "Current conditions at %s"
|
||||
place = lines[0].split(", ")
|
||||
if len(place) > 1:
|
||||
place = "%s, %s (%s)" % (titlecap(place[0]), place[1], id.upper())
|
||||
else: place = id.upper()
|
||||
output.append(title%place)
|
||||
output.append("Last updated " + lines[1])
|
||||
for header in headerlist:
|
||||
for line in lines:
|
||||
if line.lower().startswith(header + ":"):
|
||||
if line.endswith(":0"):
|
||||
if line.endswith(":0") or line.endswith(":1"):
|
||||
line = line[:-2]
|
||||
if imperial: line = filter_units(line, units="imperial")
|
||||
elif metric: line = filter_units(line, units="metric")
|
||||
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):
|
||||
def get_alert(
|
||||
zone,
|
||||
verbose=False,
|
||||
quiet=False,
|
||||
atype=None,
|
||||
aurl=None,
|
||||
imperial=False,
|
||||
metric=False
|
||||
):
|
||||
"""Return alert notice for the specified zone and type."""
|
||||
if not zone:
|
||||
import sys
|
||||
sys.stderr.write("weather: error: zone required for alerts\n")
|
||||
sys.exit(1)
|
||||
if not atype: atype = "severe_weather_stmt"
|
||||
if not aurl:
|
||||
aurl = \
|
||||
"http://weather.noaa.gov/pub/data/watches_warnings/%atype%/%zone%.txt"
|
||||
aurl = aurl.replace("%ATYPE%", atype.upper())
|
||||
aurl = aurl.replace("%Atype%", atype.capitalize())
|
||||
aurl = aurl.replace("%atypE%", atype)
|
||||
aurl = aurl.replace("%atype%", atype.lower())
|
||||
aurl = aurl.replace("%ZONE%", zone.upper())
|
||||
aurl = aurl.replace("%Zone%", zone.capitalize())
|
||||
aurl = aurl.replace("%zonE%", zone)
|
||||
aurl = aurl.replace("%zone%", zone.lower())
|
||||
aurl = aurl.replace(" ", "_")
|
||||
alert = get_url(aurl, ignore_fail=True).strip()
|
||||
if alert:
|
||||
if verbose: return alert
|
||||
else:
|
||||
lines = alert.split("\n")
|
||||
muted = True
|
||||
import calendar, re, time
|
||||
valid_time = time.strftime("%Y%m%d%H%M")
|
||||
#if not quiet: output = [ lines[3], lines[5] ]
|
||||
#if not quiet: output = [ lines[8], lines[10] ]
|
||||
#else: output = []
|
||||
output = []
|
||||
for line in lines:
|
||||
if line.startswith("Expires:") and "Expires:"+valid_time > line:
|
||||
return ""
|
||||
if muted and line.find("...") != -1:
|
||||
muted = False
|
||||
if line == "$$" \
|
||||
or line.startswith("LAT...LON") \
|
||||
or line.startswith("TIME...MOT...LOC"):
|
||||
muted = True
|
||||
if line and not (
|
||||
muted \
|
||||
or line == "&&"
|
||||
or re.match("^/.*/$", line) \
|
||||
or re.match("^"+zone.split("/")[1][:3].upper()+".*", line)
|
||||
):
|
||||
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,
|
||||
imperial=False,
|
||||
metric=False
|
||||
):
|
||||
"""Return the forecast for a specified city/st combination."""
|
||||
if not city or not st:
|
||||
import sys
|
||||
@@ -131,6 +298,8 @@ def get_forecast(city, st, verbose=False, quiet=False, flines="0", furl=None):
|
||||
flines = int(flines)
|
||||
if not flines: flines = len(lines) - 5
|
||||
for line in lines[5:flines+5]:
|
||||
if imperial: line = filter_units(line, units="imperial")
|
||||
elif metric: line = filter_units(line, units="metric")
|
||||
if line.startswith("."):
|
||||
if quiet: output.append(line.replace(".", "", 1))
|
||||
else: output.append(line.replace(".", " ", 1))
|
||||
@@ -149,6 +318,50 @@ def get_options(config):
|
||||
import optparse
|
||||
option_parser = optparse.OptionParser(usage=usage, version=verstring)
|
||||
|
||||
# the -a/--alert option
|
||||
if config.has_option("default", "alert"):
|
||||
default_alert = bool(config.get("default", "alert"))
|
||||
else: default_alert = False
|
||||
option_parser.add_option("-a", "--alert",
|
||||
dest="alert",
|
||||
action="store_true",
|
||||
default=default_alert,
|
||||
help="include local alert notices")
|
||||
|
||||
# the --atypes option
|
||||
if config.has_option("default", "atypes"):
|
||||
default_atypes = config.get("default", "atypes")
|
||||
else:
|
||||
default_atypes = \
|
||||
"flash_flood/statement," \
|
||||
+ "flash_flood/warning," \
|
||||
+ "flash_flood/watch," \
|
||||
+ "flood/coastal," \
|
||||
+ "flood/statement," \
|
||||
+ "flood/warning," \
|
||||
+ "non_precip," \
|
||||
+ "severe_weather_stmt," \
|
||||
+ "special_weather_stmt," \
|
||||
+ "thunderstorm," \
|
||||
+ "tornado," \
|
||||
+ "urgent_weather_message"
|
||||
option_parser.add_option("--atypes",
|
||||
dest="atypes",
|
||||
default=default_atypes,
|
||||
help="alert notification types to display")
|
||||
|
||||
# the --aurl option
|
||||
if config.has_option("default", "aurl"):
|
||||
default_aurl = config.get("default", "aurl")
|
||||
else:
|
||||
default_aurl = \
|
||||
"http://weather.noaa.gov/pub/data/watches_warnings/%atype%/%zone%.txt"
|
||||
option_parser.add_option("--aurl",
|
||||
dest="aurl",
|
||||
default=default_aurl,
|
||||
help="alert URL (including %atype% and %zone%)")
|
||||
|
||||
# separate options object from list of arguments and return both
|
||||
# the -c/--city option
|
||||
if config.has_option("default", "city"):
|
||||
default_city = config.get("default", "city")
|
||||
@@ -213,6 +426,16 @@ def get_options(config):
|
||||
default=default_id,
|
||||
help="the METAR station ID (ex: KRDU)")
|
||||
|
||||
# the --imperial option
|
||||
if config.has_option("default", "imperial"):
|
||||
default_imperial = bool(config.get("default", "imperial"))
|
||||
else: default_imperial = False
|
||||
option_parser.add_option("--imperial",
|
||||
dest="imperial",
|
||||
action="store_true",
|
||||
default=default_imperial,
|
||||
help="filter/convert for US/UK units")
|
||||
|
||||
# the -l/--list option
|
||||
option_parser.add_option("-l", "--list",
|
||||
dest="list",
|
||||
@@ -220,6 +443,16 @@ def get_options(config):
|
||||
default=False,
|
||||
help="print a list of configured aliases")
|
||||
|
||||
# the -m/--metric option
|
||||
if config.has_option("default", "metric"):
|
||||
default_metric = bool(config.get("default", "metric"))
|
||||
else: default_metric = False
|
||||
option_parser.add_option("-m", "--metric",
|
||||
dest="metric",
|
||||
action="store_true",
|
||||
default=default_metric,
|
||||
help="filter/convert for metric units")
|
||||
|
||||
# the --murl option
|
||||
if config.has_option("default", "murl"):
|
||||
default_murl = config.get("default", "murl")
|
||||
@@ -277,7 +510,15 @@ def get_options(config):
|
||||
default=default_verbose,
|
||||
help="show full decoded feeds (cancels -q)")
|
||||
|
||||
# separate options object from list of arguments and return both
|
||||
# the -z/--zones option
|
||||
if config.has_option("default", "zones"):
|
||||
default_zones = config.get("default", "zones")
|
||||
else: default_zones = ""
|
||||
option_parser.add_option("-z", "--zones",
|
||||
dest="zones",
|
||||
default=default_zones,
|
||||
help="alert zones (ex: nc/ncc183,nc/ncz041)")
|
||||
|
||||
options, arguments = option_parser.parse_args()
|
||||
return options, arguments
|
||||
|
||||
|
||||
Reference in New Issue
Block a user