Add Weather in the new plugin format.

This commit is contained in:
James Vega 2005-02-08 16:12:49 +00:00
parent d9e5b719b6
commit 097b8f2bd5
6 changed files with 1155 additions and 0 deletions

View File

@ -0,0 +1,450 @@
"""Beautiful Soup
Elixir and Tonic
"The Screen-Scraper's Friend"
The BeautifulSoup class turns arbitrarily bad HTML into a tree-like
nested tag-soup list of Tag objects and text snippets. A Tag object
corresponds to an HTML tag. It knows about the HTML tag's attributes,
and contains a representation of everything contained between the
original tag and its closing tag (if any). It's easy to extract Tags
that meet certain criteria.
A well-formed HTML document will yield a well-formed data
structure. An ill-formed HTML document will yield a correspondingly
ill-formed data structure. If your document is only locally
well-formed, you can use this to process the well-formed part of it.
#Example:
#--------
from BeautifulSoup import BeautifulSoup
text = '''<html>
<head><title>The Title</title></head>
<body>
<a class="foo" href="http://www.crummy.com/">Link <i>text (italicized)</i></a>
<a href="http://www.foo.com/">Link text 2</a>
</body>
</html>'''
soup = BeautifulSoup()
soup.feed(text)
print soup("a") #Returns a list of 2 Tag objects, one for each link in
#the source
print soup.first("a", {'class':'foo'})['href'] #Returns http://www.crummy.com/
print soup.first("title").contents[0] #Returns "The title"
print soup.first("a", {'href':'http://www.crummy.com/'}).first("i").contents[0]
#Returns "text (italicized)"
#Example of SQL-style attribute wildcards -- all four 'find' calls will
#find the link.
#----------------------------------------------------------------------
soup = BeautifulSoup()
soup.feed('''<a href="http://foo.com/">bla</a>''')
print soup.fetch('a', {'href': 'http://foo.com/'})
print soup.fetch('a', {'href': 'http://%'})
print soup.fetch('a', {'href': '%.com/'})
print soup.fetch('a', {'href': '%o.c%'})
#Example with horrible HTML:
#---------------------------
soup = BeautifulSoup()
soup.feed('''<body>
Go <a class="that" href="here.html"><i>here</i></a>
or <i>go <b><a href="index.html">Home</a>
</html>''')
print soup.fetch('a') #Returns a list of 2 Tag objects.
print soup.first(attrs={'href': 'here.html'})['class'] #Returns "that"
print soup.first(attrs={'class': 'that'}).first('i').contents[0] #returns "here"
This library has no external dependencies. It works with Python 1.5.2
and up. If you can install a Python extension, you might want to use
the ElementTree Tidy HTML Tree Builder instead:
http://www.effbot.org/zone/element-tidylib.htm
You can use BeautifulSoup on any SGML-like substance, such as XML or a
domain-specific language that looks like HTML but has different tag
names. For such purposes you may want to use the BeautifulStoneSoup
class, which knows nothing at all about HTML per se. I also reserve
the right to make the BeautifulSoup parser smarter between releases,
so if you want forwards-compatibility without having to think about
it, you might want to go with BeautifulStoneSoup.
Release status:
(I do a new release whenever I make a change that breaks backwards
compatibility.)
Current release:
Applied patch from Richie Hindle (richie at entrian dot com) that
makes tag.string a shorthand for tag.contents[0].string when the tag
has only one string-owning child.
1.2 "Who for such dainties would not stoop?" (2004/07/08): Applied
patch from Ben Last (ben at benlast dot com) that made
Tag.renderContents() correctly handle Unicode.
Made BeautifulStoneSoup even dumber by making it not implicitly
close a tag when another tag of the same type is encountered; only
when an actual closing tag is encountered. This change courtesy of
Fuzzy (mike at pcblokes dot com). BeautifulSoup still works as
before.
1.1 "Swimming in a hot tureen": Added more 'nestable' tags. Changed
popping semantics so that when a nestable tag is encountered, tags are
popped up to the previously encountered nestable tag (of whatever kind).
I will revert this if enough people complain, but it should make
more people's lives easier than harder.
This enhancement was suggested by Anthony Baxter (anthony at
interlink dot com dot au).
1.0 "So rich and green": Initial release.
Retreived from: http://www.crummy.com/software/BeautifulSoup/
"""
__author__ = "Leonard Richardson (leonardr@segfault.org)"
__version__ = "1.1 $Revision: 1.2 $"
__date__ = "$Date: 2004/08/27 20:06:12 $"
__copyright__ = "Copyright (c) 2004 Leonard Richardson"
__license__ = "Python"
from sgmllib import SGMLParser
import string
import types
class PageElement:
"""Contains the navigational information for some part of the page
(either a tag or a piece of text)"""
def __init__(self, parent=None, previous=None):
self.parent = parent
self.previous = previous
self.next = None
class NavigableText(PageElement):
"""A simple wrapper around a string that keeps track of where in
the document the string was found. Doesn't implement all the
string methods because I'm lazy. You could have this extend
UserString if you were using 2.2."""
def __init__(self, string, parent=None, previous=None):
PageElement.__init__(self, parent, previous)
self.string = string
def __eq__(self, other):
return self.string == str(other)
def __str__(self):
return self.string
def strip(self):
return self.string.strip()
class Tag(PageElement):
"""Represents a found HTML tag with its attributes and contents."""
def __init__(self, name, attrs={}, parent=None, previous=None):
PageElement.__init__(self, parent, previous)
self.name = name
self.attrs = attrs
self.contents = []
self.foundClose = 0
def get(self, key, default=None):
return self._getAttrMap().get(key, default)
def __call__(self, *args):
return apply(self.fetch, args)
def __getitem__(self, key):
return self._getAttrMap()[key]
def __setitem__(self, key, value):
self._getAttrMap()
self.attrMap[key] = value
for i in range(0, len(self.attrs)):
if self.attrs[i][0] == key:
self.attrs[i] = (key, value)
def _getAttrMap(self):
if not hasattr(self, 'attrMap'):
self.attrMap = {}
for (key, value) in self.attrs:
self.attrMap[key] = value
return self.attrMap
def __repr__(self):
return str(self)
def __ne__(self, other):
return not self == other
def __eq__(self, other):
if not isinstance(other, Tag) or self.name != other.name or self.attrs != other.attrs or len(self.contents) != len(other.contents):
return 0
for i in range(0, len(self.contents)):
if self.contents[i] != other.contents[i]:
return 0
return 1
def __str__(self):
attrs = ''
if self.attrs:
for key, val in self.attrs:
attrs = attrs + ' %s="%s"' % (key, val)
close = ''
closeTag = ''
if self.isSelfClosing():
close = ' /'
elif self.foundClose:
closeTag = '</%s>' % self.name
s = self.renderContents()
if not hasattr(self, 'hideTag'):
s = '<%s%s%s>' % (self.name, attrs, close) + s + closeTag
return s
def renderContents(self):
s='' #non-Unicode
for c in self.contents:
try:
s = s + str(c)
except UnicodeEncodeError:
if type(s) <> types.UnicodeType:
s = s.decode('utf8') #convert ascii to Unicode
#str() should, strictly speaking, not return a Unicode
#string, but NavigableText never checks and will return
#Unicode data if it was initialised with it.
s = s + str(c)
return s
def isSelfClosing(self):
return self.name in BeautifulSoup.SELF_CLOSING_TAGS
def append(self, tag):
self.contents.append(tag)
def first(self, name=None, attrs={}, contents=None, recursive=1):
r = None
l = self.fetch(name, attrs, contents, recursive)
if l:
r = l[0]
return r
def fetch(self, name=None, attrs={}, contents=None, recursive=1):
"""Extracts Tag objects that match the given criteria. You
can specify the name of the Tag, any attributes you want the
Tag to have, and what text and Tags you want to see inside the
Tag."""
if contents and type(contents) != type([]):
contents = [contents]
results = []
for i in self.contents:
if isinstance(i, Tag):
if not name or i.name == name:
match = 1
for attr, value in attrs.items():
check = i.get(attr)
#By default, find the specific value called for.
#Use SQL-style wildcards to find substrings, prefix,
#suffix, etc.
result = (check == value)
if check and value:
if len(value) > 1 and value[0] == '%' and value[-1] == '%' and value[-2] != '\\':
result = (check.find(value[1:-1]) != -1)
elif value[0] == '%':
print "blah"
result = check.rfind(value[1:]) == len(check)-len(value)+1
elif value[-1] == '%':
result = check.find(value[:-1]) == 0
if not result:
match = 0
break
match = match and (not contents or i.contents == contents)
if match:
results.append(i)
if recursive:
results.extend(i.fetch(name, attrs, contents, recursive))
return results
class BeautifulSoup(SGMLParser, Tag):
"""The actual parser. It knows the following facts about HTML, and
not much else:
* Some tags have no closing tag and should be interpreted as being
closed as soon as they are encountered.
* Most tags can't be nested; encountering an open tag when there's
already an open tag of that type in the stack means that the
previous tag of that type should be implicitly closed. However,
some tags can be nested. When a nestable tag is encountered,
it's okay to close all unclosed tags up to the last nestable
tag. It might not be safe to close any more, so that's all it
closes.
* The text inside some tags (ie. 'script') may contain tags which
are not really part of the document and which should be parsed
as text, not tags. If you want to parse the text as tags, you can
always get it and parse it explicitly."""
SELF_CLOSING_TAGS = ['br', 'hr', 'input', 'img', 'meta', 'spacer',
'link', 'frame']
NESTABLE_TAGS = ['font', 'table', 'tr', 'td', 'th', 'tbody', 'p',
'div']
QUOTE_TAGS = ['script']
IMPLICITLY_CLOSE_TAGS = 1
def __init__(self, text=None):
Tag.__init__(self, '[document]')
SGMLParser.__init__(self)
self.quoteStack = []
self.hideTag = 1
self.reset()
if text:
self.feed(text)
def feed(self, text):
SGMLParser.feed(self, text)
self.endData()
def reset(self):
SGMLParser.reset(self)
self.currentData = ''
self.currentTag = None
self.tagStack = []
self.pushTag(self)
def popTag(self, closedTagName=None):
tag = self.tagStack.pop()
if closedTagName == tag.name:
tag.foundClose = 1
# Tags with just one string-owning child get the same string
# property as the child, so that soup.tag.string is shorthand
# for soup.tag.contents[0].string
if len(self.currentTag.contents) == 1 and \
hasattr(self.currentTag.contents[0], 'string'):
self.currentTag.string = self.currentTag.contents[0].string
#print "Pop", tag.name
self.currentTag = self.tagStack[-1]
return self.currentTag
def pushTag(self, tag):
#print "Push", tag.name
if self.currentTag:
self.currentTag.append(tag)
self.tagStack.append(tag)
self.currentTag = self.tagStack[-1]
def endData(self):
if self.currentData:
if not string.strip(self.currentData):
if '\n' in self.currentData:
self.currentData = '\n'
else:
self.currentData = ' '
o = NavigableText(self.currentData, self.currentTag, self.previous)
if self.previous:
self.previous.next = o
self.previous = o
self.currentTag.contents.append(o)
self.currentData = ''
def _popToTag(self, name, closedTag=0):
"""Pops the tag stack up to and including the most recent
instance of the given tag. If a list of tags is given, will
accept any of those tags as an excuse to stop popping, and will
*not* pop the tag that caused it to stop popping."""
if self.IMPLICITLY_CLOSE_TAGS:
closedTag = 1
numPops = 0
mostRecentTag = None
oneTag = (type(name) == types.StringType)
for i in range(len(self.tagStack)-1, 0, -1):
thisTag = self.tagStack[i].name
if (oneTag and thisTag == name) \
or (not oneTag and thisTag in name):
numPops = len(self.tagStack)-i
break
if not oneTag:
numPops = numPops - 1
closedTagName = None
if closedTag:
closedTagName = name
for i in range(0, numPops):
mostRecentTag = self.popTag(closedTagName)
return mostRecentTag
def unknown_starttag(self, name, attrs):
if self.quoteStack:
#This is not a real tag.
#print "<%s> is not real!" % name
attrs = map(lambda(x, y): '%s="%s"' % (x, y), attrs)
self.handle_data('<%s %s>' % (name, attrs))
return
self.endData()
tag = Tag(name, attrs, self.currentTag, self.previous)
if self.previous:
self.previous.next = tag
self.previous = tag
if not name in self.SELF_CLOSING_TAGS:
if name in self.NESTABLE_TAGS:
self._popToTag(self.NESTABLE_TAGS)
else:
self._popToTag(name)
self.pushTag(tag)
if name in self.SELF_CLOSING_TAGS:
self.popTag()
if name in self.QUOTE_TAGS:
#print "Beginning quote (%s)" % name
self.quoteStack.append(name)
def unknown_endtag(self, name):
if self.quoteStack and self.quoteStack[-1] != name:
#This is not a real end tag.
#print "</%s> is not real!" % name
self.handle_data('</%s>' % name)
return
self.endData()
self._popToTag(name, 1)
if self.quoteStack and self.quoteStack[-1] == name:
#print "That's the end of %s!" % self.quoteStack[-1]
self.quoteStack.pop()
def handle_data(self, data):
self.currentData = self.currentData + data
def handle_comment(self, text):
"Propagate comments right through."
self.handle_data("<!--%s-->" % text)
def handle_charref(self, ref):
"Propagate char refs right through."
self.handle_data('&#%s;' % ref)
def handle_entityref(self, ref):
"Propagate entity refs right through."
self.handle_data('&%s;' % ref)
def handle_decl(self, data):
"Propagate DOCTYPEs right through."
self.handle_data('<!%s>' % data)
class BeautifulStoneSoup(BeautifulSoup):
"""A version of BeautifulSoup that doesn't know anything at all
about what HTML tags have special behavior. Useful for parsing
things that aren't HTML, or when BeautifulSoup makes an assumption
counter to what you were expecting."""
IMPLICITLY_CLOSE_TAGS = 0
SELF_CLOSING_TAGS = []
NESTABLE_TAGS = []
QUOTE_TAGS = []

