WunderWeather: Import plugin.

This commit is contained in:
Valentin Lorentz 2013-06-16 09:33:47 +02:00
parent d7f89cc97f
commit 6adea633db
6 changed files with 832 additions and 0 deletions

17
WunderWeather/README.txt Normal file
View 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
View 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
View 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
View 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
View 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
View 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: