mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-27 13:19:24 +01:00
Move Weather to supybot-plugins.
This commit is contained in:
parent
2693d404b4
commit
587689f1bf
@ -1,450 +0,0 @@
|
|||||||
"""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 = []
|
|
@ -1 +0,0 @@
|
|||||||
Insert a description of your plugin here, with any notes, etc. about using it.
|
|
@ -1,71 +0,0 @@
|
|||||||
###
|
|
||||||
# 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 rssparser
|
|
||||||
import BeautifulSoup
|
|
||||||
reload(rssparser)
|
|
||||||
reload(BeautifulSoup)
|
|
||||||
|
|
||||||
if world.testing:
|
|
||||||
import test
|
|
||||||
|
|
||||||
Class = plugin.Class
|
|
||||||
configure = config.configure
|
|
||||||
|
|
||||||
|
|
||||||
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|
|
@ -1,79 +0,0 @@
|
|||||||
###
|
|
||||||
# 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)
|
|
||||||
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
|
|
@ -1,477 +0,0 @@
|
|||||||
###
|
|
||||||
# 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 rssparser
|
|
||||||
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.Plugin):
|
|
||||||
weatherCommands = ['wunder', 'rsswunder', 'cnn', 'ham']
|
|
||||||
threaded = True
|
|
||||||
def __init__(self, irc):
|
|
||||||
self.__parent = super(Weather, self)
|
|
||||||
self.__parent.__init__(irc)
|
|
||||||
|
|
||||||
def callCommand(self, method, irc, msg, *args, **kwargs):
|
|
||||||
try:
|
|
||||||
self.__parent.callCommand(method, irc, msg, *args, **kwargs)
|
|
||||||
except utils.web.Error, e:
|
|
||||||
irc.error(str(e))
|
|
||||||
|
|
||||||
def _noLocation(self):
|
|
||||||
raise NoLocation, noLocationError
|
|
||||||
|
|
||||||
def weather(self, irc, msg, args, location):
|
|
||||||
"""<US zip code | US/Canada city, state | Foreign city, country>
|
|
||||||
|
|
||||||
Returns the approximate weather conditions for a given city.
|
|
||||||
"""
|
|
||||||
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+)(°)(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)
|
|
||||||
if m:
|
|
||||||
severe = ircutils.bold(format(' %s', m.group(1)))
|
|
||||||
if 'Search not found' in text or \
|
|
||||||
re.search(r'size="2"> Place </font>', text, 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 differently.
|
|
||||||
if units.startswith(' '):
|
|
||||||
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()[3:] # We only want temp format
|
|
||||||
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()[3:]
|
|
||||||
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, KeyError):
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
resp.append('Wind: %s at %s %s.' % tuple(info['Wind'].split()))
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
(chill, deg, unit) = info['Windchill'].split()[3:]
|
|
||||||
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, KeyError):
|
|
||||||
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'])
|
|
||||||
|
|
||||||
_rsswunderUrl = 'http://www.wunderground.com/cgi-bin/findweather/' \
|
|
||||||
'getForecast?query=%s'
|
|
||||||
_rsswunderfeed = re.compile(r'<link rel="alternate".*href="([^"]+)">',
|
|
||||||
re.I)
|
|
||||||
_rsswunderSevere = re.compile(r'font color="?#ff0000"?><b>([^<]+)<', re.I)
|
|
||||||
def rsswunder(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 = self._rsswunderUrl % utils.web.urlquote(loc)
|
|
||||||
url = url.replace('%20', '+')
|
|
||||||
text = utils.web.getUrl(url)
|
|
||||||
severe = ''
|
|
||||||
m = self._rsswunderSevere.search(text)
|
|
||||||
if m:
|
|
||||||
severe = ircutils.bold(m.group(1))
|
|
||||||
if 'Search not found' in text or \
|
|
||||||
re.search(r'size="2"> Place </font>', text, re.I):
|
|
||||||
self._noLocation()
|
|
||||||
feed = self._rsswunderfeed.search(text)
|
|
||||||
if not feed:
|
|
||||||
irc._noLocation()
|
|
||||||
feed = feed.group(1)
|
|
||||||
rss = utils.web.getUrl(feed)
|
|
||||||
info = rssparser.parse(rss)
|
|
||||||
resp = [e['summary'] for e in info['entries']]
|
|
||||||
resp = [s.encode('utf-8') for s in resp]
|
|
||||||
resp.append(severe)
|
|
||||||
irc.reply(utils.web.htmlToText('; '.join(resp)))
|
|
||||||
rsswunder = wrap(rsswunder, ['text'])
|
|
||||||
|
|
||||||
Class = Weather
|
|
||||||
|
|
||||||
|
|
||||||
# vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78:
|
|
File diff suppressed because it is too large
Load Diff
@ -1,110 +0,0 @@
|
|||||||
###
|
|
||||||
# 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:
|
|
Loading…
Reference in New Issue
Block a user