View File

@ -0,0 +1 @@
Insert a description of your plugin here, with any notes, etc. about using it.

View File

@ -0,0 +1,69 @@
###
# Copyright (c) 2005, James Vega
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
"""
This plugin does weather-related stuff. It can't change the weather, though,
so don't get your hopes up. We just report it.
"""
import supybot
import supybot.world as world
# Use this for the version of this plugin. You may wish to put a CVS keyword
# in here if you're keeping the plugin in CVS or some similar system.
__version__ = "%%VERSION%%"
__author__ = supybot.authors.unknown
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {
supybot.authors.jamessan: ['cnn', 'wunder',
'temperatureUnit configuration variable',
'convert configuration variable'],
supybot.authors.jemfinch: ['weather'],
supybot.authors.bwp: ['ham'],
}
import config
import plugin
reload(plugin) # In case we're being reloaded.
# Add more reloads here if you add third-party modules and want them to be
# reloaded when this plugin is reloaded. Don't forget to import them as well!
import BeautifulSoup
reload(BeautifulSoup)
if world.testing:
import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

82
plugins/Weather/config.py Normal file
View File

@ -0,0 +1,82 @@
###
# Copyright (c) 2005, James Vega
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import plugin
import supybot.conf as conf
import supybot.utils as utils
import supybot.registry as registry
def configure(advanced):
# This will be called by supybot to configure this module. advanced is
# a bool that specifies whether the user identified himself as an advanced
# user or not. You should effect your configuration by manipulating the
# registry as appropriate.
from supybot.questions import expect, anything, something, yn
conf.registerPlugin('Weather', True)
class WeatherUnit(registry.String):
def setValue(self, s):
#print '***', repr(s)
s = s.capitalize()
if s not in plugin.unitAbbrevs:
raise registry.InvalidRegistryValue,\
'Unit must be one of Fahrenheit, Celsius, or Kelvin.'
s = plugin.unitAbbrevs[s]
registry.String.setValue(self, s)
class WeatherCommand(registry.String):
def setValue(self, s):
m = plugin.Weather.weatherCommands
if s not in m:
raise registry.InvalidRegistryValue,\
format('Command must be one of %L', m)
else:
method = getattr(plugin.Weather, s)
plugin.Weather.weather.im_func.__doc__ = method.__doc__
registry.String.setValue(self, s)
Weather = conf.registerPlugin('Weather')
conf.registerChannelValue(Weather, 'temperatureUnit',
WeatherUnit('Fahrenheit', """Sets the default temperature unit to use when
reporting the weather."""))
conf.registerChannelValue(Weather, 'command',
WeatherCommand('wunder', """Sets the default command to use when retrieving
the weather. Command must be one of %s.""" %
utils.str.commaAndify(plugin.Weather.weatherCommands, And='or')))
conf.registerChannelValue(Weather, 'convert',
registry.Boolean(True, """Determines whether the weather commands will
automatically convert weather units to the unit specified in
supybot.plugins.Weather.temperatureUnit."""))
conf.registerUserValue(conf.users.plugins.Weather, 'lastLocation',
registry.String('', ''))
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78

