initial v2 format implementation (fixes #332)

This commit is contained in:
Igor Chubin 2019-08-25 20:39:11 +02:00
parent 0f4dbd2e09
commit 74d005c0cc
5 changed files with 590 additions and 10 deletions

559
lib/spark.py Normal file
View file

@ -0,0 +1,559 @@
# vim: fileencoding=utf-8
# vim: foldmethod=marker foldenable:
"""
[X] emoji
[ ] wego icon
[ ] v2.wttr.in
[X] astronomical (sunset)
[X] time
[X] frames
[X] colorize rain data
[ ] date + locales
[X] wind color
[ ] highlight current date
[ ] bind to real site
[ ] max values: temperature
[X] max value: rain
[ ] comment github
[ ] commit
"""
import sys
import re
import math
import json
import datetime
import StringIO
import requests
import diagram
import pyjq
import pytz
import numpy as np
from astral import Astral, Location
from scipy.interpolate import interp1d
from babel.dates import format_datetime
from globals import WWO_KEY
import constants
import translations
import wttr_line
reload(sys)
sys.setdefaultencoding("utf-8")
# data processing {{{
def get_data(config):
"""
Fetch data for `query_string`
"""
url = (
'http://'
'localhost:5001/premium/v1/weather.ashx'
'?key=%s'
'&q=%s&format=json&num_of_days=3&tp=3&lang=None'
) % (WWO_KEY, config["location"])
text = requests.get(url).text
parsed_data = json.loads(text)
return parsed_data
def interpolate_data(input_data, max_width):
"""
Resample `input_data` to number of `max_width` counts
"""
x = list(range(len(input_data)))
y = input_data
xvals = np.linspace(0, len(input_data)-1, max_width)
yinterp = interp1d(x, y, kind='cubic')
return yinterp(xvals)
def jq_query(query, data_parsed):
"""
Apply `query` to structued data `data_parsed`
"""
pyjq_data = pyjq.all(query, data_parsed)
data = map(float, pyjq_data)
return data
# }}}
# utils {{{
def colorize(string, color_code):
return "\033[%sm%s\033[0m" % (color_code, string)
# }}}
# draw_spark {{{
def draw_spark(data, height, width, color_data):
"""
Spark-style visualize `data` in a region `height` x `width`
"""
_BARS = u' _▁▂▃▄▅▇█'
def _box(height, row, value, max_value):
row_height = 1.0 * max_value / height
if row_height * row >= value:
return _BARS[0]
if row_height * (row+1) <= value:
return _BARS[-1]
return _BARS[int(1.0*(value - row_height*row)/(row_height*1.0)*len(_BARS))]
max_value = max(data)
output = ""
color_code = 20
for i in range(height):
for j in range(width):
character = _box(height, height-i-1, data[j], max_value)
if data[j] != 0:
chance_of_rain = color_data[j]/100.0 * 2
if chance_of_rain > 1:
chance_of_rain = 1
color_index = int(5*chance_of_rain)
color_code = 16 + color_index # int(math.floor((20-16) * 1.0 * (height-1-i)/height*(max_value/data[j])))
output += "\033[38;5;%sm%s\033[0m" % (color_code, character)
output += "\n"
# labeling max value
if max_value == 0:
max_line = " "*width
else:
max_line = ""
for j in range(width):
if data[j] == max_value:
max_line = "%3.2fmm|%s%%" % (max_value, int(color_data[j]))
orig_max_line = max_line
# aligning it
if len(max_line)/2 < j and len(max_line)/2 + j < width:
spaces = " "*(j - len(max_line)/2)
max_line = spaces + max_line # + spaces
max_line = max_line + " "*(width - len(max_line))
elif len(max_line)/2 + j >= width:
max_line = " "*(width - len(max_line)) + max_line
max_line = max_line.replace(orig_max_line, colorize(orig_max_line, "38;5;33"))
break
if max_line:
output = "\n" + max_line + "\n" + output + "\n"
return output
# }}}
# draw_diagram {{{
def draw_diagram(data, height, width):
option = diagram.DOption()
option.size = diagram.Point([width, height])
option.mode = 'g'
stream = StringIO.StringIO()
gram = diagram.DGWrapper(
data=[list(data), range(len(data))],
dg_option=option,
ostream=stream)
gram.show()
return stream.getvalue()
# }}}
# draw_date {{{
def draw_date(config, geo_data):
"""
"""
tzinfo = pytz.timezone(geo_data["timezone"])
locale = config.get("locale", "en_US")
datetime_day_start = datetime.datetime.utcnow()
answer = ""
for day in range(3):
datetime_ = datetime_day_start + datetime.timedelta(hours=24*day)
date = format_datetime(datetime_, "EEE dd MMM", locale=locale, tzinfo=tzinfo)
spaces = ((24-len(date))/2)*" "
date = spaces + date + spaces
date = " "*(24-len(date)) + date
answer += date
answer += "\n"
for _ in range(3):
answer += " "*23 + u""
return answer[:-1] + " "
# }}}
# draw_time {{{
def draw_time(geo_data):
"""
"""
tzinfo = pytz.timezone(geo_data["timezone"])
line = ["", ""]
for _ in range(3):
part = u""*5 + u"" + u""*5
line[0] += part + u"" + part + u""
line[0] += "\n"
for _ in range(3):
line[1] += " 6 12 18 "
line[1] += "\n"
# highlight current time
hour_number = \
(datetime.datetime.now(tzinfo)
- datetime.datetime.now(tzinfo).replace(hour=0, minute=0, second=0, microsecond=0)
).seconds//3600
for line_number, _ in enumerate(line):
line[line_number] = \
line[line_number][:hour_number] \
+ colorize(line[line_number][hour_number], "46") \
+ line[line_number][hour_number+1:]
return "".join(line)
# }}}
# draw_astronomical {{{
def draw_astronomical(city_name, geo_data):
datetime_day_start = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
a = Astral()
a.solar_depression = 'civil'
city = Location()
city.latitude = geo_data["latitude"]
city.longitude = geo_data["longitude"]
city.timezone = geo_data["timezone"]
answer = ""
moon_line = ""
for time_interval in range(72):
current_date = (
datetime_day_start
+ datetime.timedelta(hours=1*time_interval)).replace(tzinfo=pytz.timezone(geo_data["timezone"]))
sun = city.sun(date=current_date, local=False)
dawn = sun['dawn'] # .replace(tzinfo=None)
dusk = sun['dusk'] # .replace(tzinfo=None)
sunrise = sun['sunrise'] # .replace(tzinfo=None)
sunset = sun['sunset'] # .replace(tzinfo=None)
if current_date < dawn:
char = " "
elif current_date > dusk:
char = " "
elif dawn < current_date and current_date < sunrise:
char = u""
elif sunset < current_date and current_date < dusk:
char = u""
elif sunrise < current_date and current_date < sunset:
char = u""
answer += char
# moon
if time_interval % 3 == 0:
moon_phase = city.moon_phase(
date=datetime_day_start + datetime.timedelta(hours=time_interval))
moon_phase_emoji = constants.MOON_PHASES[int(math.floor(moon_phase*1.0/28.0*8))]
if time_interval in [0, 24, 48, 69]:
moon_line += moon_phase_emoji + " "
else:
moon_line += " "
answer = moon_line + "\n" + answer + "\n"
answer += "\n"
return answer
# }}}
# draw_emoji {{{
def draw_emoji(data):
answer = ""
for i in data:
emoji = constants.WEATHER_SYMBOL.get(
constants.WWO_CODE.get(
str(int(i)), "Unknown"))
space = " "*(3-constants.WEATHER_SYMBOL_WIDTH_VTE.get(emoji))
answer += emoji + space
answer += "\n"
return answer
# }}}
# draw_wind {{{
def draw_wind(data, color_data):
def _color_code_for_wind_speed(wind_speed):
color_codes = [
(3, 82), # 82
(6, 118), # 118
(9, 154), # 154
(12, 250), # 190
(15, 246), # 226
(19, 253), # 220
(23, 214),
(27, 208),
(31, 202),
(-1, 196)
]
color_codes = [
(3, 241), # 82
(6, 242), # 118
(9, 243), # 154
(12, 246), # 190
(15, 250), # 226
(19, 253), # 220
(23, 214),
(27, 208),
(31, 202),
(-1, 196)
]
for this_wind_speed, this_color_code in color_codes:
if wind_speed <= this_wind_speed:
return this_color_code
return color_codes[-1][1]
answer = ""
answer_line2 = ""
for j, degree in enumerate(data):
degree = int(degree)
if degree:
wind_direction = constants.WIND_DIRECTION[((degree+22)%360)/45]
else:
wind_direction = ""
color_code = "38;5;%s" % _color_code_for_wind_speed(int(color_data[j]))
answer += " %s " % colorize(wind_direction, color_code)
# wind_speed
wind_speed = int(color_data[j])
wind_speed_str = colorize(str(wind_speed), color_code)
if wind_speed < 10:
wind_speed_str = " " + wind_speed_str + " "
elif wind_speed < 100:
wind_speed_str = " " + wind_speed_str
answer_line2 += wind_speed_str
answer += "\n"
answer += answer_line2 + "\n"
return answer
# }}}
# panel implementation {{{
def add_frame(output, width, config):
"""
Add frame arond `output` that has width `width`
"""
empty_line = " "*width
output = "\n".join(u""+(x or empty_line)+u"" for x in output.splitlines()) + "\n"
weather_report = \
translations.CAPTION[config["lang"]] \
+ " " \
+ (config["override_location"] or config["location"])
caption = u"" + " " + weather_report + " " + u""
output = u"" + caption + u""*(width-len(caption)) + u"\n" \
+ output + \
u"" + u""*width + u"\n"
return output
def generate_panel(data_parsed, geo_data, config):
"""
"""
max_width = 72
precip_mm_query = "[.data.weather[] | .hourly[]] | .[].precipMM"
precip_chance_query = "[.data.weather[] | .hourly[]] | .[].chanceofrain"
feels_like_query = "[.data.weather[] | .hourly[]] | .[].FeelsLikeC"
weather_code_query = "[.data.weather[] | .hourly[]] | .[].weatherCode"
wind_direction_query = "[.data.weather[] | .hourly[]] | .[].winddirDegree"
wind_speed_query = "[.data.weather[] | .hourly[]] | .[].windspeedKmph"
output = ""
output += "\n\n"
output += draw_date(config, geo_data)
output += "\n"
output += "\n"
output += "\n"
data = jq_query(feels_like_query, data_parsed)
data_interpolated = interpolate_data(data, max_width)
output += draw_diagram(data_interpolated, 10, max_width)
output += "\n"
output += draw_time(geo_data)
data = jq_query(precip_mm_query, data_parsed)
color_data = jq_query(precip_chance_query, data_parsed)
data_interpolated = interpolate_data(data, max_width)
color_data_interpolated = interpolate_data(color_data, max_width)
output += draw_spark(data_interpolated, 5, max_width, color_data_interpolated)
output += "\n"
data = jq_query(weather_code_query, data_parsed)
output += draw_emoji(data)
data = jq_query(wind_direction_query, data_parsed)
color_data = jq_query(wind_speed_query, data_parsed)
output += draw_wind(data, color_data)
output += "\n"
output += draw_astronomical(config["location"], geo_data)
output += "\n"
output = add_frame(output, max_width, config)
return output
# }}}
# textual information {{{
def textual_information(data_parsed, geo_data, config):
"""
Add textual information about current weather and
astronomical conditions
"""
def _shorten_full_location(full_location, city_only=False):
def _count_runes(string):
return len(string.encode('utf-16-le')) // 2
words = full_location.split(",")
output = words[0]
if city_only:
return output
for word in words[1:]:
if _count_runes(output + "," + word) > 50:
return output
output += "," + word
return output
city = Location()
city.latitude = geo_data["latitude"]
city.longitude = geo_data["longitude"]
city.timezone = geo_data["timezone"]
output = []
timezone = city.timezone
datetime_day_start = datetime.datetime.now()\
.replace(hour=0, minute=0, second=0, microsecond=0)
sun = city.sun(date=datetime_day_start, local=True)
format_line = "%c %C, %t, %h, %w, %P"
current_condition = data_parsed['data']['current_condition'][0]
query = {}
weather_line = wttr_line.render_line(format_line, current_condition, query)
output.append('Weather: %s' % weather_line)
output.append('Timezone: %s' % timezone)
tmp_output = []
tmp_output.append(' Now: %s'
% datetime.datetime.now(pytz.timezone(timezone)).strftime("%H:%M:%S%z"))
tmp_output.append('Dawn: %s'
% str(sun['dawn'].strftime("%H:%M:%S")))
tmp_output.append('Sunrise: %s'
% str(sun['sunrise'].strftime("%H:%M:%S")))
tmp_output.append(' Noon: %s'
% str(sun['noon'].strftime("%H:%M:%S ")))
tmp_output.append('Sunset: %s'
% str(sun['sunset'].strftime("%H:%M:%S")))
tmp_output.append('Dusk: %s'
% str(sun['dusk'].strftime("%H:%M:%S")))
tmp_output = [
re.sub("^([A-Za-z]*:)", lambda m: colorize(m.group(1), "2"), x)
for x in tmp_output]
output.append(
"%20s" % tmp_output[0] \
+ " | %20s " % tmp_output[1] \
+ " | %20s" % tmp_output[2])
output.append(
"%20s" % tmp_output[3] \
+ " | %20s " % tmp_output[4] \
+ " | %20s" % tmp_output[5])
city_only = False
suffix = ""
if "Simferopol" in timezone:
city_only = True
suffix = ", Крым"
if config["full_address"]:
output.append('Location: %s%s [%5.4f,%5.4f]' \
% (
_shorten_full_location(config["full_address"], city_only=city_only),
suffix,
geo_data["latitude"],
geo_data["longitude"],
))
output = [
re.sub("^( *[A-Za-z]*:)", lambda m: colorize(m.group(1), "2"),
re.sub("^( +[A-Za-z]*:)", lambda m: colorize(m.group(1), "2"),
re.sub(r"(\|)", lambda m: colorize(m.group(1), "2"), x)))
for x in output]
return "".join("%s\n" % x for x in output)
# }}}
# get_geodata {{{
def get_geodata(location):
text = requests.get("http://localhost:8004/%s" % location).text
return json.loads(text)
# }}}
def main(location, override_location=None, data=None, full_address=None):
config = {
"lang": "en",
"locale": "en_US",
"location": location,
"override_location": override_location,
"full_address": full_address,
}
geo_data = get_geodata(location)
if data is None:
data_parsed = get_data(config)
else:
data_parsed = data
output = generate_panel(data_parsed, geo_data, config)
output += textual_information(data_parsed, geo_data, config)
return output
if __name__ == '__main__':
sys.stdout.write(main(sys.argv[1]))

View file

@ -16,7 +16,7 @@ def get_weather_data(location, lang):
'&num_of_days=3&tp=3&lang=%s') % (key, location, lang)
url = 'http://127.0.0.1:5001' + url
response = requests.get(url, timeout=1)
response = requests.get(url, timeout=10)
try:
data = json.loads(response.content)
except ValueError:

View file

@ -16,9 +16,11 @@ Initial implementation of one-line output mode.
import sys
import re
import datetime
import json
from astral import Astral, Location
from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION
from weather_data import get_weather_data
import spark
PRECONFIGURED_FORMAT = {
'1': u'%c %t',
@ -229,26 +231,36 @@ def format_weather_data(format_line, location, override_location, full_address,
if 'data' not in data:
return 'Unknown location; please try ~%s' % location
if format_line == "j1":
return render_json(data['data'])
if format_line == "v2":
return spark.main(location,
override_location=override_location,
full_address=full_address, data=data)
current_condition = data['data']['current_condition'][0]
current_condition['location'] = location
current_condition['override_location'] = override_location
output = render_line(format_line, current_condition, query)
return output
def wttr_line(location, override_location_name, query, lang):
def wttr_line(location, override_location_name, full_address, query, lang, fmt):
"""
Return 1line weather information for `location`
in format `line_format`
"""
format_line = query.get('format', '')
format_line = query.get('format', fmt or '')
if format_line in PRECONFIGURED_FORMAT:
format_line = PRECONFIGURED_FORMAT[format_line]
weather_data = get_weather_data(location, lang)
output = format_weather_data(format_line, location, override_location_name, weather_data, query)
output = format_weather_data(
format_line, location, override_location_name, full_address,
weather_data, query)
output = output.rstrip("\n")+"\n"
return output
@ -262,7 +274,7 @@ def main():
'line': sys.argv[2],
}
sys.stdout.write(wttr_line(location, location, query, 'en'))
sys.stdout.write(wttr_line(location, location, None, query, 'en', "v1"))
if __name__ == '__main__':
main()

View file

@ -114,16 +114,20 @@ def _parse_language_header(header):
return _find_supported_language(_parse_accept_language(header))
def get_answer_language(request):
def get_answer_language_and_format(request):
"""
Return preferred answer language based on
domain name, query arguments and headers
"""
lang = None
fmt = None
hostname = request.headers['Host']
if hostname != 'wttr.in' and hostname.endswith('.wttr.in'):
lang = hostname[:-8]
if lang == "v2":
fmt = "v2"
lang = None
if 'lang' in request.args:
lang = request.args.get('lang')
@ -132,7 +136,7 @@ def get_answer_language(request):
if lang is None and header_accept_language:
lang = _parse_language_header(header_accept_language)
return lang
return lang, fmt
def get_output_format(request, query):
"""
@ -202,7 +206,7 @@ def wttr(location, request):
png_filename = location
location = location[:-4]
lang = get_answer_language(request)
lang, fmt = get_answer_language_and_format(request)
query = parse_query.parse_query(request.args)
html_output = get_output_format(request, query)
user_agent = request.headers.get('User-Agent', '').lower()
@ -237,8 +241,10 @@ def wttr(location, request):
# We are ready to return the answer
try:
if 'format' in query:
return _wrap_response(wttr_line(location, override_location_name, query, lang), html_output)
if fmt or 'format' in query:
return _wrap_response(
wttr_line(location, override_location_name, full_address, query, lang, fmt),
html_output)
if png_filename:
options = {

View file

@ -11,3 +11,6 @@ timezonefinder
Pillow
pyte
python-dateutil
diagram
pyjq
scipy