mirror of
https://github.com/jlu5/SupyPlugins.git
synced 2025-04-26 04:51:08 -05:00
NuWeather: split formatting another module; implement format_temp with only c input
This commit is contained in:
parent
608a056ad7
commit
cb49620a2d
6
.pylintrc
Normal file
6
.pylintrc
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[FORMAT]
|
||||||
|
max-line-length=120
|
||||||
|
good-names=ip,f,i,e,s
|
||||||
|
|
||||||
|
[MESSAGES CONTROL]
|
||||||
|
disable=missing-class-docstring,missing-function-docstring,invalid-name
|
@ -54,14 +54,11 @@ __contributors__ = {}
|
|||||||
# This is a url where the most recent plugin package can be downloaded.
|
# This is a url where the most recent plugin package can be downloaded.
|
||||||
__url__ = 'https://github.com/jlu5/SupyPlugins/tree/master/NuWeather'
|
__url__ = 'https://github.com/jlu5/SupyPlugins/tree/master/NuWeather'
|
||||||
|
|
||||||
from . import config
|
from . import config, formatter, plugin
|
||||||
from . import plugin
|
from importlib import reload
|
||||||
if sys.version_info >= (3, 4):
|
|
||||||
from importlib import reload
|
|
||||||
else:
|
|
||||||
from imp import reload
|
|
||||||
# In case we're being reloaded.
|
# In case we're being reloaded.
|
||||||
reload(config)
|
reload(config)
|
||||||
|
reload(formatter)
|
||||||
reload(plugin)
|
reload(plugin)
|
||||||
|
|
||||||
from .local import accountsdb
|
from .local import accountsdb
|
||||||
|
250
NuWeather/formatter.py
Normal file
250
NuWeather/formatter.py
Normal file
@ -0,0 +1,250 @@
|
|||||||
|
###
|
||||||
|
# Copyright (c) 2011-2014, Valentin Lorentz
|
||||||
|
# Copyright (c) 2018-2022, James Lu <james@overdrivenetworks.com>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
#
|
||||||
|
# * Redistributions of source code must retain the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright notice,
|
||||||
|
# this list of conditions, and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of the author of this software nor the name of
|
||||||
|
# contributors to this software may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written consent.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||||
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
||||||
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
||||||
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||||
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||||
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||||
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||||
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||||
|
# POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
###
|
||||||
|
|
||||||
|
import re
|
||||||
|
import string
|
||||||
|
|
||||||
|
from supybot import callbacks, conf, ircutils, log, utils
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pendulum
|
||||||
|
except ImportError:
|
||||||
|
pendulum = None
|
||||||
|
log.warning('NuWeather: pendulum is not installed; extended forecasts will not be formatted properly')
|
||||||
|
|
||||||
|
try:
|
||||||
|
from supybot.i18n import PluginInternationalization
|
||||||
|
_ = PluginInternationalization('NuWeather')
|
||||||
|
except ImportError:
|
||||||
|
_ = lambda x: x
|
||||||
|
|
||||||
|
from .config import DEFAULT_FORMAT, DEFAULT_FORECAST_FORMAT, DEFAULT_FORMAT_CURRENTONLY
|
||||||
|
|
||||||
|
_channel_context = None
|
||||||
|
# dummy fallback for testing
|
||||||
|
_registryValue = lambda *args, **kwargs: ''
|
||||||
|
|
||||||
|
# Based off https://github.com/ProgVal/Supybot-plugins/blob/master/GitHub/plugin.py
|
||||||
|
def flatten_subdicts(dicts, flat=None):
|
||||||
|
"""Flattens a dict containing dicts or lists of dicts. Useful for string formatting."""
|
||||||
|
if flat is None:
|
||||||
|
# Instanciate the dictionnary when the function is run and now when it
|
||||||
|
# is declared; otherwise the same dictionnary instance will be kept and
|
||||||
|
# it will have side effects (memory exhaustion, ...)
|
||||||
|
flat = {}
|
||||||
|
if isinstance(dicts, list):
|
||||||
|
return flatten_subdicts(dict(enumerate(dicts)))
|
||||||
|
elif isinstance(dicts, dict):
|
||||||
|
for key, value in dicts.items():
|
||||||
|
if isinstance(value, dict):
|
||||||
|
value = dict(flatten_subdicts(value))
|
||||||
|
for subkey, subvalue in value.items():
|
||||||
|
flat['%s__%s' % (key, subkey)] = subvalue
|
||||||
|
elif isinstance(value, list):
|
||||||
|
for num, subvalue in enumerate(value):
|
||||||
|
if isinstance(subvalue, dict):
|
||||||
|
for subkey, subvalue in subvalue.items():
|
||||||
|
flat['%s__%s__%s' % (key, num, subkey)] = subvalue
|
||||||
|
else:
|
||||||
|
flat['%s__%s' % (key, num)] = subvalue
|
||||||
|
else:
|
||||||
|
flat[key] = value
|
||||||
|
return flat
|
||||||
|
else:
|
||||||
|
return dicts
|
||||||
|
|
||||||
|
def format_temp(f=None, c=None):
|
||||||
|
"""
|
||||||
|
Colorizes temperatures and formats them to show either Fahrenheit, Celsius, or both.
|
||||||
|
"""
|
||||||
|
if f is None and c is None:
|
||||||
|
return _('N/A')
|
||||||
|
if f is None:
|
||||||
|
f = c * 9/5 + 32
|
||||||
|
elif c is None:
|
||||||
|
c = (f - 32) * 5/9
|
||||||
|
|
||||||
|
f = float(f)
|
||||||
|
if f < 10:
|
||||||
|
color = 'light blue'
|
||||||
|
elif f < 32:
|
||||||
|
color = 'teal'
|
||||||
|
elif f < 50:
|
||||||
|
color = 'blue'
|
||||||
|
elif f < 60:
|
||||||
|
color = 'light green'
|
||||||
|
elif f < 70:
|
||||||
|
color = 'green'
|
||||||
|
elif f < 80:
|
||||||
|
color = 'yellow'
|
||||||
|
elif f < 90:
|
||||||
|
color = 'orange'
|
||||||
|
else:
|
||||||
|
color = 'red'
|
||||||
|
# Show temp values to one decimal place
|
||||||
|
c = '%.1f' % c
|
||||||
|
f = '%.1f' % f
|
||||||
|
|
||||||
|
displaymode = _registryValue('units.temperature', channel=_channel_context)
|
||||||
|
if displaymode == 'F/C':
|
||||||
|
s = '%sF/%sC' % (f, c)
|
||||||
|
elif displaymode == 'C/F':
|
||||||
|
s = '%sC/%sF' % (c, f)
|
||||||
|
elif displaymode == 'F':
|
||||||
|
s = '%sF' % f
|
||||||
|
elif displaymode == 'C':
|
||||||
|
s = '%sC' % c
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown display mode for temperature.")
|
||||||
|
return ircutils.mircColor(s, color)
|
||||||
|
|
||||||
|
_TEMPERATURES_RE = re.compile(r'((\d+)°?F)') # Only need FtoC conversion so far
|
||||||
|
def mangle_temperatures(forecast):
|
||||||
|
"""Runs _format_temp() on temperature values embedded within forecast strings."""
|
||||||
|
if not forecast:
|
||||||
|
return forecast
|
||||||
|
for (text, value) in set(_TEMPERATURES_RE.findall(forecast)):
|
||||||
|
forecast = forecast.replace(text, format_temp(f=value))
|
||||||
|
return forecast
|
||||||
|
|
||||||
|
def wind_direction(angle):
|
||||||
|
"""Returns wind direction (N, W, S, E, etc.) given an angle."""
|
||||||
|
# Adapted from https://stackoverflow.com/a/7490772
|
||||||
|
directions = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW')
|
||||||
|
if angle is None:
|
||||||
|
return directions[0] # dummy output
|
||||||
|
angle = int(angle)
|
||||||
|
idx = int((angle/(360/len(directions)))+.5)
|
||||||
|
return directions[idx % len(directions)]
|
||||||
|
|
||||||
|
def format_uv(uv):
|
||||||
|
"""Formats UV levels with IRC colouring"""
|
||||||
|
if uv is None:
|
||||||
|
return _('N/A')
|
||||||
|
# From https://en.wikipedia.org/wiki/Ultraviolet_index#Index_usage 2018-12-30
|
||||||
|
uv = float(uv)
|
||||||
|
if uv <= 2.9:
|
||||||
|
color, risk = 'green', 'Low'
|
||||||
|
elif uv <= 5.9:
|
||||||
|
color, risk = 'yellow', 'Moderate'
|
||||||
|
elif uv <= 7.9:
|
||||||
|
color, risk = 'orange', 'High'
|
||||||
|
elif uv <= 10.9:
|
||||||
|
color, risk = 'red', 'Very high'
|
||||||
|
else:
|
||||||
|
# Closest we have to violet
|
||||||
|
color, risk = 'pink', 'Extreme'
|
||||||
|
s = '%d (%s)' % (uv, risk)
|
||||||
|
return ircutils.mircColor(s, color)
|
||||||
|
|
||||||
|
def format_precip(mm=None, inches=None):
|
||||||
|
"""Formats precipitation to mm/in format"""
|
||||||
|
if mm is None and inches is None:
|
||||||
|
return _('N/A')
|
||||||
|
elif mm == 0 or inches == 0:
|
||||||
|
return '0' # Don't bother with 2 units if the value is 0
|
||||||
|
|
||||||
|
if mm is None:
|
||||||
|
mm = round(inches * 25.4, 1)
|
||||||
|
elif inches is None:
|
||||||
|
inches = round(mm / 25.4, 1)
|
||||||
|
|
||||||
|
return _('%smm/%sin') % (mm, inches)
|
||||||
|
|
||||||
|
def format_distance(mi=None, km=None, speed=False):
|
||||||
|
"""Formats distance or speed values in miles and kilometers"""
|
||||||
|
if mi is None and km is None:
|
||||||
|
return _('N/A')
|
||||||
|
elif mi == 0 or km == 0:
|
||||||
|
return '0' # Don't bother with 2 units if the value is 0
|
||||||
|
|
||||||
|
if mi is None:
|
||||||
|
mi = round(km / 1.609, 1)
|
||||||
|
elif km is None:
|
||||||
|
km = round(mi * 1.609, 1)
|
||||||
|
|
||||||
|
if speed:
|
||||||
|
return _('%smph/%skph') % (mi, km)
|
||||||
|
else:
|
||||||
|
return _('%smi/%skm') % (mi, km)
|
||||||
|
|
||||||
|
def format_percentage(value):
|
||||||
|
"""
|
||||||
|
Formats percentage values given either as an int (value%) or float (0 <= value <= 1).
|
||||||
|
"""
|
||||||
|
if isinstance(value, float):
|
||||||
|
return '%.0f%%' % (value * 100)
|
||||||
|
elif isinstance(value, int):
|
||||||
|
return '%d%%' % value
|
||||||
|
else:
|
||||||
|
return 'N/A'
|
||||||
|
|
||||||
|
def get_dayname(ts, idx, *, tz=None):
|
||||||
|
"""
|
||||||
|
Returns the day name given a Unix timestamp, day index and (optionally) a timezone.
|
||||||
|
"""
|
||||||
|
if pendulum is not None:
|
||||||
|
p = pendulum.from_timestamp(ts, tz=tz)
|
||||||
|
return p.format('dddd')
|
||||||
|
else:
|
||||||
|
# Fallback
|
||||||
|
if idx == 0:
|
||||||
|
return 'Today'
|
||||||
|
elif idx == 1:
|
||||||
|
return 'Tomorrow'
|
||||||
|
else:
|
||||||
|
return 'Day_%d' % idx
|
||||||
|
|
||||||
|
def format_weather(data, forecast=False):
|
||||||
|
"""
|
||||||
|
Formats and returns current conditions.
|
||||||
|
"""
|
||||||
|
# Work around IRC length limits for config opts...
|
||||||
|
data['c'] = data['current']
|
||||||
|
data['f'] = data.get('forecast')
|
||||||
|
|
||||||
|
flat_data = flatten_subdicts(data)
|
||||||
|
if flat_data.get('url'):
|
||||||
|
flat_data['url'] = utils.str.url(flat_data['url'])
|
||||||
|
|
||||||
|
forecast_available = bool(data.get('forecast'))
|
||||||
|
if forecast: # --forecast option was given
|
||||||
|
if forecast_available:
|
||||||
|
fmt = _registryValue('outputFormat.forecast', channel=_channel_context) or DEFAULT_FORECAST_FORMAT
|
||||||
|
else:
|
||||||
|
raise callbacks.Error(_("Extended forecast info is not available from this backend."))
|
||||||
|
else:
|
||||||
|
if forecast_available:
|
||||||
|
fmt = _registryValue('outputFormat', channel=_channel_context) or DEFAULT_FORMAT
|
||||||
|
else:
|
||||||
|
fmt = _registryValue('outputFormat.currentOnly', channel=_channel_context) or DEFAULT_FORMAT_CURRENTONLY
|
||||||
|
template = string.Template(fmt)
|
||||||
|
|
||||||
|
return template.safe_substitute(flat_data)
|
@ -1,6 +1,5 @@
|
|||||||
###
|
###
|
||||||
# Copyright (c) 2011-2014, Valentin Lorentz
|
# Copyright (c) 2018-2022, James Lu <james@overdrivenetworks.com>
|
||||||
# Copyright (c) 2018-2020, James Lu <james@overdrivenetworks.com>
|
|
||||||
# All rights reserved.
|
# All rights reserved.
|
||||||
#
|
#
|
||||||
# Redistribution and use in source and binary forms, with or without
|
# Redistribution and use in source and binary forms, with or without
|
||||||
@ -29,8 +28,6 @@
|
|||||||
###
|
###
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import string
|
|
||||||
|
|
||||||
from supybot import utils, plugins, ircutils, callbacks, world, conf, log
|
from supybot import utils, plugins, ircutils, callbacks, world, conf, log
|
||||||
from supybot.commands import *
|
from supybot.commands import *
|
||||||
@ -42,48 +39,14 @@ except ImportError:
|
|||||||
# without the i18n module
|
# without the i18n module
|
||||||
_ = lambda x: x
|
_ = lambda x: x
|
||||||
|
|
||||||
try:
|
from .config import BACKENDS, GEOCODE_BACKENDS
|
||||||
import pendulum
|
|
||||||
except ImportError:
|
|
||||||
pendulum = None
|
|
||||||
log.warning('NuWeather: pendulum is not installed; extended forecasts will not be formatted properly')
|
|
||||||
|
|
||||||
from .config import BACKENDS, GEOCODE_BACKENDS, DEFAULT_FORMAT, DEFAULT_FORECAST_FORMAT, DEFAULT_FORMAT_CURRENTONLY
|
|
||||||
from .local import accountsdb
|
from .local import accountsdb
|
||||||
|
from . import formatter
|
||||||
|
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
'User-agent': 'Mozilla/5.0 (compatible; Supybot/Limnoria %s; NuWeather weather plugin)' % conf.version
|
'User-agent': 'Mozilla/5.0 (compatible; Supybot/Limnoria %s; NuWeather weather plugin)' % conf.version
|
||||||
}
|
}
|
||||||
|
|
||||||
# Based off https://github.com/ProgVal/Supybot-plugins/blob/master/GitHub/plugin.py
|
|
||||||
def flatten_subdicts(dicts, flat=None):
|
|
||||||
"""Flattens a dict containing dicts or lists of dicts. Useful for string formatting."""
|
|
||||||
if flat is None:
|
|
||||||
# Instanciate the dictionnary when the function is run and now when it
|
|
||||||
# is declared; otherwise the same dictionnary instance will be kept and
|
|
||||||
# it will have side effects (memory exhaustion, ...)
|
|
||||||
flat = {}
|
|
||||||
if isinstance(dicts, list):
|
|
||||||
return flatten_subdicts(dict(enumerate(dicts)))
|
|
||||||
elif isinstance(dicts, dict):
|
|
||||||
for key, value in dicts.items():
|
|
||||||
if isinstance(value, dict):
|
|
||||||
value = dict(flatten_subdicts(value))
|
|
||||||
for subkey, subvalue in value.items():
|
|
||||||
flat['%s__%s' % (key, subkey)] = subvalue
|
|
||||||
elif isinstance(value, list):
|
|
||||||
for num, subvalue in enumerate(value):
|
|
||||||
if isinstance(subvalue, dict):
|
|
||||||
for subkey, subvalue in subvalue.items():
|
|
||||||
flat['%s__%s__%s' % (key, num, subkey)] = subvalue
|
|
||||||
else:
|
|
||||||
flat['%s__%s' % (key, num)] = subvalue
|
|
||||||
else:
|
|
||||||
flat[key] = value
|
|
||||||
return flat
|
|
||||||
else:
|
|
||||||
return dicts
|
|
||||||
|
|
||||||
class NuWeather(callbacks.Plugin):
|
class NuWeather(callbacks.Plugin):
|
||||||
"""Weather plugin for Limnoria"""
|
"""Weather plugin for Limnoria"""
|
||||||
threaded = True
|
threaded = True
|
||||||
@ -101,7 +64,8 @@ class NuWeather(callbacks.Plugin):
|
|||||||
self.geocode_db = {}
|
self.geocode_db = {}
|
||||||
world.flushers.append(self.db.flush)
|
world.flushers.append(self.db.flush)
|
||||||
world.flushers.append(self._flush_geocode_db)
|
world.flushers.append(self._flush_geocode_db)
|
||||||
self._channel_context = None
|
# this is hacky but less annoying than navigating the registry ourselves
|
||||||
|
formatter._registryValue = self.registryValue
|
||||||
|
|
||||||
def _flush_geocode_db(self):
|
def _flush_geocode_db(self):
|
||||||
geocode_db_filename = conf.supybot.directories.data.dirize("NuWeather-geocode.json")
|
geocode_db_filename = conf.supybot.directories.data.dirize("NuWeather-geocode.json")
|
||||||
@ -115,150 +79,6 @@ class NuWeather(callbacks.Plugin):
|
|||||||
self._flush_geocode_db()
|
self._flush_geocode_db()
|
||||||
super().die()
|
super().die()
|
||||||
|
|
||||||
def _format_temp(self, f, c=None):
|
|
||||||
"""
|
|
||||||
Colorizes temperatures and formats them to show either Fahrenheit, Celsius, or both.
|
|
||||||
"""
|
|
||||||
if f is None:
|
|
||||||
return _('N/A')
|
|
||||||
|
|
||||||
f = float(f)
|
|
||||||
if f < 10:
|
|
||||||
color = 'light blue'
|
|
||||||
elif f < 32:
|
|
||||||
color = 'teal'
|
|
||||||
elif f < 50:
|
|
||||||
color = 'blue'
|
|
||||||
elif f < 60:
|
|
||||||
color = 'light green'
|
|
||||||
elif f < 70:
|
|
||||||
color = 'green'
|
|
||||||
elif f < 80:
|
|
||||||
color = 'yellow'
|
|
||||||
elif f < 90:
|
|
||||||
color = 'orange'
|
|
||||||
else:
|
|
||||||
color = 'red'
|
|
||||||
# Round to nearest tenth for display purposes
|
|
||||||
if c is None:
|
|
||||||
c = round((f - 32) * 5/9, 1)
|
|
||||||
else:
|
|
||||||
c = round(c, 1)
|
|
||||||
f = round(f, 1)
|
|
||||||
|
|
||||||
displaymode = self.registryValue('units.temperature', channel=self._channel_context)
|
|
||||||
if displaymode == 'F/C':
|
|
||||||
string = '%sF/%sC' % (f, c)
|
|
||||||
elif displaymode == 'C/F':
|
|
||||||
string = '%sC/%sF' % (c, f)
|
|
||||||
elif displaymode == 'F':
|
|
||||||
string = '%sF' % f
|
|
||||||
elif displaymode == 'C':
|
|
||||||
string = '%sC' % c
|
|
||||||
else:
|
|
||||||
raise ValueError("Unknown display mode for temperature.")
|
|
||||||
return ircutils.mircColor(string, color)
|
|
||||||
|
|
||||||
_temperatures_re = re.compile(r'((\d+)°?F)') # Only need FtoC conversion so far
|
|
||||||
def _mangle_temperatures(self, forecast):
|
|
||||||
"""Runs _format_temp() on temperature values embedded within forecast strings."""
|
|
||||||
if not forecast:
|
|
||||||
return forecast
|
|
||||||
for (text, value) in set(self._temperatures_re.findall(forecast)):
|
|
||||||
forecast = forecast.replace(text, self._format_temp(f=value))
|
|
||||||
return forecast
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _wind_direction(angle):
|
|
||||||
"""Returns wind direction (N, W, S, E, etc.) given an angle."""
|
|
||||||
# Adapted from https://stackoverflow.com/a/7490772
|
|
||||||
directions = ('N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW')
|
|
||||||
if angle is None:
|
|
||||||
return directions[0] # dummy output
|
|
||||||
angle = int(angle)
|
|
||||||
idx = int((angle/(360/len(directions)))+.5)
|
|
||||||
return directions[idx % len(directions)]
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_uv(uv):
|
|
||||||
if uv is None:
|
|
||||||
return _('N/A')
|
|
||||||
# From https://en.wikipedia.org/wiki/Ultraviolet_index#Index_usage 2018-12-30
|
|
||||||
uv = float(uv)
|
|
||||||
if uv <= 2.9:
|
|
||||||
color, risk = 'green', 'Low'
|
|
||||||
elif uv <= 5.9:
|
|
||||||
color, risk = 'yellow', 'Moderate'
|
|
||||||
elif uv <= 7.9:
|
|
||||||
color, risk = 'orange', 'High'
|
|
||||||
elif uv <= 10.9:
|
|
||||||
color, risk = 'red', 'Very high'
|
|
||||||
else:
|
|
||||||
# Closest we have to violet
|
|
||||||
color, risk = 'pink', 'Extreme'
|
|
||||||
string = '%d (%s)' % (uv, risk)
|
|
||||||
return ircutils.mircColor(string, color)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_precip(mm=None, inches=None):
|
|
||||||
if mm is None and inches is None:
|
|
||||||
return _('N/A')
|
|
||||||
elif mm == 0 or inches == 0:
|
|
||||||
return '0' # Don't bother with 2 units if the value is 0
|
|
||||||
|
|
||||||
if mm is None:
|
|
||||||
mm = round(inches * 25.4, 1)
|
|
||||||
elif inches is None:
|
|
||||||
inches = round(mm / 25.4, 1)
|
|
||||||
|
|
||||||
return _('%smm/%sin') % (mm, inches)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_distance(mi=None, km=None, speed=False):
|
|
||||||
if mi is None and km is None:
|
|
||||||
return _('N/A')
|
|
||||||
elif mi == 0 or km == 0:
|
|
||||||
return '0' # Don't bother with 2 units if the value is 0
|
|
||||||
|
|
||||||
if mi is None:
|
|
||||||
mi = round(km / 1.609, 1)
|
|
||||||
elif km is None:
|
|
||||||
km = round(mi * 1.609, 1)
|
|
||||||
|
|
||||||
if speed:
|
|
||||||
return _('%smph/%skph') % (mi, km)
|
|
||||||
else:
|
|
||||||
return _('%smi/%skm') % (mi, km)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _format_percentage(value):
|
|
||||||
"""
|
|
||||||
Formats percentage values given either as an int (value%) or float (0 <= value <= 1).
|
|
||||||
"""
|
|
||||||
if isinstance(value, float):
|
|
||||||
return '%.0f%%' % (value * 100)
|
|
||||||
elif isinstance(value, int):
|
|
||||||
return '%d%%' % value
|
|
||||||
else:
|
|
||||||
return 'N/A'
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _get_dayname(ts, idx, *, tz=None):
|
|
||||||
"""
|
|
||||||
Returns the day name given a Unix timestamp, day index and (optionally) a timezone.
|
|
||||||
"""
|
|
||||||
if pendulum is not None:
|
|
||||||
p = pendulum.from_timestamp(ts, tz=tz)
|
|
||||||
return p.format('dddd')
|
|
||||||
else:
|
|
||||||
# Fallback
|
|
||||||
if idx == 0:
|
|
||||||
return 'Today'
|
|
||||||
elif idx == 1:
|
|
||||||
return 'Tomorrow'
|
|
||||||
else:
|
|
||||||
return 'Day_%d' % idx
|
|
||||||
|
|
||||||
def _nominatim_geocode(self, location):
|
def _nominatim_geocode(self, location):
|
||||||
location = location.lower()
|
location = location.lower()
|
||||||
|
|
||||||
@ -368,7 +188,7 @@ class NuWeather(callbacks.Plugin):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def _geocode(self, location, geobackend=None):
|
def _geocode(self, location, geobackend=None):
|
||||||
geocode_backend = geobackend or self.registryValue('geocodeBackend', channel=self._channel_context)
|
geocode_backend = geobackend or self.registryValue('geocodeBackend', channel=formatter._channel_context)
|
||||||
if geocode_backend not in GEOCODE_BACKENDS:
|
if geocode_backend not in GEOCODE_BACKENDS:
|
||||||
raise callbacks.Error(_("Unknown geocode backend %r. Valid ones are: %s") % (geocode_backend, ', '.join(GEOCODE_BACKENDS)))
|
raise callbacks.Error(_("Unknown geocode backend %r. Valid ones are: %s") % (geocode_backend, ', '.join(GEOCODE_BACKENDS)))
|
||||||
|
|
||||||
@ -387,33 +207,6 @@ class NuWeather(callbacks.Plugin):
|
|||||||
self.geocode_db[result_pair] = result # Cache result persistently
|
self.geocode_db[result_pair] = result # Cache result persistently
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _format(self, data, forecast=False):
|
|
||||||
"""
|
|
||||||
Formats and returns current conditions.
|
|
||||||
"""
|
|
||||||
# Work around IRC length limits for config opts...
|
|
||||||
data['c'] = data['current']
|
|
||||||
data['f'] = data.get('forecast')
|
|
||||||
|
|
||||||
flat_data = flatten_subdicts(data)
|
|
||||||
if flat_data.get('url'):
|
|
||||||
flat_data['url'] = utils.str.url(flat_data['url'])
|
|
||||||
|
|
||||||
forecast_available = bool(data.get('forecast'))
|
|
||||||
if forecast: # --forecast option was given
|
|
||||||
if forecast_available:
|
|
||||||
fmt = self.registryValue('outputFormat.forecast', channel=self._channel_context) or DEFAULT_FORECAST_FORMAT
|
|
||||||
else:
|
|
||||||
raise callbacks.Error(_("Extended forecast info is not available from this backend."))
|
|
||||||
else:
|
|
||||||
if forecast_available:
|
|
||||||
fmt = self.registryValue('outputFormat', channel=self._channel_context) or DEFAULT_FORMAT
|
|
||||||
else:
|
|
||||||
fmt = self.registryValue('outputFormat.currentOnly', channel=self._channel_context) or DEFAULT_FORMAT_CURRENTONLY
|
|
||||||
template = string.Template(fmt)
|
|
||||||
|
|
||||||
return template.safe_substitute(flat_data)
|
|
||||||
|
|
||||||
def _weatherstack_fetcher(self, location, geobackend=None):
|
def _weatherstack_fetcher(self, location, geobackend=None):
|
||||||
"""Grabs weather data from weatherstack (formerly Apixu)."""
|
"""Grabs weather data from weatherstack (formerly Apixu)."""
|
||||||
apikey = self.registryValue('apikeys.weatherstack')
|
apikey = self.registryValue('apikeys.weatherstack')
|
||||||
@ -439,14 +232,14 @@ class NuWeather(callbacks.Plugin):
|
|||||||
'url': '',
|
'url': '',
|
||||||
'current': {
|
'current': {
|
||||||
'condition': currentdata['weather_descriptions'][0],
|
'condition': currentdata['weather_descriptions'][0],
|
||||||
'temperature': self._format_temp(f=currentdata['temperature']),
|
'temperature': formatter.format_temp(f=currentdata['temperature']),
|
||||||
'feels_like': self._format_temp(f=currentdata['feelslike']),
|
'feels_like': formatter.format_temp(f=currentdata['feelslike']),
|
||||||
'humidity': self._format_percentage(currentdata['humidity']),
|
'humidity': formatter.format_percentage(currentdata['humidity']),
|
||||||
'precip': self._format_precip(inches=currentdata['precip']),
|
'precip': formatter.format_precip(inches=currentdata['precip']),
|
||||||
'wind': self._format_distance(mi=currentdata['wind_speed'], speed=True),
|
'wind': formatter.format_distance(mi=currentdata['wind_speed'], speed=True),
|
||||||
'wind_dir': currentdata['wind_dir'],
|
'wind_dir': currentdata['wind_dir'],
|
||||||
'uv': self._format_uv(currentdata['uv_index']),
|
'uv': formatter.format_uv(currentdata['uv_index']),
|
||||||
'visibility': self._format_distance(mi=currentdata.get('visibility')),
|
'visibility': formatter.format_distance(mi=currentdata.get('visibility')),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -479,19 +272,19 @@ class NuWeather(callbacks.Plugin):
|
|||||||
'url': 'https://darksky.net/forecast/%s,%s' % (lat, lon),
|
'url': 'https://darksky.net/forecast/%s,%s' % (lat, lon),
|
||||||
'current': {
|
'current': {
|
||||||
'condition': currentdata.get('summary', 'N/A'),
|
'condition': currentdata.get('summary', 'N/A'),
|
||||||
'temperature': self._format_temp(f=currentdata.get('temperature')),
|
'temperature': formatter.format_temp(f=currentdata.get('temperature')),
|
||||||
'feels_like': self._format_temp(f=currentdata.get('apparentTemperature')),
|
'feels_like': formatter.format_temp(f=currentdata.get('apparentTemperature')),
|
||||||
'humidity': self._format_percentage(currentdata.get('humidity')),
|
'humidity': formatter.format_percentage(currentdata.get('humidity')),
|
||||||
'precip': self._format_precip(mm=currentdata.get('precipIntensity')),
|
'precip': formatter.format_precip(mm=currentdata.get('precipIntensity')),
|
||||||
'wind': self._format_distance(mi=currentdata.get('windSpeed', 0), speed=True),
|
'wind': formatter.format_distance(mi=currentdata.get('windSpeed', 0), speed=True),
|
||||||
'wind_gust': self._format_distance(mi=currentdata.get('windGust', 0), speed=True),
|
'wind_gust': formatter.format_distance(mi=currentdata.get('windGust', 0), speed=True),
|
||||||
'wind_dir': self._wind_direction(currentdata.get('windBearing')),
|
'wind_dir': formatter.wind_direction(currentdata.get('windBearing')),
|
||||||
'uv': self._format_uv(currentdata.get('uvIndex')),
|
'uv': formatter.format_uv(currentdata.get('uvIndex')),
|
||||||
'visibility': self._format_distance(mi=currentdata.get('visibility')),
|
'visibility': formatter.format_distance(mi=currentdata.get('visibility')),
|
||||||
},
|
},
|
||||||
'forecast': [{'dayname': self._get_dayname(forecastdata['time'], idx, tz=data['timezone']),
|
'forecast': [{'dayname': formatter.get_dayname(forecastdata['time'], idx, tz=data['timezone']),
|
||||||
'max': self._format_temp(f=forecastdata.get('temperatureHigh')),
|
'max': formatter.format_temp(f=forecastdata.get('temperatureHigh')),
|
||||||
'min': self._format_temp(f=forecastdata.get('temperatureLow')),
|
'min': formatter.format_temp(f=forecastdata.get('temperatureLow')),
|
||||||
'summary': forecastdata.get('summary', 'N/A').rstrip('.')} for idx, forecastdata in enumerate(data['daily']['data'])]
|
'summary': forecastdata.get('summary', 'N/A').rstrip('.')} for idx, forecastdata in enumerate(data['daily']['data'])]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -522,9 +315,9 @@ class NuWeather(callbacks.Plugin):
|
|||||||
|
|
||||||
# XXX: are the units for this consistent across APIs?
|
# XXX: are the units for this consistent across APIs?
|
||||||
if currentdata.get('snow'):
|
if currentdata.get('snow'):
|
||||||
precip = self._format_precip(mm=currentdata['snow']['1h'] * 10)
|
precip = formatter.format_precip(mm=currentdata['snow']['1h'] * 10)
|
||||||
elif currentdata.get('rain'):
|
elif currentdata.get('rain'):
|
||||||
precip = self._format_precip(mm=currentdata['rain']['1h'])
|
precip = formatter.format_precip(mm=currentdata['rain']['1h'])
|
||||||
else:
|
else:
|
||||||
precip = 'N/A'
|
precip = 'N/A'
|
||||||
|
|
||||||
@ -538,22 +331,22 @@ class NuWeather(callbacks.Plugin):
|
|||||||
}),
|
}),
|
||||||
'current': {
|
'current': {
|
||||||
'condition': currentdata['weather'][0]['description'],
|
'condition': currentdata['weather'][0]['description'],
|
||||||
'temperature': self._format_temp(f=currentdata['temp']),
|
'temperature': formatter.format_temp(f=currentdata['temp']),
|
||||||
'feels_like': self._format_temp(f=currentdata['feels_like']),
|
'feels_like': formatter.format_temp(f=currentdata['feels_like']),
|
||||||
'humidity': self._format_percentage(currentdata['humidity']),
|
'humidity': formatter.format_percentage(currentdata['humidity']),
|
||||||
'precip': precip,
|
'precip': precip,
|
||||||
'wind': self._format_distance(mi=currentdata['wind_speed'], speed=True),
|
'wind': formatter.format_distance(mi=currentdata['wind_speed'], speed=True),
|
||||||
'wind_dir': self._wind_direction(currentdata['wind_deg']),
|
'wind_dir': formatter.wind_direction(currentdata['wind_deg']),
|
||||||
'wind_gust': self._format_distance(mi=currentdata.get('wind_gust'), speed=True),
|
'wind_gust': formatter.format_distance(mi=currentdata.get('wind_gust'), speed=True),
|
||||||
'uv': self._format_uv(currentdata.get('uvi')),
|
'uv': formatter.format_uv(currentdata.get('uvi')),
|
||||||
'visibility': self._format_distance(km=currentdata['visibility']/1000),
|
'visibility': formatter.format_distance(km=currentdata['visibility']/1000),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
output['forecast'] = [
|
output['forecast'] = [
|
||||||
{'dayname': self._get_dayname(forecast['dt'], idx, tz=data['timezone']),
|
{'dayname': formatter.get_dayname(forecast['dt'], idx, tz=data['timezone']),
|
||||||
'max': self._format_temp(f=forecast['temp']['max']),
|
'max': formatter.format_temp(f=forecast['temp']['max']),
|
||||||
'min': self._format_temp(f=forecast['temp']['min']),
|
'min': formatter.format_temp(f=forecast['temp']['min']),
|
||||||
'summary': forecast['weather'][0]['description']}
|
'summary': forecast['weather'][0]['description']}
|
||||||
for idx, forecast in enumerate(data['daily'])
|
for idx, forecast in enumerate(data['daily'])
|
||||||
]
|
]
|
||||||
@ -592,11 +385,11 @@ class NuWeather(callbacks.Plugin):
|
|||||||
irc.error(_("Unknown weather backend %s. Valid ones are: %s") % (weather_backend, ', '.join(BACKENDS)), Raise=True)
|
irc.error(_("Unknown weather backend %s. Valid ones are: %s") % (weather_backend, ', '.join(BACKENDS)), Raise=True)
|
||||||
geocode_backend = optlist.get('geocode-backend', self.registryValue('geocodeBackend', msg.args[0]))
|
geocode_backend = optlist.get('geocode-backend', self.registryValue('geocodeBackend', msg.args[0]))
|
||||||
|
|
||||||
self._channel_context = msg.channel
|
formatter._channel_context = msg.channel
|
||||||
backend_func = getattr(self, '_%s_fetcher' % weather_backend)
|
backend_func = getattr(self, '_%s_fetcher' % weather_backend)
|
||||||
raw_data = backend_func(location, geocode_backend)
|
raw_data = backend_func(location, geocode_backend)
|
||||||
|
|
||||||
s = self._format(raw_data, forecast='forecast' in optlist)
|
s = formatter.format_weather(raw_data, forecast='forecast' in optlist)
|
||||||
irc.reply(s)
|
irc.reply(s)
|
||||||
|
|
||||||
@wrap([getopts({'user': 'nick', 'backend': None}), 'text'])
|
@wrap([getopts({'user': 'nick', 'backend': None}), 'text'])
|
||||||
|
@ -34,7 +34,6 @@ from supybot.test import *
|
|||||||
from supybot import log
|
from supybot import log
|
||||||
|
|
||||||
NO_NETWORK_REASON = "Network-based tests are disabled by --no-network"
|
NO_NETWORK_REASON = "Network-based tests are disabled by --no-network"
|
||||||
|
|
||||||
class NuWeatherTestCase():
|
class NuWeatherTestCase():
|
||||||
plugins = ('NuWeather',)
|
plugins = ('NuWeather',)
|
||||||
|
|
||||||
@ -82,6 +81,103 @@ class NuWeatherWeatherstackTestCase(NuWeatherTestCase, PluginTestCase):
|
|||||||
class NuWeatherOpenWeatherMapTestCase(NuWeatherTestCase, PluginTestCase):
|
class NuWeatherOpenWeatherMapTestCase(NuWeatherTestCase, PluginTestCase):
|
||||||
BACKEND = 'openweathermap'
|
BACKEND = 'openweathermap'
|
||||||
|
|
||||||
|
from . import formatter
|
||||||
|
|
||||||
|
class NuWeatherFormatterTestCase(unittest.TestCase):
|
||||||
|
def test_format_temp(self):
|
||||||
|
func = formatter.format_temp
|
||||||
|
self.assertEqual(func(f=50, c=10), '\x030950.0F/10.0C\x03')
|
||||||
|
self.assertEqual(func(f=100), '\x0304100.0F/37.8C\x03')
|
||||||
|
self.assertEqual(func(c=25.55), '\x030878.0F/25.6C\x03')
|
||||||
|
self.assertEqual(func(), 'N/A')
|
||||||
|
|
||||||
|
def test_format_temp_displaymode(self):
|
||||||
|
func = formatter.format_temp
|
||||||
|
with conf.supybot.plugins.NuWeather.units.temperature.context('F/C'):
|
||||||
|
self.assertEqual(func(c=-5.3), '\x031022.5F/-5.3C\x03')
|
||||||
|
with conf.supybot.plugins.NuWeather.units.temperature.context('C/F'):
|
||||||
|
self.assertEqual(func(f=50, c=10), '\x030910.0C/50.0F\x03')
|
||||||
|
with conf.supybot.plugins.NuWeather.units.temperature.context('C'):
|
||||||
|
self.assertEqual(func(c=36), '\x030436.0C\x03')
|
||||||
|
with conf.supybot.plugins.NuWeather.units.temperature.context('F'):
|
||||||
|
self.assertEqual(func(f=72), '\x030872.0F\x03')
|
||||||
|
|
||||||
|
def test_format_distance_speed(self):
|
||||||
|
func = formatter.format_distance
|
||||||
|
self.assertEqual(func(mi=123), '123mi/197.9km')
|
||||||
|
self.assertEqual(func(km=42.6), '26.5mi/42.6km')
|
||||||
|
self.assertEqual(func(mi=26, km=42), '26mi/42km')
|
||||||
|
self.assertEqual(func(mi=0), '0') # special case
|
||||||
|
self.assertEqual(func(), 'N/A')
|
||||||
|
|
||||||
|
def test_format_default(self):
|
||||||
|
data = {'location': "Narnia",
|
||||||
|
'poweredby': 'Dummy',
|
||||||
|
'url': 'http://dummy.invalid/api/',
|
||||||
|
'current': {
|
||||||
|
'condition': 'Sunny',
|
||||||
|
'temperature': formatter.format_temp(f=80),
|
||||||
|
'feels_like': formatter.format_temp(f=85),
|
||||||
|
'humidity': formatter.format_percentage(0.8),
|
||||||
|
'precip': formatter.format_precip(mm=90),
|
||||||
|
'wind': formatter.format_distance(mi=12, speed=True),
|
||||||
|
'wind_gust': formatter.format_distance(mi=20, speed=True),
|
||||||
|
'wind_dir': formatter.wind_direction(15),
|
||||||
|
'uv': formatter.format_uv(6),
|
||||||
|
'visibility': formatter.format_distance(mi=1000),
|
||||||
|
},
|
||||||
|
'forecast': [{'dayname': 'Today',
|
||||||
|
'max': formatter.format_temp(f=100),
|
||||||
|
'min': formatter.format_temp(f=60),
|
||||||
|
'summary': 'Cloudy'},
|
||||||
|
{'dayname': 'Tomorrow',
|
||||||
|
'max': formatter.format_temp(f=70),
|
||||||
|
'min': formatter.format_temp(f=55),
|
||||||
|
'summary': 'Light rain'}]}
|
||||||
|
self.assertEqual(formatter.format_weather(data),
|
||||||
|
'\x02Narnia\x02 :: Sunny \x030780.0F/26.7C\x03 (Humidity: 80%) | '
|
||||||
|
'\x02Feels like:\x02 \x030785.0F/29.4C\x03 | '
|
||||||
|
'\x02Wind\x02: 12mph/19.3kph NNE | '
|
||||||
|
'\x02Wind gust\x02: 20mph/32.2kph | '
|
||||||
|
'\x02Today\x02: Cloudy. High \x0304100.0F/37.8C\x03. Low \x030360.0F/15.6C\x03. | '
|
||||||
|
'\x02Tomorrow\x02: Light rain. High \x030870.0F/21.1C\x03. Low \x030955.0F/12.8C\x03. | '
|
||||||
|
'Powered by \x02Dummy\x02 <http://dummy.invalid/api/>')
|
||||||
|
#print(repr(formatter.format_weather(data)))
|
||||||
|
|
||||||
|
def test_format_forecast(self):
|
||||||
|
data = {'location': "Testville",
|
||||||
|
'poweredby': 'Dummy',
|
||||||
|
'url': 'http://dummy.invalid/api/',
|
||||||
|
'current': {
|
||||||
|
'condition': 'Sunny',
|
||||||
|
'temperature': formatter.format_temp(f=80),
|
||||||
|
'feels_like': formatter.format_temp(f=85),
|
||||||
|
'humidity': formatter.format_percentage(0.8),
|
||||||
|
'precip': formatter.format_precip(mm=90),
|
||||||
|
'wind': formatter.format_distance(mi=12, speed=True),
|
||||||
|
'wind_gust': formatter.format_distance(mi=20, speed=True),
|
||||||
|
'wind_dir': formatter.wind_direction(15),
|
||||||
|
'uv': formatter.format_uv(6),
|
||||||
|
'visibility': formatter.format_distance(mi=1000),
|
||||||
|
},
|
||||||
|
'forecast': [{'dayname': 'Today',
|
||||||
|
'max': formatter.format_temp(f=100),
|
||||||
|
'min': formatter.format_temp(f=60),
|
||||||
|
'summary': 'Cloudy'},
|
||||||
|
{'dayname': 'Tomorrow',
|
||||||
|
'max': formatter.format_temp(f=70),
|
||||||
|
'min': formatter.format_temp(f=55),
|
||||||
|
'summary': 'Light rain'},
|
||||||
|
{'dayname': 'Tomorrow',
|
||||||
|
'max': formatter.format_temp(f=56),
|
||||||
|
'min': formatter.format_temp(f=40),
|
||||||
|
'summary': 'Heavy rain'}]}
|
||||||
|
self.assertIn('\x02Testville\x02 :: \x02Today\x02: Cloudy (\x030360.0F/15.6C\x03 to \x0304100.0F/37.8C\x03) | '
|
||||||
|
'\x02Tomorrow\x02: Light rain (\x030955.0F/12.8C\x03 to \x030870.0F/21.1C\x03) | '
|
||||||
|
'\x02Tomorrow\x02: Heavy rain (\x030240.0F/4.4C\x03 to \x030956.0F/13.3C\x03)',
|
||||||
|
formatter.format_weather(data, True))
|
||||||
|
#print(repr(formatter.format_weather(data, True)))
|
||||||
|
|
||||||
# FIXME: test geocode backends
|
# FIXME: test geocode backends
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user