443
plugins/Weather/plugin.py Normal file
View File

@ -0,0 +1,443 @@
###
# Copyright (c) 2005, James Vega
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
import re
import BeautifulSoup
import supybot.utils as utils
from supybot.commands import *
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
unitAbbrevs = utils.abbrev(['Fahrenheit', 'Celsius', 'Centigrade', 'Kelvin'])
unitAbbrevs['C'] = 'Celsius'
unitAbbrevs['Ce'] = 'Celsius'
noLocationError = 'No such location could be found.'
class NoLocation(callbacks.Error):
pass
class Weather(callbacks.Privmsg):
weatherCommands = ['wunder', 'cnn', 'ham']
threaded = True
def __init__(self, irc):
self.__parent = super(Weather, self)
self.__parent.__init__(irc)
def callCommand(self, name, irc, msg, *L, **kwargs):
try:
self.__parent.callCommand(name, irc, msg, *L, **kwargs)
except utils.web.Error, e:
irc.error(str(e))
def _noLocation(self):
raise NoLocation, noLocationError
def weather(self, irc, msg, args, location):
# This specifically does not have a docstring.
channel = None
if irc.isChannel(msg.args[0]):
channel = msg.args[0]
if not location:
location = self.userValue('lastLocation', msg.prefix)
if not location:
raise callbacks.ArgumentError
self.setUserValue('lastLocation', msg.prefix,
location, ignoreNoUser=True)
args = [location]
realCommandName = self.registryValue('command', channel)
realCommand = getattr(self, realCommandName)
try:
realCommand(irc, msg, args[:])
except NoLocation:
self.log.info('%s lookup failed, Trying others.', realCommandName)
for command in self.weatherCommands:
if command != realCommandName:
self.log.info('Trying %s.', command)
try:
getattr(self, command)(irc, msg, args[:])
self.log.info('%s lookup succeeded.', command)
return
except NoLocation:
self.log.info('%s lookup failed as backup.', command)
irc.error(format('Could not retrieve weather for %q.', location))
weather = wrap(weather, [additional('text')])
def _toCelsius(self, temp, unit):
if unit == 'K':
return temp - 273.15
elif unit == 'F':
return (temp - 32) * 5 /9
else:
return temp
_temp = re.compile(r'(-?\d+)(.*?)(F|C)')
def _getTemp(self, temp, deg, unit, chan):
assert unit == unit.upper()
assert temp == int(temp)
default = self.registryValue('temperatureUnit', chan)
if unitAbbrevs[unit] == default:
# Short circuit if we're the same unit as the default.
return format('%i%s%s', temp, deg, unit)
temp = self._toCelsius(temp, unit)
unit = 'C'
if default == 'Kelvin':
temp = temp + 273.15
unit = 'K'
deg = ' '
elif default == 'Fahrenheit':
temp = temp * 9 / 5 + 32
unit = 'F'
return '%i%s%s' % (temp, deg, unit)
_hamLoc = re.compile(
r'<td><font size="4" face="arial"><b>'
r'(.*?), (.*?),(.*?)</b></font></td>', re.I)
_interregex = re.compile(
r'<td><font size="4" face="arial"><b>'
r'([^,]+), ([^<]+)</b></font></td>', re.I)
_hamCond = re.compile(
r'<td width="100%" colspan="2" align="center"><strong>'
r'<font face="arial">([^<]+)</font></strong></td>', re.I)
_hamTemp = re.compile(
r'<td valign="top" align="right"><strong><font face="arial">'
r'(-?\d+)(.*?)(F|C)</font></strong></td>', re.I)
_hamChill = re.compile(
r'Wind Chill</font></strong>:</small></td>\s+<td align="right">'
r'<small><font face="arial">([^N][^<]+)</font></small></td>',
re.I | re.S)
_hamHeat = re.compile(
r'Heat Index</font></strong>:</small></td>\s+<td align="right">'
r'<small><font face="arial">([^N][^<]+)</font></small></td>',
re.I | re.S)
# States
_realStates = set(['ak', 'al', 'ar', 'az', 'ca', 'co', 'ct',
'dc', 'de', 'fl', 'ga', 'hi', 'ia', 'id',
'il', 'in', 'ks', 'ky', 'la', 'ma', 'md',
'me', 'mi', 'mn', 'mo', 'ms', 'mt', 'nc',
'nd', 'ne', 'nh', 'nj', 'nm', 'nv', 'ny',
'oh', 'ok', 'or', 'pa', 'ri', 'sc', 'sd',
'tn', 'tx', 'ut', 'va', 'vt', 'wa', 'wi',
'wv', 'wy'])
# Provinces. (Province being a metric state measurement mind you. :D)
_fakeStates = set(['ab', 'bc', 'mb', 'nb', 'nf', 'ns', 'nt',
'nu', 'on', 'pe', 'qc', 'sk', 'yk'])
# Certain countries are expected to use a standard abbreviation
# The weather we pull uses weird codes. Map obvious ones here.
_hamCountryMap = {'uk': 'gb'}
def ham(self, irc, msg, args, loc):
"""<US zip code | US/Canada city, state | Foreign city, country>
Returns the approximate weather conditions for a given city.
"""
#If we received more than one argument, then we have received
#a city and state argument that we need to process.
if ' ' in loc:
#If we received more than 1 argument, then we got a city with a
#multi-word name. ie ['Garden', 'City', 'KS'] instead of
#['Liberal', 'KS']. We join it together with a + to pass
#to our query
loc = utils.str.rsplit(loc, None, 1)
state = loc.pop().lower()
city = '+'.join(loc)
city = city.rstrip(',').lower()
#We must break the States up into two sections. The US and
#Canada are the only countries that require a State argument.
if state in self._realStates:
country = 'us'
elif state in self._fakeStates:
country = 'ca'
else:
country = state
state = ''
if country in self._hamCountryMap.keys():
country = self._hamCountryMap[country]
url = 'http://www.hamweather.net/cgi-bin/hw3/hw3.cgi?' \
'pass=&dpp=&forecast=zandh&config=&' \
'place=%s&state=%s&country=%s' % (city, state, country)
html = utils.web.getUrl(url)
if 'was not found' in html:
url = 'http://www.hamweather.net/cgi-bin/hw3/hw3.cgi?' \
'pass=&dpp=&forecast=zandh&config=&' \
'place=%s&state=&country=%s' % (city, state)
html = utils.web.getUrl(url)
if 'was not found' in html: # Still.
self._noLocation()
#We received a single argument. Zipcode or station id.
else:
zip = loc.replace(',', '')
zip = zip.lower()
url = 'http://www.hamweather.net/cgi-bin/hw3/hw3.cgi?' \
'config=&forecast=zandh&pands=%s&Submit=GO' % zip
html = utils.web.getUrl(url)
if 'was not found' in html:
self._noLocation()
headData = self._hamLoc.search(html)
if headData is not None:
(city, state, country) = headData.groups()
else:
headData = self._interregex.search(html)
if headData:
(city, state) = headData.groups()
else:
self._noLocation()
city = city.strip()
state = state.strip()
temp = self._hamTemp.search(html)
convert = self.registryValue('convert', msg.args[0])
if temp is not None:
(temp, deg, unit) = temp.groups()
if convert:
temp = self._getTemp(int(temp), deg, unit, msg.args[0])
else:
temp = deg.join((temp, unit))
conds = self._hamCond.search(html)
if conds is not None:
conds = conds.group(1)
index = ''
chill = self._hamChill.search(html)
if chill is not None:
chill = chill.group(1)
chill = utils.web.htmlToText(chill)
if convert:
tempsplit = self._temp.search(chill)
if tempsplit:
(chill, deg, unit) = tempsplit.groups()
chill = self._getTemp(int(chill), deg, unit,msg.args[0])
if float(chill[:-2]) < float(temp[:-2]):
index = format(' (Wind Chill: %s)', chill)
heat = self._hamHeat.search(html)
if heat is not None:
heat = heat.group(1)
heat = utils.web.htmlToText(heat)
if convert:
tempsplit = self._temp.search(heat)
if tempsplit:
(heat, deg, unit) = tempsplit.groups()
if convert:
heat = self._getTemp(int(heat), deg, unit,msg.args[0])
if float(heat[:-2]) > float(temp[:-2]):
index = format(' (Heat Index: %s)', heat)
if temp and conds and city and state:
conds = conds.replace('Tsra', 'Thunderstorms')
conds = conds.replace('Ts', 'Thunderstorms')
s = format('The current temperature in %s, %s is %s%s. '
'Conditions: %s.',
city, state, temp, index, conds)
irc.reply(s)
else:
irc.errorPossibleBug('The format of the page was odd.')
ham = wrap(ham, ['text'])
_cnnUrl = 'http://weather.cnn.com/weather/search?wsearch='
_cnnFTemp = re.compile(r'(-?\d+)(&deg;)(F)</span>', re.I | re.S)
_cnnCond = re.compile(r'align="center"><b>([^<]+)</b></div></td>',
re.I | re.S)
_cnnHumid = re.compile(r'Rel. Humidity: <b>(\d+%)</b>', re.I | re.S)
_cnnWind = re.compile(r'Wind: <b>([^<]+)</b>', re.I | re.S)
_cnnLoc = re.compile(r'<title>([^<]+)</title>', re.I | re.S)
_cnnMultiLoc = re.compile(r'href="([^f]+forecast.jsp[^"]+)', re.I)
# Certain countries are expected to use a standard abbreviation
# The weather we pull uses weird codes. Map obvious ones here.
_cnnCountryMap = {'uk': 'en', 'de': 'ge'}
def cnn(self, irc, msg, args, loc):
"""<US zip code | US/Canada city, state | Foreign city, country>
Returns the approximate weather conditions for a given city.
"""
if ' ' in loc:
#If we received more than 1 argument, then we got a city with a
#multi-word name. ie ['Garden', 'City', 'KS'] instead of
#['Liberal', 'KS'].
loc = utils.str.rsplit(loc, None, 1)
state = loc.pop().lower()
city = ' '.join(loc)
city = city.rstrip(',').lower()
if state in self._cnnCountryMap:
state = self._cnnCountryMap[state]
loc = ' '.join([city, state])
else:
#We received a single argument. Zipcode or station id.
loc = loc.replace(',', '')
url = '%s%s' % (self._cnnUrl, utils.web.urlquote(loc))
text = utils.web.getUrl(url) # Errors caught in callCommand.
if 'No search results' in text or \
'does not match a zip code' in text:
self._noLocation()
elif 'several matching locations for' in text:
m = self._cnnMultiLoc.search(text)
if m:
text = utils.web.getUrl(m.group(1))
else:
self._noLocation()
location = self._cnnLoc.search(text)
temp = self._cnnFTemp.search(text)
conds = self._cnnCond.search(text)
humidity = self._cnnHumid.search(text)
wind = self._cnnWind.search(text)
convert = self.registryValue('convert', msg.args[0])
if location and temp:
location = location.group(1)
location = location.split('-')[-1].strip()
(temp, deg, unit) = temp.groups()
if convert:
temp = self._getTemp(int(temp), deg, unit, msg.args[0])
else:
temp = deg.join((temp, unit))
resp = [format('The current temperature in %s is %s.',
location, temp)]
if conds is not None:
resp.append(format('Conditions: %s.', conds.group(1)))
if humidity is not None:
resp.append(format('Humidity: %s.', humidity.group(1)))
if wind is not None:
resp.append(format('Wind: %s.', wind.group(1)))
resp = map(utils.web.htmlToText, resp)
irc.reply(' '.join(resp))
else:
irc.errorPossibleBug('Could not find weather information.')
cnn = wrap(cnn, ['text'])
_wunderUrl = 'http://mobile.wunderground.com/cgi-bin/' \
'findweather/getForecast?query='
_wunderSevere = re.compile(r'font color="?#ff0000"?>([^<]+)<', re.I)
_wunderLoc = re.compile(r'<title> (.+?) Forecast</title>', re.I | re.S)
_wunderMultiLoc = re.compile(r'<a href="([^"]+)', re.I | re.S)
def wunder(self, irc, msg, args, loc):
"""<US zip code | US/Canada city, state | Foreign city, country>
Returns the approximate weather conditions for a given city.
"""
url = '%s%s' % (self._wunderUrl, utils.web.urlquote(loc))
text = utils.web.getUrl(url)
severe = ''
m = self._wunderSevere.search(text)
self.log.critical('%s', m)
if m:
severe = ircutils.bold(format(' %s', m.group(1)))
if 'Search not found' in text or \
re.search(text, r'size="2"> Place </font>', re.I):
self._noLocation()
soup = BeautifulSoup.BeautifulSoup()
soup.feed(text)
# Get the table with all the weather info
table = soup.first('table', {'border':'1'})
if table is None:
self._noLocation()
trs = table.fetch('tr')
try:
time = trs.pop(0).first('b').string
except AttributeError:
time = ''
info = {}
def isText(t):
return not isinstance(t,BeautifulSoup.NavigableText) and t.contents
def getText(t):
s = getattr(t, 'string', None)
if s is None:
t = t.contents
num = t[0].string
units = t[1].string
# htmlToText strips leading whitespace, so we have to handle
# strings with &nbsp; differently.
if units.startswith('&nbsp;'):
units = utils.web.htmlToText(units)
s = ' '.join((num, units))
else:
units = utils.web.htmlToText(units)
s = ' '.join((num, units[0], units[1:]))
return s
for tr in trs:
k = tr.first('td').string
v = filter(isText, tr.fetch('td')[1].contents)
value = map(getText, v)
info[k] = ' '.join(value)
location = self._wunderLoc.search(text)
temp = info['Temperature']
convert = self.registryValue('convert', msg.args[0])
if location and temp:
(temp, deg, unit) = temp.split()
if convert:
temp = self._getTemp(int(temp), deg, unit, msg.args[0])
else:
temp = deg.join((temp, unit))
resp = ['The current temperature in %s is %s (%s).' %\
(location.group(1), temp, time)]
conds = info['Conditions']
resp.append('Conditions: %s.' % info['Conditions'])
humidity = info['Humidity']
resp.append('Humidity: %s.' % info['Humidity'])
# Apparently, the "Dew Point" and "Wind" categories are occasionally
# set to "-" instead of an actual reading. So, we'll just catch
# the ValueError from trying to unpack a tuple of the wrong size.
try:
(dew, deg, unit) = info['Dew Point'].split()
if convert:
dew = self._getTemp(int(dew), deg, unit, msg.args[0])
else:
dew = deg.join((dew, unit))
resp.append('Dew Point: %s.' % dew)
except ValueError:
pass
try:
resp.append('Wind: %s at %s %s.' % tuple(info['Wind'].split()))
except (ValueError, TypeError):
pass
try:
(chill, deg, unit) = info['Dew Point'].split()
if convert:
chill = self._getTemp(int(chill), deg, unit, msg.args[0])
else:
dew = deg.join((chill, unit))
resp.append('Windchill: %s.' % chill)
except (ValueError, TypeError):
pass
if info['Pressure']:
resp.append('Pressure: %s.' % info['Pressure'])
if info['Visibility']:
resp.append('Visibility: %s.' % info['Visibility'])
resp.append(severe)
resp = map(utils.web.htmlToText, resp)
irc.reply(' '.join(resp))
else:
irc.error('Could not find weather information.')
wunder = wrap(wunder, ['text'])
Class = Weather
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:

