Weather: remove and replace with a migration stub

This commit is contained in:
James Lu 2019-06-03 19:29:25 -07:00
parent 54bc27f297
commit fbda706503
11 changed files with 9 additions and 930 deletions

View File

@ -38,6 +38,9 @@ Please note that this list may not always be up to date; your best bet is to act
Most of these plugins also have their own READMEs in their folders; you can usually find a usage demonstration or further explanation of what they do.
##### AQI
- Retrieves [air quality index](https://en.wikipedia.org/wiki/Air_quality_index) info from the [World Air Quality Index project](https://aqicn.org).
##### CtcpNext
- Alternative to the official Ctcp plugin, with a database for configurable replies.
@ -104,10 +107,6 @@ Most of these plugins also have their own READMEs in their folders; you can usua
##### Voteserv
- A plugin for storing and manipulating votes/polls.
##### [Weather](Weather/README.md) **[DEPRECATED]**
- My fork of [reticulatingspline's Weather](https://github.com/reticulatingspline/Weather) plugin, with rewritten output handling, explicit location search, and many other tweaks.
- **Update 2018012**: Weather Underground is shutting down free weather access, so this plugin will no longer be maintained. See the NuWeather plugin in this folder for an alternative using other backends.
##### Wikifetch
- Fork of [ProgVal's Wikipedia plugin](https://github.com/ProgVal/Supybot-plugins), with support for other wikis (via a `--site` option) and other improvements.
- **Requires:** [lxml](https://lxml.de/installation.html)

35
Weather/.gitignore vendored
View File

@ -1,35 +0,0 @@
*.py[cod]
# C extensions
*.so
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
lib
lib64
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
nosetests.xml
# Translations
*.mo
# Mr Developer
.mr.developer.cfg
.project
.pydevproject

View File

@ -1,25 +0,0 @@
sudo: false
language: python
python:
- "2.7"
- "3.2"
- "3.3"
- "3.4"
- "3.5"
- "3.5-dev"
- pypy
- pypy3
# command to install dependencies,
install:
- pip install -vr requirements.txt
# command to run tests, e.g. python setup.py test
script:
- cd .. && mv Supybot-Weather Weather
- supybot-test Weather
notifications:
email: false
matrix:
fast_finish: true
allow_failures:
- python: "pypy"
- python: "pypy3"

View File

@ -1,21 +0,0 @@
The MIT License (MIT)
Copyright (c) 2014 spline
Copyright (c) 2014-2015 James Lu
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -1,62 +1,3 @@
# [DEPRECATED] Limnoria plugin for Weather Underground
The Weather plugin is no longer supported as Weather Underground has ceased providing free API access.
Update 201903: Weather Underground has shut down free weather access, so this plugin will no longer be maintained. See the [NuWeather](../NuWeather) plugin for an alternative using other backends.
## Installation
1) Download the plugin, either via Git or Limnoria's PluginDownloader (`install GLolol Weather`).
2) Load the plugin:
```
/msg bot load Weather
```
3) [Fetch an API key from Wunderground](http://www.wunderground.com/weather/api/) by signing up (free).
Once you get this key, you will need to set it up on your bot:
```
/msg <yourbot> config plugins.Weather.apiKey <APIKEY>
```
## Usage
When calling the `weather` command, you can provide locations in one of many forms:
- City names (e.g. Vancouver)
- U.S. ZIP codes (e.g. 10002)
- Canadian, U.K. postal codes (e.g. V6C 3T4)
- City, country pairs (e.g. "Sydney, Australia", "Paris, France")
- City, state/province pairs (e.g. "Washington, D.C.", "Kitchener, Ontario")
- [ICAO airport codes](https://en.wikipedia.org/wiki/International_Civil_Aviation_Organization_airport_code) (e.g. KJFK)
Example:
```
<GLolol> @weather 10002
<Atlas> New York, NY :: Mostly Cloudy :: 55F/12C (Humidity: 53%) | Monday: Mostly cloudy. Low 11C. Monday Night: Cloudy. Slight chance of a rain shower. Low 11C. Winds ENE at 10 to 15 km/h.
```
### Saving locations
Users can also have their location remembered by the bot so that they don't have to continually type in their location.
```
<GLolol> @setweather 10002
<Atlas> Done.
```
This allows a user to use the `weather` command without any arguments:
```
<GLolol> @weather
<Atlas> New York, NY :: Clear :: 64F/17C | Wind: N@7kph | Thursday: Clear. Low 14C. Thursday Night: A clear sky. Low 14C. Winds SSE at 10 to 15 km/h.
```
### User options
Users can also have the bot remember their preferred options, such as using metric units when displaying forecasts:
```
<GLolol> @setuser metric True
<Atlas> Done.
```
Please consider using migrating to an alternative such as https://github.com/jlu5/SupyPlugins/tree/master/NuWeather

View File

@ -1,45 +1,5 @@
###
# Copyright (c) 2012-2014, spline
# All rights reserved.
###
#!/usr/bin/env python3
from supybot import callbacks
"""
Add a description of the plugin (to be presented to the user inside the wizard)
here. This should describe *what* the plugin does.
"""
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__ = "2019.03.03+git"
# XXX Replace this with an appropriate author or supybot.Author instance.
__author__ = supybot.Author('James Lu', 'GLolol', 'GLolol@overdrivenetworks.com')
# This is a dictionary mapping supybot.Author instances to lists of
# contributions.
__contributors__ = {}
# This is a url where the most recent plugin package can be downloaded.
__url__ = 'https://github.com/GLolol/SupyPlugins/'
from . import config
from . import plugin
from imp import reload
# In case we're being reloaded.
reload(config)
reload(plugin)
# 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:
from . import test
Class = plugin.Class
configure = config.configure
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
raise callbacks.Error("The Weather plugin is no longer supported as Weather Underground has ceased providing free API access. "
"Please consider using migrating to an alternative such as https://github.com/jlu5/SupyPlugins/tree/master/NuWeather")

View File

@ -1,44 +0,0 @@
###
# Copyright (c) 2012-2014, spline
# All rights reserved.
###
import supybot.conf as conf
import supybot.registry as registry
from supybot.i18n import PluginInternationalization, internationalizeDocstring
_ = PluginInternationalization('Weather')
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)
Weather = conf.registerPlugin('Weather')
conf.registerGlobalValue(Weather, 'apiKey',
registry.String('', ("""Sets the API key for the plugin. You can obtain an API key at http://www.wunderground.com/weather/api/."""), private=True))
conf.registerChannelValue(Weather, 'useImperial',
registry.Boolean(True, ("""Determines whether imperial units (Fahrenheit, etc.) will be used.""")))
conf.registerGlobalValue(Weather,'forecast',
registry.Boolean(True, ("""Determines whether forecasts will be displayed by default.""")))
conf.registerGlobalValue(Weather,'alerts',
registry.Boolean(False, ("""Determines whether forecasts will be displayed by default.""")))
conf.registerGlobalValue(Weather, 'almanac',
registry.Boolean(False, ("""Determines whether almanac will be displayed by default.""")))
conf.registerGlobalValue(Weather, 'astronomy',
registry.Boolean(False, ("""Determines whether astronomy will be displayed by default.""")))
conf.registerGlobalValue(Weather, 'showPressure',
registry.Boolean(False, ("""Determines whether pressure will be displayed by default.""")))
conf.registerGlobalValue(Weather, 'showWind',
registry.Boolean(False, ("""Determines whether winde will be displayed by default.""")))
conf.registerGlobalValue(Weather, 'showUpdated',
registry.Boolean(False, ("""Determines whether the bot will show the data's "last updated" time by default.""")))
conf.registerGlobalValue(Weather, 'lang',
registry.String('EN', ("""Determines the language used by the plugin.""")))
conf.registerChannelValue(Weather, 'disableColoredTemp',
registry.Boolean(False, """If True, this will disable coloring temperatures based on values."""))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=250:

View File

@ -1 +0,0 @@
# Stub so local is a module, used for third-party modules

View File

@ -1,628 +0,0 @@
# -*- coding: utf-8 -*-
###
# Copyright (c) 2012-2014, spline
# Copyright (c) 2014-2018, James Lu <james@overdrivenetworks.com>
# All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
###
from __future__ import unicode_literals
import json
from math import floor
import sqlite3
import string
try:
from itertools import izip
except ImportError: # Python 3
izip = zip
import supybot.conf as conf
import supybot.log as log
import supybot.utils as utils
from supybot.commands import *
import supybot.plugins as plugins
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
try:
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('Weather')
except ImportError:
# Placeholder that allows to run the plugin on a bot
# without the i18n module
_ = lambda x:x
class WeatherDB():
"""WeatherDB class to store our users and their settings."""
def __init__(self):
self.filename = conf.supybot.directories.data.dirize("Weather.db")
self.log = log.getPluginLogger('Weather')
self._conn = sqlite3.connect(self.filename, check_same_thread=False)
self._conn.text_factory = str
self.makeDb()
def makeDb(self):
"""Create our DB."""
self.log.info("Weather: Checking/Creating DB.")
with self._conn as conn:
cursor = conn.cursor()
cursor.execute("""CREATE TABLE IF NOT EXISTS users (
nick TEXT PRIMARY KEY,
location TEXT NOT NULL,
metric INTEGER DEFAULT 0,
alerts INTEGER DEFAULT 0,
almanac INTEGER DEFAULT 0,
astronomy INTEGER DEFAULT 0,
forecast INTEGER DEFAULT 0,
pressure INTEGER DEFAULT 0,
wind INTEGER DEFAULT 0,
uv INTEGER DEFAULT 0,
visibility INTEGER DEFAULT 0,
dewpoint INTEGER DEFAULT 0,
humidity INTEGER DEFAULT 0,
updated INTEGER DEFAULT 0)""")
self._conn.commit() # this fails silently if already there.
# next, we see if we need to upgrade the old table structure.
cursor = conn.cursor() # the old table is 4.
tablelength = len([l[1] for l in cursor.execute("pragma table_info('users')").fetchall()])
if tablelength == 4: # old table is 4: users, location, metric, colortemp.
self.log.info("Weather: Upgrading database version.")
columns = ['alerts', 'almanac', 'astronomy', 'forecast', 'pressure', 'wind', 'uv', 'visibility', 'dewpoint', 'humidity', 'updated']
for column in columns:
try:
cursor.execute('ALTER TABLE users ADD COLUMN %s INTEGER DEFAULT 0' % column)
self._conn.commit()
except: # fail silently.
pass
def setweather(self, username, location):
"""Stores or update a user's location. Adds user if not found."""
with self._conn as conn:
cursor = conn.cursor()
if self.getuser(username): # username exists.
cursor.execute("""UPDATE users SET location=? WHERE nick=?""", (location, username,))
else: # username does not exist so add it in.
cursor.execute("""INSERT OR REPLACE INTO users (nick, location) VALUES (?,?)""", (username, location,))
self._conn.commit() # commit.
def setsetting(self, username, setting, value):
"""Set one of the user settings."""
with self._conn as conn:
cursor = conn.cursor()
query = "UPDATE users SET %s=? WHERE nick=?" % setting
cursor.execute(query, (value, username,))
self._conn.commit()
def getsettings(self):
"""Get all 'user' settings that can be set."""
with self._conn as conn:
cursor = conn.cursor() # below, we get all column names that are settings (INTEGERS)
settings = [str(l[1]) for l in cursor.execute("pragma table_info('users')").fetchall() if l[2] == "INTEGER"]
return settings
def getweather(self, user):
"""Return a dict of user's settings."""
self._conn.row_factory = sqlite3.Row
with self._conn as conn:
cursor = conn.cursor()
cursor.execute("""SELECT * from users where nick=?""", (user,))
row = cursor.fetchone()
if not row: # user does not exist.
return None
else: # user exists.
rowdict = dict(izip(row.keys(), row))
return rowdict
def getuser(self, user):
"""Returns a boolean if a user exists."""
with self._conn as conn:
cursor = conn.cursor()
cursor.execute("""SELECT location from users where nick=?""", (user,))
row = cursor.fetchone()
if row:
return True
else:
return False
class WeatherAPIError(RuntimeError):
pass
class Weather(callbacks.Plugin):
"""This plugin provides access to information from Weather Underground."""
threaded = True
def __init__(self, irc):
self.__parent = super(Weather, self)
self.__parent.__init__(irc)
self.db = WeatherDB()
##############
# FORMATTING #
##############
def _bold(self, string):
return ircutils.bold(string)
def _bu(self, string):
return ircutils.underline(ircutils.bold(string))
############################
# INTERNAL WEATHER HELPERS #
############################
def _temp(self, channel, f, c=None):
"""Returns a colored string based on the temperature."""
# lets be safe and wrap in a try/except because we can't always trust data purity.
try:
if str(f).startswith('NA'): # Wunderground sends a field that's not available
return f
f = int(f)
if not c:
c = int((f - 32) * 5/9)
s = "{0}F/{1}C".format(f, c)
# determine color.
if not self.registryValue('disableColoredTemp', channel):
if f < 10.0:
color = 'light blue'
elif 10.0 <= f <= 32.0:
color = 'teal'
elif 32.1 <= f <= 50.0:
color = 'blue'
elif 50.1 <= f <= 60.0:
color = 'light green'
elif 60.1 <= f <= 70.0:
color = 'green'
elif 70.1 <= f <= 80.0:
color = 'yellow'
elif 80.1 <= f <= 90.0:
color = 'orange'
elif f > 90.0:
color = 'red'
else:
color = 'light grey'
s = ircutils.mircColor(s, color)
# return.
return s
except (TypeError, ValueError) as e:
self.log.info("Weather: ValueError trying to convert temp: {0} message: {1}".format(f, e))
return "N/A"
def _wind(self, angle, useSymbols=False):
"""Converts degrees to direction for wind. Can optionally return a symbol."""
if not useSymbols: # ordinal names.
direction_names = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
else: # symbols.
direction_names = ['', '', '', '', '', '', '', '']
# do math below to figure the angle->direction out.
directions_num = len(direction_names)
directions_step = 360./directions_num
index = int(round((angle/360. - floor(angle/360.)*360.)/directions_step))
index %= directions_num
# return.
return direction_names[index]
@staticmethod
def _format_geolookup_name(result):
"""Formats a place name from Wunderground Geolookup."""
if result['state'] and not result['state'].isdigit():
template = '{city}, {state}, {country_name}'
else:
template = '{city}, {country_name}'
return template.format(**result)
##############################################
# PUBLIC FUNCTIONS TO WORK WITH THE DATABASE #
##############################################
def setuser(self, irc, msg, args, optset, optbool):
"""<setting> <True|False>
Sets a user's <setting> to True or False.
Valid settings include: alerts, almanac, astronomy, forecast, pressure,
wind, uv, visibility, dewpoint, humidity, and updated.
"""
# first, lower
optset = optset.lower()
# grab a list of valid settings.
validset = self.db.getsettings()
if optset not in validset:
irc.error(format("%r is an invalid setting. Must be one of: %L.", optset,
sorted(validset)), Raise=True)
if optbool: # True.
value = 1
else: # False.
value = 0
# check user first.
if not self.db.getuser(msg.nick.lower()): # user exists
irc.error("You are not in the database; you must use 'setweather' first.", Raise=True)
else: # user is valid. perform the op.
self.db.setsetting(msg.nick.lower(), optset, value)
irc.replySuccess()
setuser = wrap(setuser, [('somethingWithoutSpaces'), ('boolean')])
def setweather(self, irc, msg, args, optlocation):
"""<location code>
Sets the weather location for your nick. Location codes can be city names, "City, Country"
pairs, ICAO airport codes, US ZIP codes, or raw zmw codes as returned by the
'locationsearch' command.
"""
self.db.setweather(msg.nick.lower(), optlocation)
irc.replySuccess()
setweather = wrap(setweather, [('text')])
##########################
# WUNDERGROUND API CALLS #
##########################
def _wuac(self, q, return_names=False):
"""Internal helper to find locations via Wunderground's GeoLookup API.
Previous versions of this plugin used the Autocompete API instead."""
if q.startswith('zmw:'):
# If we're given a ZMW code, just return it as is.
return [q]
apikey = self.registryValue('apiKey')
if not apikey:
raise callbacks.Error("No Wunderground API key was defined; set "
"the 'plugins.Weather.apiKey' config variable.")
url = 'http://api.wunderground.com/api/%s/geolookup/q/%s.json' % (apikey, utils.web.urlquote(q))
self.log.debug("Weather: GeoLookup URL %s", url)
page = utils.web.getUrl(url, timeout=5)
data = json.loads(page.decode('utf-8'))
if data.get('location'):
# This form is used when there's only one result.
zmw = 'zmw:{zip}.{magic}.{wmo}'.format(**data['location'])
if return_names:
name = self._format_geolookup_name(data['location'])
return [(name, zmw)]
else:
return [zmw]
else:
if data['response'].get('error'):
errdata = data['response']['error']
raise WeatherAPIError('Error in _wuac step: [%s] %s' %
(errdata.get('type', 'N/A'),
errdata.get('description', 'No message specified')))
# This form of result is returned there are multiple places matching a query
results = data['response'].get('results')
if not results:
return []
if return_names:
results = [(self._format_geolookup_name(result), 'zmw:' + result['zmw']) for result in results]
else:
results = [('zmw:' + result['zmw']) for result in results]
return results
####################
# PUBLIC FUNCTIONS #
####################
@wrap([getopts({'user': 'nick'}), optional('text')])
def weather(self, irc, msg, args, optlist, location):
"""[--user <othernick>] [<location>]
Fetches weather and forecast information for <location>. <location> can be left blank if you have a previously set location (via 'setweather').
If the --user option is specified, show weather for the saved location of that nick, instead of the caller.
Location can take many forms, including a simple city name, US state/city (CA/San_Francisco), zip code, country/city (Australia/Sydney), or an airport code (KJFK).
Ex: 10021 or Sydney, Australia or KJFK
"""
apikey = self.registryValue('apiKey')
if not apikey:
irc.error("No Wunderground API key was defined; set the 'plugins.Weather.apiKey' config variable.",
Raise=True)
channel = msg.args[0]
optlist = dict(optlist)
# Default to looking at the caller's saved info, but optionally they can look at someone else's weather too.
nick = optlist.get('user') or msg.nick
# urlargs will be used to build the url to query the API.
# besides lang, these are preset values that should not be changed.
urlArgs = {'features': ['conditions', 'forecast'],
'lang': self.registryValue('lang'),
'bestfct': '1',
'pws': '0' }
loc = None
args = {'imperial': self.registryValue('useImperial', msg.args[0]),
'alerts': self.registryValue('alerts'),
'almanac': self.registryValue('almanac'),
'astronomy': self.registryValue('astronomy'),
'pressure': self.registryValue('showPressure'),
'wind': self.registryValue('showWind'),
'updated': self.registryValue('showUpdated'),
'forecast': False,
'humidity': False,
'uv': False,
'visibility': False,
'dewpoint': False}
usersetting = self.db.getweather(nick.lower())
if usersetting:
for (k, v) in usersetting.items():
args[k] = v
# Prefer the location given in the command, falling back to the one stored in the DB if not given.
location = location or usersetting["location"]
args['imperial'] = (not usersetting["metric"])
# If both command line and DB locations aren't given, bail.
if not location:
if nick != msg.nick:
irc.error("I did not find a preset location for %s." % nick, Raise=True)
else:
irc.error("I did not find a preset location for you. Set one via 'setweather <location>'.", Raise=True)
loc = self._wuac(location)
if not loc:
irc.error("Failed to find a valid location for: %r" % location, Raise=True)
else:
# Use the first location.
loc = loc[0]
for check in ['alerts', 'almanac', 'astronomy']:
if args[check]:
urlArgs['features'].append(check) # append to dict->key (list)
baseurl = 'http://api.wunderground.com/api/%s/' % apikey
# Prepare API options
for (key, value) in urlArgs.items():
if key == "features": # will always be at least conditions.
# Join features directly to the URL
baseurl += "/".join(value)
baseurl += "/"
if key in ("lang", "bestfct", "pws"):
# Preset and configured (only lang) options, added with key:value
baseurl += "{0}:{1}/".format(key, value)
url = '%s/q/%s.json' % (baseurl.rstrip('/'), loc)
self.log.debug("Weather URL: {0}".format(url))
page = utils.web.getUrl(url, timeout=5)
data = json.loads(page.decode('utf-8'))
if data['response'].get('error'):
errdata = data['response']['error']
raise WeatherAPIError('Error in weather step: [%s] %s' %
(errdata.get('type', 'N/A'),
errdata.get('description', 'No message specified')))
elif 'current_observation' not in data:
irc.error("Failed to fetch current conditions for %r." % loc, Raise=True)
outdata = {'weather': data['current_observation']['weather'],
'location': data['current_observation']['display_location']['full'],
'humidity': data['current_observation']['relative_humidity'],
'uv': data['current_observation']['UV']}
if data['current_observation']['wind_mph'] < 1: # no wind.
outdata['wind'] = "None"
else:
if args['imperial']:
outdata['wind'] = "{0}@{1}mph".format(self._wind(data['current_observation']['wind_degrees']), data['current_observation']['wind_mph'])
if int(data['current_observation']['wind_gust_mph']) > 0:
outdata['wind'] += " ({0}mph gusts)".format(data['current_observation']['wind_gust_mph'])
else:
outdata['wind'] = "{0}@{1}kph".format(self._wind(data['current_observation']['wind_degrees']),data['current_observation']['wind_kph'])
if int(data['current_observation']['wind_gust_kph']) > 0:
outdata['wind'] += " ({0}kph gusts)".format(data['current_observation']['wind_gust_kph'])
# Show the last updated time if available.
observationTime = data['current_observation'].get('observation_epoch')
localTime = data['current_observation'].get('local_epoch')
if not observationTime or not localTime:
outdata['observation'] = data.get('observation_time', 'unknown').lstrip('Last Updated on ')
else: # Prefer relative times, if available
s = int(localTime) - int(observationTime) # format into seconds.
if s <= 1:
outdata['observation'] = 'just now'
elif s < 60:
outdata['observation'] = '{0}s ago'.format(s)
elif s < 120:
outdata['observation'] = '1m ago'
elif s < 3600:
outdata['observation'] = '{0}m ago'.format(s/60)
elif s < 7200:
outdata['observation'] = '1hr ago'
else:
outdata['observation'] = '{0}hrs ago'.format(s/3600)
outdata['temp'] = self._temp(channel, data['current_observation']['temp_f'])
# pressure.
pin = str(data['current_observation']['pressure_in']) + 'in'
pmb = str(data['current_observation']['pressure_mb']) + 'mb'
outdata['pressure'] = "{0}/{1}".format(pin, pmb)
# dewpoint.
outdata['dewpoint'] = self._temp(channel, data['current_observation']['dewpoint_f'])
# heatindex.
outdata['heatindex'] = self._temp(channel, data['current_observation']['heat_index_f'])
# windchill.
outdata['windchill'] = self._temp(channel, data['current_observation']['windchill_f'])
# feels like
outdata['feelslike'] = self._temp(channel, data['current_observation']['feelslike_f'])
# visibility.
vmi = str(data['current_observation']['visibility_mi']) + 'mi'
vkm = str(data['current_observation']['visibility_km']) + 'km'
outdata['visibility'] = "{0}/{1}".format(vmi, vkm)
# handle forecast data. This is internally stored as a dict with integer keys (days from now)
# with the forecast text as values.
forecastdata = {}
if 'forecast' in data:
for forecastday in data['forecast']['txt_forecast']['forecastday']:
# Slightly different wording and results (e.g. rainfall for X inches vs. X cm) are given
# depending on whether imperial or metric units are the same.
if args['imperial']:
text = forecastday['fcttext']
else:
text = forecastday['fcttext_metric']
forecastdata[int(forecastday['period'])] = {'day': forecastday['title'],
'text': text}
output = "{0} :: {1} ::".format(self._bold(outdata['location']), outdata['weather'])
output += " {0} ".format(outdata['temp'])
# humidity.
if args['humidity']:
output += "(Humidity: {0}) ".format(outdata['humidity'])
# windchill/heatindex are conditional on season but test with startswith to see what to include
# NA means not available, so ignore those fields
if not outdata['windchill'].startswith("NA"):
output += "| {0} {1} ".format(self._bold('Wind Chill:'), outdata['windchill'])
if not outdata['heatindex'].startswith("NA"):
output += "| {0} {1} ".format(self._bold('Heat Index:'), outdata['heatindex'])
# Iterate over the args dict for what extra data to include
for k in ('wind', 'visibility', 'uv', 'pressure', 'dewpoint'):
if args[k]:
output += "| {0}: {1} ".format(self._bold(k.title()), outdata[k])
if forecastdata:
# Add in the first two forecasts item in conditions + the "last updated" time.
output += "| {0}: {1}".format(self._bold(forecastdata[0]['day']), forecastdata[0]['text'])
output += " {0}: {1}".format(self._bold(forecastdata[1]['day']), forecastdata[1]['text'])
if args['updated']:
# Round updated time (given as a string) to the nearest unit.
# This is annoying because Wunderground sends these as raw strings, in the form
# "1hr ago" or "2.7666666666666666m ago"
tailstr = outdata['observation'].lstrip(string.digits + '.')
updated_time = outdata['observation'].rstrip(string.ascii_letters + ' ')
try:
updated_time = round(float(updated_time))
except ValueError:
pass
output += " | Updated %s%s" % (ircutils.bold(updated_time), tailstr)
# finally, output the basic weather.
irc.reply(output)
# handle alerts - everything here and below sends as separate replies if enabled
if args['alerts'] and data['alerts']: # only look for alerts if enabled and present.
outdata['alerts'] = data['alerts'][0]['message'] # need to do some formatting below.
outdata['alerts'] = outdata['alerts'].replace('\n', ' ')
outdata['alerts'] = utils.str.normalizeWhitespace(outdata['alerts']) # fix pesky double whitespacing.
irc.reply("{0} {1}".format(self._bu("Alerts:"), outdata['alerts']))
# handle almanac
if args['almanac']:
try:
outdata['highyear'] = data['almanac']['temp_high'].get('recordyear')
outdata['lowyear'] = data['almanac']['temp_low'].get('recordyear')
outdata['highaverage'] = self._temp(channel, data['almanac']['temp_high']['normal']['F'])
outdata['lowaverage'] = self._temp(channel, data['almanac']['temp_low']['normal']['F'])
if outdata['highyear'] != "NA" and outdata['lowyear'] != "NA":
outdata['highrecord'] = self._temp(channel, data['almanac']['temp_high']['record']['F'])
outdata['lowrecord'] = self._temp(channel, data['almanac']['temp_low']['record']['F'])
else:
outdata['highrecord'] = outdata['lowrecord'] = "NA"
except KeyError:
output = "%s Not available." % self._bu('Almanac:')
else:
output = ("{0} Average High: {1} (Record: {2} in {3}) | Average Low: {4} (Record: {5} in {6})".format(
self._bu('Almanac:'), outdata['highaverage'], outdata['highrecord'], outdata['highyear'],
outdata['lowaverage'], outdata['lowrecord'], outdata['lowyear']))
irc.reply(output)
# handle astronomy
if args['astronomy']:
sunriseh = data['moon_phase']['sunrise']['hour']
sunrisem = data['moon_phase']['sunrise']['minute']
sunseth = data['moon_phase']['sunset']['hour']
sunsetm = data['moon_phase']['sunset']['minute']
sunrise = "{0}:{1}".format(sunriseh, sunrisem)
sunset = "{0}:{1}".format(sunseth, sunsetm)
# Oh god, this one-liner... -GLolol
lengthofday = "%dh%dm" % divmod((((int(sunseth)-int(sunriseh))+float((int(sunsetm)-int(sunrisem))/60.0))*60 ),60)
astronomy = {'Moon illum:': str(data['moon_phase']['percentIlluminated']) + "%",
'Moon age:': str(data['moon_phase']['ageOfMoon']) + "d",
'Sunrise:': sunrise,
'Sunset:': sunset,
'Length of Day:': lengthofday}
output = [format('%s %s', self._bold(k), v) for k, v in sorted(astronomy.items())]
output = format("%s %s", self._bu('Astronomy:'), " | ".join(output))
irc.reply(output)
# handle forecast
if args['forecast']:
fullforecastdata = {} # key = day (int), value = dict of forecast data.
for forecastday in data['forecast']['simpleforecast']['forecastday']:
high = self._temp(channel, forecastday['high']['fahrenheit'])
low = self._temp(channel, forecastday['low']['fahrenheit'])
tmpdict = {'day': forecastday['date']['weekday_short'],
'text': forecastday['conditions'],
'low': low,
'high': high}
fullforecastdata[int(forecastday['period'])] = tmpdict
outforecast = [] # prep string for output.
for (k, v) in fullforecastdata.items(): # iterate through forecast data.
outforecast.append("{0}: {1} (High: {2} Low: {3})".format(self._bold(v['day']),
v['text'], v['high'], v['low']))
output = "{0} {1}".format(self._bu('Forecast:'), " | ".join(outforecast))
irc.reply(output)
@wrap(['text'])
def locationsearch(self, irc, msg, args, text):
"""<location>
Returns a list of raw Wunderground (ZMW) codes given the search query <location>. This can be
helpful if Wunderground's autocomplete is not picking up the right place, as you can directly
look up weather using any ZMW codes returned here.
Warning: ZMW codes are not fixed and are prone to sudden changes!
"""
apikey = self.registryValue('apiKey')
if not apikey:
irc.error("No Wunderground API key was defined; set 'config plugins.Weather.apiKey'.",
Raise=True)
results = self._wuac(text, return_names=True)
if not results:
irc.error("No results found.")
else:
irc.reply(format('%L', ('\x02{0}\x02: {1}'.format(*result) for result in results)))
Class = Weather
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=250:

View File

@ -1 +0,0 @@
git+https://github.com/ProgVal/Limnoria.git

View File

@ -1,66 +0,0 @@
###
# Copyright (c) 2012-2014, spline
# Copyright (c) 2018, James Lu <james@overdrivenetworks.com>
# Permission is hereby granted, free of charge, to any person obtaining a copy of
# this software and associated documentation files (the "Software"), to deal in
# the Software without restriction, including without limitation the rights to
# use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
# the Software, and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
# IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
###
from supybot.test import *
import os
class WeatherTestCase(PluginTestCase):
plugins = ('Weather',)
def setUp(self):
PluginTestCase.setUp(self)
apiKey = os.environ.get('weather_apikey')
if not apiKey:
e = """The Wunderground API key has not been set.
please set this value correctly via the environment variable
"weather_apikey"."""
raise Exception(e)
conf.supybot.plugins.Weather.apiKey.setValue(apiKey)
def testWeatherBasic(self):
self.assertRegexp('weather New York City', 'New York, NY')
self.assertError('weather InvalidLocationTestCasePleaseIgnore')
def testWeatherUSZIPCode(self):
self.assertRegexp('weather 10002', 'New York, NY')
def testWeatherAmbiguous(self):
# Returns Albany, NY last time I checked (2017-01-28)
self.assertRegexp('weather New York', ', NY')
# Alturas, CA (2017-01-28)
self.assertRegexp('weather california', ', CA')
# I'll be very upset if this returns the wrong one ;)
self.assertRegexp('weather Vancouver', 'Vancouver, British Columbia')
def testWeatherAirport(self):
# IATA codes (e.g. YVR, PEK, LAX for these 3) are unreliable and
# sometimes clash with other places
self.assertRegexp('weather CYVR', 'Vancouver International')
self.assertRegexp('weather ZBAA', 'Beijing Capital')
self.assertRegexp('weather KLAX', 'Los Angeles International')
def testWeatherSavesLocation(self):
self.assertNotError('setweather 10002')
self.assertNotError('setuser metric True')
self.assertRegexp('weather', 'New York, NY')
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79: