WunderWeather: Import plugin.
This commit is contained in:
parent
d7f89cc97f
commit
6adea633db
17
WunderWeather/README.txt
Normal file
17
WunderWeather/README.txt
Normal file
@ -0,0 +1,17 @@
|
||||
Plugin which allows users to query weather conditions from Wunderground
|
||||
using their XML API.
|
||||
|
||||
To use:
|
||||
Download WunderWeather source
|
||||
Place source files in your supybot plugins folder
|
||||
Call "load WunderWeather" on your supybot
|
||||
|
||||
Configurable options:
|
||||
supybot.plugins.WunderWeather.metric
|
||||
supybot.plugins.WunderWeather.imperial
|
||||
supybot.plugins.WunderWeather.showPressure
|
||||
supybot.plugins.WunderWeather.forecastDays
|
||||
|
||||
Dependencies:
|
||||
None
|
||||
|
60
WunderWeather/__init__.py
Normal file
60
WunderWeather/__init__.py
Normal file
@ -0,0 +1,60 @@
|
||||
###
|
||||
# Copyright (c) 2005, James Vega
|
||||
# Copyright (c) 2009 Michael Tughan
|
||||
# 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
|
||||
|
||||
supybot.authors.mtughan = supybot.Author('Michael Tughan', 'mtughan', 'michaelsprogramming@gmail.com')
|
||||
|
||||
# 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.mtughan
|
||||
|
||||
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!
|
||||
|
||||
if world.testing:
|
||||
import test
|
||||
|
||||
Class = plugin.Class
|
||||
configure = config.configure
|
||||
|
||||
|
||||
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|
70
WunderWeather/config.py
Normal file
70
WunderWeather/config.py
Normal file
@ -0,0 +1,70 @@
|
||||
###
|
||||
# Copyright (c) 2005, James Vega
|
||||
# Copyright (c) 2009-2010 Michael Tughan
|
||||
# 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 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('WunderWeather', True)
|
||||
|
||||
WunderWeather = conf.registerPlugin('WunderWeather')
|
||||
conf.registerChannelValue(WunderWeather, 'imperial',
|
||||
registry.Boolean(True, """Shows imperial formatted data (Fahrenheit, miles/hour)
|
||||
in the weather output if true. You can have both imperial and metric enabled,
|
||||
and the bot will show both."""))
|
||||
conf.registerChannelValue(WunderWeather, 'metric',
|
||||
registry.Boolean(True, """Shows metric formatted data (Celsius, kilometres/hour)
|
||||
in the weather output if true. You can have both imperial and metric enabled,
|
||||
and the bot will show both."""))
|
||||
conf.registerChannelValue(WunderWeather, 'showPressure',
|
||||
registry.Boolean(True, """Determines whether the bot will show pressures in its
|
||||
output. The type of pressure shown will depend on the metric/imperial settings."""))
|
||||
conf.registerChannelValue(WunderWeather, 'forecastDays',
|
||||
registry.NonNegativeInteger(0, """Determines how many days the forecast shows, up to 7.
|
||||
If set to 0, show all days. See showForecast configuration variable to turn off
|
||||
forecast display."""))
|
||||
conf.registerChannelValue(WunderWeather, 'showCurrentByDefault',
|
||||
registry.Boolean(True, """If True, will show the current conditions in the weather
|
||||
output if no ouput control switches are given."""))
|
||||
conf.registerChannelValue(WunderWeather, 'showForecastByDefault',
|
||||
registry.Boolean(True, """If True, will show the forecast in the weather
|
||||
output if no ouput control switches are given."""))
|
||||
|
||||
conf.registerUserValue(conf.users.plugins.WunderWeather, 'lastLocation',
|
||||
registry.String('', ''))
|
||||
|
||||
|
||||
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|
303
WunderWeather/plugin.py
Normal file
303
WunderWeather/plugin.py
Normal file
@ -0,0 +1,303 @@
|
||||
###
|
||||
# Copyright (c) 2005, James Vega
|
||||
# Copyright (c) 2009-2010 Michael Tughan
|
||||
# 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 xml.dom.minidom as dom
|
||||
|
||||
import supybot.utils as utils
|
||||
from supybot.commands import wrap, additional, getopts
|
||||
import supybot.callbacks as callbacks
|
||||
|
||||
import shortforms
|
||||
reload(shortforms)
|
||||
|
||||
noLocationError = 'No such location could be found.'
|
||||
class NoLocation(callbacks.Error):
|
||||
pass
|
||||
|
||||
class WunderWeather(callbacks.Plugin):
|
||||
"""Uses the Wunderground XML API to get weather conditions for a given location.
|
||||
Always gets current conditions, and by default shows a 7-day forecast as well."""
|
||||
threaded = True
|
||||
|
||||
|
||||
########## GLOBAL VARIABLES ##########
|
||||
|
||||
_weatherCurrentCondsURL = 'http://api.wunderground.com/auto/wui/geo/WXCurrentObXML/index.xml?query=%s'
|
||||
_weatherForecastURL = 'http://api.wunderground.com/auto/wui/geo/ForecastXML/index.xml?query=%s'
|
||||
|
||||
|
||||
########## EXPOSED METHODS ##########
|
||||
|
||||
def weather(self, irc, msg, args, options, location):
|
||||
"""[--current|--forecast|--all] [US zip code | US/Canada city, state | Foreign city, country]
|
||||
|
||||
Returns the approximate weather conditions for a given city from Wunderground.
|
||||
--current, --forecast, and --all control what kind of information the command
|
||||
shows.
|
||||
"""
|
||||
matchedLocation = self._commandSetup(irc, msg, location)
|
||||
locationName = self._getNodeValue(matchedLocation[0], 'full', 'Unknown Location')
|
||||
|
||||
output = []
|
||||
showCurrent = False
|
||||
showForecast = False
|
||||
|
||||
if not options:
|
||||
# use default output
|
||||
showCurrent = self.registryValue('showCurrentByDefault', self.__channel)
|
||||
showForecast = self.registryValue('showForecastByDefault', self.__channel)
|
||||
else:
|
||||
for (type, arg) in options:
|
||||
if type == 'current':
|
||||
showCurrent = True
|
||||
elif type == 'forecast':
|
||||
showForecast = True
|
||||
elif type == 'all':
|
||||
showCurrent = True
|
||||
showForecast = True
|
||||
|
||||
if showCurrent and showForecast:
|
||||
output.append(u'Weather for ' + locationName)
|
||||
elif showCurrent:
|
||||
output.append(u'Current weather for ' + locationName)
|
||||
elif showForecast:
|
||||
output.append(u'Forecast for ' + locationName)
|
||||
|
||||
if showCurrent:
|
||||
output.append(self._getCurrentConditions(matchedLocation[0]))
|
||||
|
||||
if showForecast:
|
||||
# _getForecast returns a list, so we have to call extend rather than append
|
||||
output.extend(self._getForecast(matchedLocation[1]))
|
||||
|
||||
if not showCurrent and not showForecast:
|
||||
irc.error("Something weird happened... I'm not supposed to show current conditions or a forecast!")
|
||||
|
||||
irc.reply(self._formatUnicodeOutput(output))
|
||||
weather = wrap(weather, [getopts({'current': '', 'forecast': '', 'all': ''}), additional('text')])
|
||||
|
||||
|
||||
########## SUPPORTING METHODS ##########
|
||||
|
||||
def _checkLocation(self, location):
|
||||
if not location:
|
||||
location = self.userValue('lastLocation', self.__msg.prefix)
|
||||
if not location:
|
||||
raise callbacks.ArgumentError
|
||||
self.setUserValue('lastLocation', self.__msg.prefix, location, ignoreNoUser=True)
|
||||
|
||||
# Check for shortforms, because Wunderground will attempt to check
|
||||
# for US locations without a full country name.
|
||||
|
||||
# checkShortforms may return Unicode characters in the country name.
|
||||
# Need Latin 1 for Supybot's URL handlers to work
|
||||
webLocation = shortforms.checkShortforms(location)
|
||||
conditions = self._getDom(self._weatherCurrentCondsURL % utils.web.urlquote(webLocation))
|
||||
observationLocation = conditions.getElementsByTagName('observation_location')[0]
|
||||
|
||||
# if there's no city name in the XML, we didn't get a match
|
||||
if observationLocation.getElementsByTagName('city')[0].childNodes.length < 1:
|
||||
# maybe the country shortform given conflicts with a state shortform and wasn't replaced before
|
||||
webLocation = shortforms.checkConflictingShortforms(location)
|
||||
|
||||
# if no conflicting short names match, we have the same query as before
|
||||
if webLocation == None:
|
||||
return None
|
||||
|
||||
conditions = self._getDom(self._weatherCurrentCondsURL % utils.web.urlquote(webLocation))
|
||||
observationLocation = conditions.getElementsByTagName('observation_location')[0]
|
||||
|
||||
# if there's still no match, nothing more we can do
|
||||
if observationLocation.getElementsByTagName('city')[0].childNodes.length < 1:
|
||||
return None
|
||||
|
||||
# if we get this far, we got a match. Return the DOM and location
|
||||
return (conditions, webLocation)
|
||||
|
||||
def _commandSetup(self, irc, msg, location):
|
||||
channel = None
|
||||
if irc.isChannel(msg.args[0]):
|
||||
channel = msg.args[0]
|
||||
|
||||
# set various variables for submethods use
|
||||
self.__irc = irc
|
||||
self.__msg = msg
|
||||
self.__channel = channel
|
||||
|
||||
matchedLocation = self._checkLocation(location)
|
||||
if not matchedLocation:
|
||||
self._noLocation()
|
||||
|
||||
return matchedLocation
|
||||
|
||||
# format temperatures using _formatForMetricOrImperial
|
||||
def _formatCurrentConditionTemperatures(self, dom, string):
|
||||
tempC = self._getNodeValue(dom, string + '_c', u'N/A') + u'\xb0C'
|
||||
tempF = self._getNodeValue(dom, string + '_f', u'N/A') + u'\xb0F'
|
||||
return self._formatForMetricOrImperial(tempF, tempC)
|
||||
|
||||
def _formatForecastTemperatures(self, dom, type):
|
||||
tempC = self._getNodeValue(dom.getElementsByTagName(type)[0], 'celsius', u'N/A') + u'\xb0C'
|
||||
tempF = self._getNodeValue(dom.getElementsByTagName(type)[0], 'fahrenheit', u'N/A') + u'\xb0F'
|
||||
return self._formatForMetricOrImperial(tempF, tempC)
|
||||
|
||||
# formats any imperial or metric values according to the config
|
||||
def _formatForMetricOrImperial(self, imperial, metric):
|
||||
returnValues = []
|
||||
|
||||
if self.registryValue('imperial', self.__channel):
|
||||
returnValues.append(imperial)
|
||||
if self.registryValue('metric', self.__channel):
|
||||
returnValues.append(metric)
|
||||
|
||||
if not returnValues:
|
||||
returnValues = (imperial, metric)
|
||||
|
||||
return u' / '.join(returnValues)
|
||||
|
||||
def _formatPressures(self, dom):
|
||||
# lots of function calls, but it just divides pressure_mb by 10 and rounds it
|
||||
pressureKpa = str(round(float(self._getNodeValue(dom, 'pressure_mb', u'0')) / 10, 1)) + 'kPa'
|
||||
pressureIn = self._getNodeValue(dom, 'pressure_in', u'0') + 'in'
|
||||
return self._formatForMetricOrImperial(pressureIn, pressureKpa)
|
||||
|
||||
def _formatSpeeds(self, dom, string):
|
||||
mphValue = float(self._getNodeValue(dom, string, u'0'))
|
||||
speedM = u'%dmph' % round(mphValue)
|
||||
speedK = u'%dkph' % round(mphValue * 1.609344) # thanks Wikipedia for the conversion rate
|
||||
return self._formatForMetricOrImperial(speedM, speedK)
|
||||
|
||||
def _formatUpdatedTime(self, dom):
|
||||
observationTime = self._getNodeValue(dom, 'observation_epoch', None)
|
||||
localTime = self._getNodeValue(dom, 'local_epoch', None)
|
||||
if not observationTime or not localTime:
|
||||
return self._getNodeValue(dom, 'observation_time', 'Unknown Time').lstrip(u'Last Updated on ')
|
||||
|
||||
seconds = int(localTime) - int(observationTime)
|
||||
minutes = int(seconds / 60)
|
||||
seconds -= minutes * 60
|
||||
hours = int(minutes / 60)
|
||||
minutes -= hours * 60
|
||||
|
||||
if seconds == 1:
|
||||
seconds = '1 sec'
|
||||
else:
|
||||
seconds = '%d secs' % seconds
|
||||
|
||||
if minutes == 1:
|
||||
minutes = '1 min'
|
||||
else:
|
||||
minutes = '%d mins' % minutes
|
||||
|
||||
if hours == 1:
|
||||
hours = '1 hr'
|
||||
else:
|
||||
hours = '%d hrs' % hours
|
||||
|
||||
if hours == '0 hrs':
|
||||
if minutes == '0 mins':
|
||||
return '%s ago' % seconds
|
||||
return '%s, %s ago' % (minutes, seconds)
|
||||
return '%s, %s, %s ago' % (hours, minutes, seconds)
|
||||
|
||||
def _getCurrentConditions(self, dom):
|
||||
output = []
|
||||
|
||||
temp = self._formatCurrentConditionTemperatures(dom, 'temp')
|
||||
if self._getNodeValue(dom, 'heat_index_string') != 'NA':
|
||||
temp += u' (Heat Index: %s)' % self._formatCurrentConditionTemperatures(dom, 'heat_index')
|
||||
if self._getNodeValue(dom, 'windchill_string') != 'NA':
|
||||
temp += u' (Wind Chill: %s)' % self._formatCurrentConditionTemperatures(dom, 'windchill')
|
||||
output.append(u'Temperature: ' + temp)
|
||||
|
||||
output.append(u'Humidity: ' + self._getNodeValue(dom, 'relative_humidity', u'N/A%'))
|
||||
if self.registryValue('showPressure', self.__channel):
|
||||
output.append(u'Pressure: ' + self._formatPressures(dom))
|
||||
output.append(u'Conditions: ' + self._getNodeValue(dom, 'weather').capitalize())
|
||||
output.append(u'Wind: ' + self._getNodeValue(dom, 'wind_dir', u'None').capitalize() + ', ' + self._formatSpeeds(dom, 'wind_mph'))
|
||||
output.append(u'Updated: ' + self._formatUpdatedTime(dom))
|
||||
return u'; '.join(output)
|
||||
|
||||
def _getDom(self, url):
|
||||
try:
|
||||
xmlString = utils.web.getUrl(url)
|
||||
return dom.parseString(xmlString)
|
||||
except utils.web.Error, e:
|
||||
error = e.args[0].capitalize()
|
||||
if error[-1] != '.':
|
||||
error = error + '.'
|
||||
self.__irc.error(error, Raise=True)
|
||||
|
||||
def _getForecast(self, location):
|
||||
dom = self._getDom(self._weatherForecastURL % utils.web.urlquote(location))
|
||||
output = []
|
||||
count = 0
|
||||
max = self.registryValue('forecastDays', self.__channel)
|
||||
|
||||
forecast = dom.getElementsByTagName('simpleforecast')[0]
|
||||
|
||||
for day in forecast.getElementsByTagName('forecastday'):
|
||||
if count >= max and max != 0:
|
||||
break
|
||||
forecastOutput = []
|
||||
|
||||
forecastOutput.append('Forecast for ' + self._getNodeValue(day, 'weekday').capitalize() + ': ' + self._getNodeValue(day, 'conditions').capitalize())
|
||||
forecastOutput.append('High of ' + self._formatForecastTemperatures(day, 'high'))
|
||||
forecastOutput.append('Low of ' + self._formatForecastTemperatures(day, 'low'))
|
||||
output.append('; '.join(forecastOutput))
|
||||
count += 1
|
||||
|
||||
return output
|
||||
|
||||
|
||||
########## STATIC METHODS ##########
|
||||
|
||||
def _formatUnicodeOutput(output):
|
||||
# UTF-8 encoding is required for Supybot to handle \xb0 (degrees) and other special chars
|
||||
# We can't (yet) pass it a Unicode string on its own (an oddity, to be sure)
|
||||
return u' | '.join(output).encode('utf-8')
|
||||
_formatUnicodeOutput = staticmethod(_formatUnicodeOutput)
|
||||
|
||||
def _getNodeValue(dom, value, default=u'Unknown'):
|
||||
subTag = dom.getElementsByTagName(value)
|
||||
if len(subTag) < 1:
|
||||
return default
|
||||
subTag = subTag[0].firstChild
|
||||
if subTag == None:
|
||||
return default
|
||||
return subTag.nodeValue
|
||||
_getNodeValue = staticmethod(_getNodeValue)
|
||||
|
||||
def _noLocation():
|
||||
raise NoLocation, noLocationError
|
||||
_noLocation = staticmethod(_noLocation)
|
||||
|
||||
Class = WunderWeather
|
||||
|
330
WunderWeather/shortforms.py
Normal file
330
WunderWeather/shortforms.py
Normal file
@ -0,0 +1,330 @@
|
||||
# coding=utf-8
|
||||
###
|
||||
# Copyright (c) 2009 Michael Tughan
|
||||
# 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.
|
||||
###
|
||||
|
||||
encoding = 'utf-8'
|
||||
|
||||
# Provinces. (Province being a metric state measurement mind you. :D)
|
||||
_shortforms = {
|
||||
# Canadian provinces
|
||||
'ab': 'alberta',
|
||||
'bc': 'british columbia',
|
||||
'mb': 'manitoba',
|
||||
'nb': 'new brunswick',
|
||||
'nf': 'newfoundland',
|
||||
'ns': 'nova scotia',
|
||||
'nt': 'northwest territories',
|
||||
'nwt':'northwest territories',
|
||||
'nu': 'nunavut',
|
||||
'on': 'ontario',
|
||||
'pe': 'prince edward island',
|
||||
'pei':'prince edward island',
|
||||
'qc': 'quebec',
|
||||
'sk': 'saskatchewan',
|
||||
'yk': 'yukon',
|
||||
|
||||
# Countries
|
||||
'ad': 'andorra',
|
||||
'ae': 'united arab emirates',
|
||||
'af': 'afghanistan',
|
||||
'ag': 'antigua and barbuda',
|
||||
'ai': 'anguilla',
|
||||
'am': 'armenia',
|
||||
'an': 'netherlands antilles',
|
||||
'ao': 'angola',
|
||||
'aq': 'antarctica',
|
||||
'as': 'american samoa',
|
||||
'at': 'austria',
|
||||
'au': 'australia',
|
||||
'aw': 'aruba',
|
||||
'ax': u'åland islands',
|
||||
'ba': 'bosnia and herzegovina',
|
||||
'bb': 'barbados',
|
||||
'bd': 'bangladesh',
|
||||
'be': 'belgium',
|
||||
'bf': 'burkina faso',
|
||||
'bg': 'bulgaria',
|
||||
'bh': 'bahrain',
|
||||
'bi': 'burundi',
|
||||
'bj': 'benin',
|
||||
'bl': 'saint barthélemy',
|
||||
'bm': 'bermuda',
|
||||
'bn': 'brunei darussalam',
|
||||
'bo': 'bolivia',
|
||||
'br': 'brazil',
|
||||
'bs': 'bahamas',
|
||||
'bt': 'bhutan',
|
||||
'bv': 'bouvet island',
|
||||
'bw': 'botswana',
|
||||
'by': 'belarus',
|
||||
'bz': 'belize',
|
||||
'cc': 'cocos (keeling) islands',
|
||||
'cd': 'congo, the democratic republic of the',
|
||||
'cf': 'central african republic',
|
||||
'cg': 'congo',
|
||||
'ch': 'switzerland',
|
||||
'ci': 'côte d\'ivoire',
|
||||
'ck': 'cook islands',
|
||||
'cl': 'chile',
|
||||
'cm': 'cameroon',
|
||||
'cn': 'china',
|
||||
'cr': 'costa rica',
|
||||
'cu': 'cuba',
|
||||
'cv': 'cape verde',
|
||||
'cx': 'christmas island',
|
||||
'cy': 'cyprus',
|
||||
'cz': 'czech republic',
|
||||
'dj': 'djibouti',
|
||||
'dk': 'denmark',
|
||||
'dm': 'dominica',
|
||||
'do': 'dominican republic',
|
||||
'dz': 'algeria',
|
||||
'ec': 'ecuador',
|
||||
'ee': 'estonia',
|
||||
'eg': 'egypt',
|
||||
'eh': 'western sahara',
|
||||
'er': 'eritrea',
|
||||
'es': 'spain',
|
||||
'et': 'ethiopia',
|
||||
'fi': 'finland',
|
||||
'fj': 'fiji',
|
||||
'fk': 'falkland islands',
|
||||
'fm': 'micronesia',
|
||||
'fo': 'faroe islands',
|
||||
'fr': 'france',
|
||||
'gb': 'united kingdom',
|
||||
'gd': 'grenada',
|
||||
'ge': 'georgia',
|
||||
'gf': 'french guiana',
|
||||
'gg': 'guernsey',
|
||||
'gh': 'ghana',
|
||||
'gi': 'gibraltar',
|
||||
'gl': 'greenland',
|
||||
'gm': 'gambia',
|
||||
'gn': 'guinea',
|
||||
'gp': 'guadeloupe',
|
||||
'gq': 'equatorial guinea',
|
||||
'gr': 'greece',
|
||||
'gs': 'south georgia and the south sandwich islands',
|
||||
'gt': 'guatemala',
|
||||
'gu': 'guam',
|
||||
'gw': 'guinea-bissau',
|
||||
'gy': 'guyana',
|
||||
'hk': 'hong kong',
|
||||
'hm': 'heard island and mcdonald islands',
|
||||
'hn': 'honduras',
|
||||
'hr': 'croatia',
|
||||
'ht': 'haiti',
|
||||
'hu': 'hungary',
|
||||
'ie': 'ireland',
|
||||
'im': 'isle of man',
|
||||
'io': 'british indian ocean territory',
|
||||
'iq': 'iraq',
|
||||
'ir': 'iran, islamic republic of',
|
||||
'is': 'iceland',
|
||||
'it': 'italy',
|
||||
'je': 'jersey',
|
||||
'jm': 'jamaica',
|
||||
'jo': 'jordan',
|
||||
'jp': 'japan',
|
||||
'ke': 'kenya',
|
||||
'kg': 'kyrgyzstan',
|
||||
'kh': 'cambodia',
|
||||
'ki': 'kiribati',
|
||||
'km': 'comoros',
|
||||
'kn': 'saint kitts and nevis',
|
||||
'kp': 'north korea',
|
||||
'kr': 'south korea',
|
||||
'kw': 'kuwait',
|
||||
'kz': 'kazakhstan',
|
||||
'lb': 'lebanon',
|
||||
'lc': 'saint lucia',
|
||||
'li': 'liechtenstein',
|
||||
'lk': 'sri lanka',
|
||||
'lr': 'liberia',
|
||||
'ls': 'lesotho',
|
||||
'lt': 'lithuania',
|
||||
'lu': 'luxembourg',
|
||||
'lv': 'latvia',
|
||||
'ly': 'libyan arab jamahiriya',
|
||||
'mc': 'monaco',
|
||||
'mf': 'saint martin',
|
||||
'mg': 'madagascar',
|
||||
'mh': 'marshall islands',
|
||||
'mk': 'macedonia, the former yugoslav republic of',
|
||||
'ml': 'mali',
|
||||
'mm': 'myanmar',
|
||||
'mp': 'northern mariana islands',
|
||||
'mq': 'martinique',
|
||||
'mr': 'mauritania',
|
||||
'mu': 'mauritius',
|
||||
'mv': 'maldives',
|
||||
'mw': 'malawi',
|
||||
'mx': 'mexico',
|
||||
'my': 'malaysia',
|
||||
'mz': 'mozambique',
|
||||
'na': 'namibia',
|
||||
'nf': 'norfolk island',
|
||||
'ng': 'nigeria',
|
||||
'ni': 'nicaragua',
|
||||
'nl': 'netherlands',
|
||||
'no': 'norway',
|
||||
'np': 'nepal',
|
||||
'nr': 'nauru',
|
||||
'nu': 'niue',
|
||||
'nz': 'new zealand',
|
||||
'om': 'oman',
|
||||
'pe': 'peru',
|
||||
'pf': 'french polynesia',
|
||||
'pg': 'papua new guinea',
|
||||
'ph': 'philippines',
|
||||
'pk': 'pakistan',
|
||||
'pl': 'poland',
|
||||
'pm': 'saint pierre and miquelon',
|
||||
'pn': 'pitcairn',
|
||||
'pr': 'puerto rico',
|
||||
'ps': 'palestinian territory',
|
||||
'pt': 'portugal',
|
||||
'pw': 'palau',
|
||||
'py': 'paraguay',
|
||||
'qa': 'qatar',
|
||||
're': 'réunion',
|
||||
'ro': 'romania',
|
||||
'rs': 'serbia',
|
||||
'ru': 'russian federation',
|
||||
'rw': 'rwanda',
|
||||
'sa': 'saudi arabia',
|
||||
'sb': 'solomon islands',
|
||||
'se': 'sweden',
|
||||
'sg': 'singapore',
|
||||
'sh': 'saint helena',
|
||||
'si': 'slovenia',
|
||||
'sj': 'svalbard and jan mayen',
|
||||
'sk': 'slovakia',
|
||||
'sl': 'sierra leone',
|
||||
'sm': 'san marino',
|
||||
'sn': 'senegal',
|
||||
'so': 'somalia',
|
||||
'sr': 'suriname',
|
||||
'st': 'sao tome and principe',
|
||||
'sv': 'el salvador',
|
||||
'sy': 'syrian arab republic',
|
||||
'sz': 'swaziland',
|
||||
'tc': 'turks and caicos islands',
|
||||
'td': 'chad',
|
||||
'tf': 'french southern territories',
|
||||
'tg': 'togo',
|
||||
'th': 'thailand',
|
||||
'tj': 'tajikistan',
|
||||
'tk': 'tokelau',
|
||||
'tl': 'timor-leste',
|
||||
'tm': 'turkmenistan',
|
||||
'to': 'tonga',
|
||||
'tr': 'turkey',
|
||||
'tt': 'trinidad and tobago',
|
||||
'tv': 'tuvalu',
|
||||
'tw': 'taiwan',
|
||||
'tz': 'tanzania',
|
||||
'ua': 'ukraine',
|
||||
'ug': 'uganda',
|
||||
'um': 'united states minor outlying islands',
|
||||
'uy': 'uruguay',
|
||||
'uz': 'uzbekistan',
|
||||
'vc': 'saint vincent and the grenadines',
|
||||
've': 'venezuela, bolivarian republic of',
|
||||
'vg': 'virgin islands, british',
|
||||
'vi': 'virgin islands, u.s.',
|
||||
'vn': 'viet nam',
|
||||
'vu': 'vanuatu',
|
||||
'wf': 'wallis and futuna',
|
||||
'ws': 'samoa',
|
||||
'ye': 'yemen',
|
||||
'yt': 'mayotte',
|
||||
'za': 'south africa',
|
||||
'zm': 'zambia',
|
||||
'zw': 'zimbabwe'
|
||||
}
|
||||
|
||||
_conflictingShortforms = {
|
||||
'al': 'albania',
|
||||
'ar': 'argentina',
|
||||
'az': 'azerbaijan',
|
||||
'ca': 'canada',
|
||||
'co': 'colombia',
|
||||
'de': 'germany',
|
||||
'ga': 'gabon',
|
||||
'id': 'indonesia',
|
||||
'il': 'israel',
|
||||
'in': 'india',
|
||||
'ky': 'cayman islands',
|
||||
'la': 'laos',
|
||||
'ma': 'morocco',
|
||||
'md': 'moldova',
|
||||
'me': 'montenegro',
|
||||
'mn': 'mongolia',
|
||||
'mo': 'macao',
|
||||
'ms': 'montserrat',
|
||||
'mt': 'malta',
|
||||
'nc': 'new caledonia',
|
||||
'ne': 'niger',
|
||||
'pa': 'panama',
|
||||
'sc': 'seychelles',
|
||||
'sd': 'sudan',
|
||||
'tn': 'tunisia',
|
||||
'va': 'vatican city'
|
||||
}
|
||||
|
||||
def checkShortforms(query): # being Canadian, I often use something like "Toronto, ON"
|
||||
# but wunderground needs "Toronto, Ontario"
|
||||
if ' ' not in query and ',' not in query:
|
||||
return query # if there's no spaces or commas, it's one word, no need to check for provinces
|
||||
|
||||
lastWord = query.split()[-1].lower() # split by spaces, see if the last word is a province shortform
|
||||
if lastWord in _shortforms:
|
||||
return (query[0:0 - len(lastWord)] + _shortforms[lastWord]).encode(encoding)
|
||||
|
||||
lastWord = query.split(',')[-1].lower() # if it's not separated by spaces, maybe commas
|
||||
if lastWord in _shortforms:
|
||||
return (query[0:0 - len(lastWord)] + _shortforms[lastWord]).encode(encoding)
|
||||
|
||||
return query # nope, probably not a province name, return original query
|
||||
|
||||
def checkConflictingShortforms(query):
|
||||
if ' ' not in query and ',' not in query:
|
||||
return None
|
||||
|
||||
lastWord = query.split()[-1].lower()
|
||||
if lastWord in _conflictingShortforms:
|
||||
return (query[0:0 - len(lastWord)] + _conflictingShortforms[lastWord]).encode(encoding)
|
||||
|
||||
lastWord = query.split(',')[-1].lower()
|
||||
if lastWord in _conflictingShortforms:
|
||||
return (query[0:0 - len(lastWord)] + _conflictingShortforms[lastWord]).encode(encoding)
|
||||
|
||||
return None
|
52
WunderWeather/test.py
Normal file
52
WunderWeather/test.py
Normal file
@ -0,0 +1,52 @@
|
||||
###
|
||||
# Copyright (c) 2005, James Vega
|
||||
# Copyright (c) 2009 Michael Tughan
|
||||
# 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 = ('WunderWeather',)
|
||||
if network:
|
||||
def testWeather(self):
|
||||
self.assertNotError('weather Columbus, OH')
|
||||
self.assertNotError('weather 43221')
|
||||
self.assertNotRegexp('weather Paris, FR', 'Virginia')
|
||||
self.assertError('weather alsdkfjasdl, asdlfkjsadlfkj')
|
||||
self.assertNotError('weather London, uk')
|
||||
self.assertNotError('weather London, UK')
|
||||
self.assertNotError('weather London, england')
|
||||
self.assertNotError('weather Munich, de')
|
||||
self.assertNotError('weather Munich, germany')
|
||||
self.assertNotError('weather Tucson, AZ')
|
||||
# "Multiple locations found" test
|
||||
self.assertNotError('weather hell')
|
||||
self.assertNotError('weather sandwich')
|
||||
|
||||
|
||||
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|
Loading…
x
Reference in New Issue
Block a user