110
plugins/Weather/test.py Normal file
View File

@ -0,0 +1,110 @@
###
# Copyright (c) 2005, James Vega
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions, and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions, and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
# * Neither the name of the author of this software nor the name of
# contributors to this software may be used to endorse or promote products
# derived from this software without specific prior written consent.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
###
from supybot.test import *
class WeatherTestCase(PluginTestCase):
plugins = ('Weather',)
if network:
def testHam(self):
self.assertNotError('ham Columbus, OH')
self.assertNotError('ham 43221')
self.assertNotRegexp('ham Paris, FR', 'Virginia')
self.assertError('ham alsdkfjasdl, asdlfkjsadlfkj')
self.assertNotError('ham London, uk')
self.assertNotError('ham London, UK')
self.assertNotError('ham Munich, de')
self.assertNotError('ham Tucson, AZ')
self.assertError('ham hell')
def testCnn(self):
self.assertNotError('cnn Columbus, OH')
self.assertNotError('cnn 43221')
self.assertNotRegexp('cnn Paris, FR', 'Virginia')
self.assertError('cnn alsdkfjasdl, asdlfkjsadlfkj')
self.assertNotError('cnn London, uk')
self.assertNotError('cnn London, UK')
self.assertNotError('cnn Munich, de')
self.assertNotError('cnn Tucson, AZ')
def testWunder(self):
self.assertNotError('wunder Columbus, OH')
self.assertNotError('wunder 43221')
self.assertNotRegexp('wunder Paris, FR', 'Virginia')
self.assertError('wunder alsdkfjasdl, asdlfkjsadlfkj')
self.assertNotError('wunder London, uk')
self.assertNotError('wunder London, UK')
self.assertNotError('wunder Munich, de')
self.assertNotError('wunder Tucson, AZ')
def testTemperatureUnit(self):
try:
orig = conf.supybot.plugins.Weather.temperatureUnit()
conf.supybot.plugins.Weather.temperatureUnit.setValue('F')
self.assertRegexp('cnn Columbus, OH', r'is -?\d+.F')
self.assertRegexp('ham Columbus, OH', r'is -?\d+.F')
conf.supybot.plugins.Weather.temperatureUnit.setValue('C')
self.assertRegexp('cnn Columbus, OH', r'is -?\d+.C')
self.assertRegexp('ham Columbus, OH', r'is -?\d+.C')
conf.supybot.plugins.Weather.temperatureUnit.setValue('K')
self.assertRegexp('cnn Columbus, OH', r'is -?\d+\.15\sK')
self.assertRegexp('ham Columbus, OH', r'is -?\d+\.15\sK')
finally:
conf.supybot.plugins.Weather.temperatureUnit.setValue(orig)
def testNoEscapingWebError(self):
self.assertNotRegexp('ham "buenos aires"', 'WebError')
def testWeatherRepliesWithBogusLocation(self):
self.assertRegexp('weather some place that doesn\'t exist', r'.')
def testConvertConfig(self):
try:
convert = conf.supybot.plugins.Weather.convert()
unit = conf.supybot.plugins.Weather.temperatureUnit()
conf.supybot.plugins.Weather.convert.setValue(False)
conf.supybot.plugins.Weather.temperatureUnit.setValue('C')
self.assertRegexp('ham london, gb', r'-?\d+.C')
self.assertRegexp('ham 02115', r'-?\d+.F')
conf.supybot.plugins.Weather.temperatureUnit.setValue('F')
self.assertRegexp('ham london, gb', r'-?\d+.C')
self.assertRegexp('ham 02115', r'-?\d+.F')
conf.supybot.plugins.Weather.convert.setValue(True)
conf.supybot.plugins.Weather.temperatureUnit.setValue('C')
self.assertRegexp('ham london, gb', r'-?\d+.C')
self.assertRegexp('ham 02115', r'-?\d+.C')
conf.supybot.plugins.Weather.temperatureUnit.setValue('F')
self.assertRegexp('ham london, gb', r'-?\d+.F')
self.assertRegexp('ham 02115', r'-?\d+.F')
finally:
conf.supybot.plugins.Weather.convert.setValue(convert)
conf.supybot.plugins.Weather.temperatureUnit.setValue(unit)
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: