From 7dd42abe8d6b6051e285e32f47bf4473f70d1611 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 3 Apr 2020 20:27:00 +0200 Subject: [PATCH 01/73] added lib/extract_emoji.py --- lib/extract_emoji.py | 62 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 lib/extract_emoji.py diff --git a/lib/extract_emoji.py b/lib/extract_emoji.py new file mode 100644 index 0000000..8c42675 --- /dev/null +++ b/lib/extract_emoji.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python +#vim: fileencoding=utf-8 + +""" + +At the moment, Pillow library does not support colorful emojis, +that is why emojis must be extracted to external files first, +and then they must be handled as usual graphical objects +and not as text. + +The files are extracted using Imagemagick. + +Usage: + + ve/bi/python lib/extract_emoji.py +""" + +import subprocess + +EMOJIS = [ + "✨", + "☁️", + "🌫", + "🌧", + "🌧", + "❄️", + "❄️", + "🌦", + "🌦", + "🌧", + "🌧", + "🌨", + "🌨", + "⛅️", + "☀️", + "🌩", + "⛈", + "⛈", + "☁️", + "🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘" +] + +def extract_emojis_to_directory(dirname): + """ + Extract emoji from an emoji font, to separate files. + """ + + emoji_font = "Noto Color Emoji" + emoji_size = 30 + + for emoji in EMOJIS: + filename = "%s/%s.png" % (dirname, emoji) + convert_string = [ + "convert", "-background", "black", "-size", "%sx%s" % (emoji_size, emoji_size), + "-set", "colorspace", "sRGB", + "pango:%s" % (emoji_font, emoji), + filename + ] + subprocess.Popen(convert_string) + +if __name__ == '__main__': + extract_emojis_to_directory("share/emoji") From f9a1ff5b2a760b1864693b2059fdff3ae951febd Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 3 Apr 2020 20:30:03 +0200 Subject: [PATCH 02/73] requirements.txt: added several new deps --- requirements.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 69744ec..9c847e4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,14 +8,17 @@ pylint cyrtranslit astral timezonefinder==2.1.2 -Pillow pyte python-dateutil diagram pyjq scipy +numpy +pillow babel pylru pysocks supervisor numba +emojis +grapheme From ac1be8305b9a17a39a8deaf28c2a49a5445babc1 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 3 Apr 2020 20:58:51 +0200 Subject: [PATCH 03/73] lib/location.py: python3 fixes --- lib/location.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/location.py b/lib/location.py index f931092..f47e833 100644 --- a/lib/location.py +++ b/lib/location.py @@ -9,6 +9,7 @@ precise location description. """ from __future__ import print_function +import sys import os import json import socket @@ -35,12 +36,15 @@ def is_ip(ip_addr): Check if `ip_addr` looks like an IP Address """ + if sys.version_info[0] < 3: + ip_addr = ip_addr.encode("utf-8") + try: - socket.inet_pton(socket.AF_INET, ip_addr.encode("utf-8")) + socket.inet_pton(socket.AF_INET, ip_addr) return True except socket.error: try: - socket.inet_pton(socket.AF_INET6, ip_addr.encode("utf-8")) + socket.inet_pton(socket.AF_INET6, ip_addr) return True except socket.error: return False From b98d919d3c4623abb9bf69da8cd63ebd46159ea6 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 3 Apr 2020 21:49:40 +0200 Subject: [PATCH 04/73] switched to new astral version --- lib/spark.py | 33 +++++++++++++-------------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/lib/spark.py b/lib/spark.py index de71527..511ebf6 100644 --- a/lib/spark.py +++ b/lib/spark.py @@ -26,20 +26,16 @@ import re import math import json import datetime -try: - import StringIO -except: - import io as StringIO +import io import requests import diagram import pyjq import pytz import numpy as np -try: - from astral import Astral, Location -except ImportError: - pass +from astral import LocationInfo +from astral import moon +from astral.sun import sun from scipy.interpolate import interp1d from babel.dates import format_datetime @@ -241,10 +237,7 @@ def draw_time(geo_data): 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 = LocationInfo() city.latitude = geo_data["latitude"] city.longitude = geo_data["longitude"] city.timezone = geo_data["timezone"] @@ -256,12 +249,12 @@ def draw_astronomical(city_name, geo_data): 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) + current_sun = sun(city.observer, date=current_date) - dawn = sun['dawn'] # .replace(tzinfo=None) - dusk = sun['dusk'] # .replace(tzinfo=None) - sunrise = sun['sunrise'] # .replace(tzinfo=None) - sunset = sun['sunset'] # .replace(tzinfo=None) + dawn = current_sun['dawn'] # .replace(tzinfo=None) + dusk = current_sun['dusk'] # .replace(tzinfo=None) + sunrise = current_sun['sunrise'] # .replace(tzinfo=None) + sunset = current_sun['sunset'] # .replace(tzinfo=None) if current_date < dawn: char = " " @@ -278,7 +271,7 @@ def draw_astronomical(city_name, geo_data): # moon if time_interval % 3 == 0: - moon_phase = city.moon_phase( + moon_phase = 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+0.5)) % len(constants.MOON_PHASES)] if time_interval in [0, 24, 48, 69]: @@ -454,7 +447,7 @@ def textual_information(data_parsed, geo_data, config): return output - city = Location() + city = LocationInfo() city.latitude = geo_data["latitude"] city.longitude = geo_data["longitude"] city.timezone = geo_data["timezone"] @@ -464,7 +457,7 @@ def textual_information(data_parsed, geo_data, config): datetime_day_start = datetime.datetime.now()\ .replace(hour=0, minute=0, second=0, microsecond=0) - sun = city.sun(date=datetime_day_start, local=True) + current_sun = sun(city.observer, date=datetime_day_start) format_line = "%c %C, %t, %h, %w, %P" current_condition = data_parsed['data']['current_condition'][0] From 88340abec242151002a0cb415706221d89225853 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 3 Apr 2020 21:49:51 +0200 Subject: [PATCH 05/73] switched to python3 --- lib/spark.py | 32 +++++++++++++++++--------------- lib/wttr.py | 33 +++++++++++++++++++++------------ lib/wttr_line.py | 2 +- lib/wttr_srv.py | 11 ++++++----- 4 files changed, 45 insertions(+), 33 deletions(-) diff --git a/lib/spark.py b/lib/spark.py index 511ebf6..edd943b 100644 --- a/lib/spark.py +++ b/lib/spark.py @@ -70,9 +70,11 @@ def interpolate_data(input_data, max_width): Resample `input_data` to number of `max_width` counts """ - x = list(range(len(input_data))) + input_data = list(input_data) + input_data_len = len(input_data) + x = list(range(input_data_len)) y = input_data - xvals = np.linspace(0, len(input_data)-1, max_width) + xvals = np.linspace(0, input_data_len-1, max_width) yinterp = interp1d(x, y, kind='cubic') return yinterp(xvals) @@ -82,7 +84,7 @@ def jq_query(query, data_parsed): """ pyjq_data = pyjq.all(query, data_parsed) - data = map(float, pyjq_data) + data = list(map(float, pyjq_data)) return data # }}} @@ -136,11 +138,11 @@ def draw_spark(data, height, width, color_data): 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) + 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: + 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")) @@ -160,13 +162,13 @@ def draw_diagram(data, height, width): option.size = diagram.Point([width, height]) option.mode = 'g' - stream = StringIO.StringIO() + stream = io.BytesIO() gram = diagram.DGWrapper( data=[list(data), range(len(data))], dg_option=option, ostream=stream) gram.show() - return stream.getvalue() + return stream.getvalue().decode("utf-8") # }}} # draw_date {{{ @@ -185,7 +187,7 @@ def draw_date(config, geo_data): 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)*" " + spaces = ((24-len(date))//2)*" " date = spaces + date + spaces date = " "*(24-len(date)) + date answer += date @@ -326,7 +328,7 @@ def draw_wind(data, color_data): degree = int(degree) if degree: - wind_direction = constants.WIND_DIRECTION[((degree+22)%360)/45] + wind_direction = constants.WIND_DIRECTION[((degree+22)%360)//45] else: wind_direction = "" @@ -470,15 +472,15 @@ def textual_information(data_parsed, geo_data, config): tmp_output = [] tmp_output.append(' Now: %%{{NOW(%s)}}' % timezone) tmp_output.append('Dawn: %s' - % str(sun['dawn'].strftime("%H:%M:%S"))) + % str(current_sun['dawn'].strftime("%H:%M:%S"))) tmp_output.append('Sunrise: %s' - % str(sun['sunrise'].strftime("%H:%M:%S"))) + % str(current_sun['sunrise'].strftime("%H:%M:%S"))) tmp_output.append(' Zenith: %s' - % str(sun['noon'].strftime("%H:%M:%S "))) + % str(current_sun['noon'].strftime("%H:%M:%S "))) tmp_output.append('Sunset: %s' - % str(sun['sunset'].strftime("%H:%M:%S"))) + % str(current_sun['sunset'].strftime("%H:%M:%S"))) tmp_output.append('Dusk: %s' - % str(sun['dusk'].strftime("%H:%M:%S"))) + % str(current_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] diff --git a/lib/wttr.py b/lib/wttr.py index b1724cd..a1982dc 100644 --- a/lib/wttr.py +++ b/lib/wttr.py @@ -8,6 +8,7 @@ from gevent.monkey import patch_all from gevent.subprocess import Popen, PIPE, STDOUT patch_all() +import sys import os import re import time @@ -36,12 +37,12 @@ def get_wetter(location, ip, html=False, lang=None, query=None, location_name=No if local_url is None: url = "" else: - url = local_url.encode('utf-8') + url = local_url if local_location is None: location = "" else: - location = local_location.encode('utf-8') + location = local_location pic_url = url.replace('?', '_') @@ -120,9 +121,12 @@ def get_wetter(location, ip, html=False, lang=None, query=None, location_name=No p = Popen(cmd, stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate() + stdout = stdout.decode("utf-8") + stderr = stderr.decode("utf-8") + if p.returncode != 0: print("ERROR: location not found: %s" % location) - if 'Unable to find any matching weather location to the query submitted' in stderr: + if u'Unable to find any matching weather location to the query submitted' in stderr: if location != NOT_FOUND_LOCATION: NOT_FOUND_MESSAGE_HEADER = u"ERROR: %s: %s\n---\n\n" % (get_message('UNKNOWN_LOCATION', lang), location) location = NOT_FOUND_LOCATION @@ -133,10 +137,10 @@ def get_wetter(location, ip, html=False, lang=None, query=None, location_name=No dirname = os.path.dirname(filename) if not os.path.exists(dirname): os.makedirs(dirname) - + if location_not_found: - stdout += get_message('NOT_FOUND_MESSAGE', lang).encode('utf-8') - stdout = NOT_FOUND_MESSAGE_HEADER.encode('utf-8') + stdout + stdout += get_message('NOT_FOUND_MESSAGE', lang) + stdout = NOT_FOUND_MESSAGE_HEADER + stdout if 'days' in query: if query['days'] == '0': @@ -146,7 +150,7 @@ def get_wetter(location, ip, html=False, lang=None, query=None, location_name=No if query['days'] == '2': stdout = "\n".join(stdout.splitlines()[:27]) + "\n" - first = stdout.splitlines()[0].decode('utf-8') + first = stdout.splitlines()[0] rest = stdout.splitlines()[1:] if query.get('no-caption', False): @@ -158,7 +162,7 @@ def get_wetter(location, ip, html=False, lang=None, query=None, location_name=No if separator: first = first.split(separator,1)[1] - stdout = "\n".join([first.strip().encode('utf-8')] + rest) + "\n" + stdout = "\n".join([first.strip()] + rest) + "\n" if query.get('no-terminal', False): stdout = remove_ansi(stdout) @@ -172,9 +176,9 @@ def get_wetter(location, ip, html=False, lang=None, query=None, location_name=No and not query.get('no-caption') and not query.get('days') == '0'): line = "%s: %s [%s]\n" % ( - get_message('LOCATION', lang).encode('utf-8'), - full_address.encode('utf-8'), - location.encode('utf-8')) + get_message('LOCATION', lang), + full_address, + location) stdout += line if query.get('padding', False): @@ -191,13 +195,15 @@ def get_wetter(location, ip, html=False, lang=None, query=None, location_name=No p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE ) stdout, stderr = p.communicate(stdout) + stdout = stdout.decode("utf-8") + stderr = stderr.decode("utf-8") if p.returncode != 0: error(stdout + stderr) if query.get('inverted_colors'): stdout = stdout.replace('', '') - title = "%s" % first.encode('utf-8') + title = "%s" % first opengraph = get_opengraph() stdout = re.sub("", "" + title + opengraph, stdout) open(filename+'.html', 'w').write(stdout) @@ -232,6 +238,7 @@ def get_moon(location, html=False, lang=None, query=None): if lang: env['LANG'] = lang p = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env) + stdout = stdout.decode("utf-8") stdout = p.communicate()[0] if query.get('no-terminal', False): @@ -240,6 +247,8 @@ def get_moon(location, html=False, lang=None, query=None): if html: p = Popen(["bash", ANSI2HTML, "--palette=solarized", "--bg=dark"], stdin=PIPE, stdout=PIPE, stderr=PIPE) stdout, stderr = p.communicate(stdout) + stdout = stdout.decode("utf-8") + stderr = stderr.decode("utf-8") if p.returncode != 0: error(stdout + stderr) diff --git a/lib/wttr_line.py b/lib/wttr_line.py index f632936..6029f4a 100644 --- a/lib/wttr_line.py +++ b/lib/wttr_line.py @@ -140,7 +140,7 @@ def render_wind(data, query): degree = "" if degree: - wind_direction = WIND_DIRECTION[((degree+22)%360)/45] + wind_direction = WIND_DIRECTION[((degree+22)%360)//45] else: wind_direction = "" diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index a55c934..802dafe 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -49,7 +49,7 @@ def show_text_file(name, lang): text = text\ .replace('NUMBER_OF_LANGUAGES', str(len(SUPPORTED_LANGS)))\ .replace('SUPPORTED_LANGUAGES', ' '.join(SUPPORTED_LANGS)) - return text.decode('utf-8') + return text def client_ip_address(request): """ @@ -238,8 +238,8 @@ def wttr(location, request): query = parse_query.metric_or_imperial(query, lang, us_ip=us_ip) # logging query - orig_location_utf8 = (orig_location or "").encode('utf-8') - location_utf8 = location.encode('utf-8') + orig_location_utf8 = (orig_location or "") + location_utf8 = location use_imperial = query.get('use_imperial', False) log(" ".join(map(str, [ip_addr, user_agent, orig_location_utf8, location_utf8, use_imperial, lang]))) @@ -297,16 +297,17 @@ def wttr(location, request): output = add_buttons(output) else: #output += '\n' + get_message('NEW_FEATURE', lang).encode('utf-8') - output += '\n' + get_message('FOLLOW_ME', lang).encode('utf-8') + '\n' + output += '\n' + get_message('FOLLOW_ME', lang) + '\n' return _wrap_response(output, html_output) except Exception as exception: # if 'Malformed response' in str(exception) \ # or 'API key has reached calls per day allowed limit' in str(exception): + logging.error("Exception has occured", exc_info=1) if html_output: return _wrap_response(MALFORMED_RESPONSE_HTML_PAGE, html_output) - return _wrap_response(get_message('CAPACITY_LIMIT_REACHED', lang).encode('utf-8'), html_output) + return _wrap_response(get_message('CAPACITY_LIMIT_REACHED', lang), html_output) # logging.error("Exception has occured", exc_info=1) # return "ERROR" From 5a6f7b317c4436f789f391f380e397d71f7fe0c3 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 3 Apr 2020 22:04:35 +0200 Subject: [PATCH 06/73] v2 fix: show localtime --- lib/spark.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/spark.py b/lib/spark.py index edd943b..a78887a 100644 --- a/lib/spark.py +++ b/lib/spark.py @@ -469,18 +469,20 @@ def textual_information(data_parsed, geo_data, config): output.append('Timezone: %s' % timezone) + local_tz = pytz.timezone(timezone) + + local_time_of = lambda x: current_sun[x]\ + .replace(tzinfo=pytz.utc)\ + .astimezone(local_tz)\ + .strftime("%H:%M:%S") + tmp_output = [] tmp_output.append(' Now: %%{{NOW(%s)}}' % timezone) - tmp_output.append('Dawn: %s' - % str(current_sun['dawn'].strftime("%H:%M:%S"))) - tmp_output.append('Sunrise: %s' - % str(current_sun['sunrise'].strftime("%H:%M:%S"))) - tmp_output.append(' Zenith: %s' - % str(current_sun['noon'].strftime("%H:%M:%S "))) - tmp_output.append('Sunset: %s' - % str(current_sun['sunset'].strftime("%H:%M:%S"))) - tmp_output.append('Dusk: %s' - % str(current_sun['dusk'].strftime("%H:%M:%S"))) + tmp_output.append('Dawn: %s' % local_time_of("dawn")) + tmp_output.append('Sunrise: %s' % local_time_of("sunrise")) + tmp_output.append(' Zenith: %s ' % local_time_of("noon")) + tmp_output.append('Sunset: %s' % local_time_of("sunset")) + tmp_output.append('Dusk: %s' % local_time_of("dusk")) tmp_output = [ re.sub("^([A-Za-z]*:)", lambda m: colorize(m.group(1), "2"), x) for x in tmp_output] From ed966407e02f3082c5f5a96085dff54d70b66d2a Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 4 Apr 2020 14:19:44 +0200 Subject: [PATCH 07/73] requirements.txt: fixed dependency name --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 9c847e4..1a0bf96 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,5 +20,5 @@ pylru pysocks supervisor numba -emojis +emoji grapheme From e29e5784da09159015b557488a408d196eda1c47 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 4 Apr 2020 14:21:14 +0200 Subject: [PATCH 08/73] moved views to view/ --- lib/view/__init__.py | 0 lib/{wttr_line.py => view/line.py} | 4 ++-- lib/{spark.py => view/v2.py} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 lib/view/__init__.py rename lib/{wttr_line.py => view/line.py} (99%) rename lib/{spark.py => view/v2.py} (99%) diff --git a/lib/view/__init__.py b/lib/view/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/wttr_line.py b/lib/view/line.py similarity index 99% rename from lib/wttr_line.py rename to lib/view/line.py index 6029f4a..1306051 100644 --- a/lib/wttr_line.py +++ b/lib/view/line.py @@ -23,7 +23,7 @@ except ImportError: pass from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION from weather_data import get_weather_data -import spark +from . import v2 PRECONFIGURED_FORMAT = { '1': u'%c %t', @@ -249,7 +249,7 @@ def format_weather_data(format_line, location, override_location, full_address, if format_line == "j1": return render_json(data['data']) if format_line[:2] == "v2": - return spark.main(location, + return v2.main(location, override_location=override_location, full_address=full_address, data=data, view=format_line) diff --git a/lib/spark.py b/lib/view/v2.py similarity index 99% rename from lib/spark.py rename to lib/view/v2.py index a78887a..acb2151 100644 --- a/lib/spark.py +++ b/lib/view/v2.py @@ -42,7 +42,7 @@ from babel.dates import format_datetime from globals import WWO_KEY import constants import translations -import wttr_line +from . import line as wttr_line if not sys.version_info >= (3, 0): reload(sys) # noqa: F821 From f363315476024fbef35093b03bc605aacff5b92d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 4 Apr 2020 14:21:56 +0200 Subject: [PATCH 09/73] moved formats to format/ --- lib/format/__init__.py | 0 lib/{wttrin_png.py => format/png.py} | 315 +++++++++++++++++---------- lib/{ => format}/unicodedata2.py | 6 + 3 files changed, 204 insertions(+), 117 deletions(-) create mode 100644 lib/format/__init__.py rename lib/{wttrin_png.py => format/png.py} (56%) rename lib/{ => format}/unicodedata2.py (99%) diff --git a/lib/format/__init__.py b/lib/format/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/lib/wttrin_png.py b/lib/format/png.py similarity index 56% rename from lib/wttrin_png.py rename to lib/format/png.py index 005a4a0..58465a3 100644 --- a/lib/wttrin_png.py +++ b/lib/format/png.py @@ -1,51 +1,59 @@ #!/usr/bin/python #vim: encoding=utf-8 +""" +This module is used to generate png-files for wttr.in queries. +The only exported function are: + +* render_ansi(png_file, text, options=None) +* make_wttr_in_png(png_file) + +`render_ansi` is the main function of the module, +which does rendering of stream into a PNG-file. + +The `make_wttr_in_png` function is a temporary helper function +which is a wraper around `render_ansi` and handles +such tasks as caching, name parsing etc. + +`make_wttr_in_png` parses `png_file` name (the shortname) and extracts +the weather query from it. It saves the weather report into the specified file. + +The module uses PIL for graphical tasks, and pyte for rendering +of ANSI stream into terminal representation. + +TODO: + + * remove make_wttr_in_png + * remove functions specific for wttr.in +""" + from __future__ import print_function + import sys import os import re import time - -""" -This module is used to generate png-files for wttr.in queries. -The only exported function is: - - make_wttr_in_png(filename) - -in filename (in the shortname) is coded the weather query. -The function saves the weather report in the file and returns None. -""" - -import requests +import glob from PIL import Image, ImageFont, ImageDraw import pyte.screens +import emoji +import grapheme -# downloaded from https://gist.github.com/2204527 -# described/recommended here: -# -# http://stackoverflow.com/questions/9868792/find-out-the-unicode-script-of-a-character -# -import unicodedata2 +import requests -MYDIR = os.path.abspath(os.path.dirname(os.path.dirname('__file__'))) -sys.path.append("%s/lib/" % MYDIR) +from . import unicodedata2 + +sys.path.insert(0, "..") +import constants import parse_query - -from globals import PNG_CACHE, log +import globals COLS = 180 ROWS = 100 - -CHAR_WIDTH = 7 -CHAR_HEIGHT = 14 - -# How to find font for non-standard scripts: -# -# $ fc-list :lang=ja - -FONT_SIZE = 12 +CHAR_WIDTH = 9 +CHAR_HEIGHT = 18 +FONT_SIZE = 15 FONT_CAT = { 'default': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 'Cyrillic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", @@ -56,30 +64,105 @@ FONT_CAT = { 'Hiragana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf", 'Katakana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf", 'Hangul': "/usr/share/fonts/truetype/lexi/LexiGulim.ttf", + 'Braille': "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf", + 'Emoji': "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf", } -def color_mapping(color): +# +# How to find font for non-standard scripts: +# +# $ fc-list :lang=ja +# +# GNU/Debian packages, that the fonts come from: +# +# * fonts-dejavu-core +# * fonts-wqy-zenhei (Han) +# * fonts-motoya-l-cedar (Hiragana/Katakana) +# * fonts-lexi-gulim (Hangul) +# * fonts-symbola (Braille/Emoji) +# + +def make_wttr_in_png(png_name, options=None): + """ The function saves the weather report in the file and returns None. + The weather query is coded in filename (in the shortname). """ - Convert pyte color to PIL color + + parsed = _parse_wttrin_png_name(png_name) + + # if location is MyLocation it should be overriden + # with autodetected location (from options) + if parsed.get('location', 'MyLocation') == 'MyLocation' or not parsed.get('location', ''): + del parsed['location'] + + if options is not None: + for key, val in options.items(): + if key not in parsed: + parsed[key] = val + url = _make_wttrin_query(parsed) + + timestamp = time.strftime("%Y%m%d%H", time.localtime()) + cached_basename = url[14:].replace('/', '_') + + cached_png_file = "%s/%s/%s.png" % (globals.PNG_CACHE, timestamp, cached_basename) + + dirname = os.path.dirname(cached_png_file) + if not os.path.exists(dirname): + os.makedirs(dirname) + + if os.path.exists(cached_png_file): + return cached_png_file + + headers = {'X-PNG-Query-For': options.get('ip_addr', '1.1.1.1')} + text = requests.get(url, headers=headers).text + + render_ansi(cached_png_file, text, options=parsed) + + return cached_png_file + +def render_ansi(png_file, text, options=None): + """Render `text` (terminal sequence) in `png_file` + paying attention to passed command line `options` """ + + screen = pyte.screens.Screen(COLS, ROWS) + screen.set_mode(pyte.modes.LNM) + stream = pyte.Stream(screen) + + text, graphemes = _fix_graphemes(text) + stream.feed(text) + + buf = sorted(screen.buffer.items(), key=lambda x: x[0]) + buf = [[x[1] for x in sorted(line[1].items(), key=lambda x: x[0])] for line in buf] + + _gen_term(png_file, buf, graphemes, options=options) + +def _color_mapping(color): + """Convert pyte color to PIL color + + Return: tuple of color values (R,G,B) + """ + if color == 'default': return 'lightgray' if color in ['green', 'black', 'cyan', 'blue', 'brown']: return color try: - return (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16)) - except: + return ( + int(color[0:2], 16), + int(color[2:4], 16), + int(color[4:6], 16)) + except (ValueError, IndexError): # if we do not know this color and it can not be decoded as RGB, # print it and return it as it is (will be displayed as black) # print color return color return color -def strip_buf(buf): - """ - Strips empty spaces from behind and from the right side. +def _strip_buf(buf): + """Strips empty spaces from behind and from the right side. (from the right side is not yet implemented) """ + def empty_line(line): "Returns True if the line consists from spaces" return all(x.data == ' ' for x in line) @@ -106,22 +189,47 @@ def strip_buf(buf): return buf -def script_category(char): - """ - Returns category of a Unicode character +def _script_category(char): + """Returns category of a Unicode character + Possible values: default, Cyrillic, Greek, Han, Hiragana """ + + if char in emoji.UNICODE_EMOJI: + return "Emoji" + cat = unicodedata2.script_cat(char)[0] if char == u':': return 'Han' if cat in ['Latin', 'Common']: return 'default' - else: - return cat + return cat -def gen_term(filename, buf, options=None): - buf = strip_buf(buf) +def _load_emojilib(): + """Load known emojis from a directory, and return dictionary + of PIL Image objects correspodent to the loaded emojis. + Each emoji is resized to the CHAR_HEIGHT size. + """ + + emojilib = {} + for filename in glob.glob("share/emoji/*.png"): + character = os.path.basename(filename)[:-4] + emojilib[character] = \ + Image.open(filename).resize((CHAR_HEIGHT, CHAR_HEIGHT)) + return emojilib + +def _gen_term(filename, buf, graphemes, options=None): + """Renders rendered pyte buffer `buf` and list of workaround `graphemes` + to a PNG file `filename`. + """ + + if not options: + options = {} + + current_grapheme = 0 + + buf = _strip_buf(buf) cols = max(len(x) for x in buf) rows = len(buf) @@ -134,30 +242,40 @@ def gen_term(filename, buf, options=None): for cat in FONT_CAT: font[cat] = ImageFont.truetype(FONT_CAT[cat], FONT_SIZE) + emojilib = _load_emojilib() + x_pos = 0 y_pos = 0 for line in buf: x_pos = 0 for char in line: - current_color = color_mapping(char.fg) + current_color = _color_mapping(char.fg) if char.bg != 'default': draw.rectangle( ((x_pos, y_pos), (x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)), - fill=color_mapping(char.bg)) + fill=_color_mapping(char.bg)) - if char.data: - cat = script_category(char.data) + if char.data == "!": + data = graphemes[current_grapheme] + current_grapheme += 1 + else: + data = char.data + + if data: + cat = _script_category(data[0]) if cat not in font: - log("Unknown font category: %s" % cat) - draw.text( - (x_pos, y_pos), - char.data, - font=font.get(cat, font.get('default')), - fill=current_color) - #sys.stdout.write(c.data) + globals.log("Unknown font category: %s" % cat) + if cat == 'Emoji' and emojilib.get(data): + image.paste(emojilib.get(data), (x_pos, y_pos)) + else: + draw.text( + (x_pos, y_pos), + data, + font=font.get(cat, font.get('default')), + fill=current_color) - x_pos += CHAR_WIDTH + x_pos += CHAR_WIDTH * constants.WEATHER_SYMBOL_WIDTH_VTE.get(data, 1) y_pos += CHAR_HEIGHT #sys.stdout.write('\n') @@ -165,8 +283,8 @@ def gen_term(filename, buf, options=None): transparency = options.get('transparency', '255') try: transparency = int(transparency) - except: - transparceny = 255 + except ValueError: + transparency = 255 if transparency < 0: transparency = 0 @@ -187,32 +305,36 @@ def gen_term(filename, buf, options=None): image.save(filename) -def typescript_to_one_frame(png_file, text, options=None): +def _fix_graphemes(text): """ - Render text (terminal sequence) in png_file + Extract long graphemes sequences that can't be handled + by pyte correctly because of the bug pyte#131. + Graphemes are omited and replaced with placeholders, + and returned as a list. + + Return: + text_without_graphemes, graphemes """ - # fixing some broken characters because of bug #... in pyte 6.0 - text = text.replace('Н', 'H').replace('Ν', 'N') + output = "" + graphemes = [] - screen = pyte.screens.Screen(COLS, ROWS) - #screen.define_charset("B", "(") + for gra in grapheme.graphemes(text): + if len(gra) > 1: + character = "!" + graphemes.append(gra) + else: + character = gra + output += character - stream = pyte.streams.ByteStream() - stream.attach(screen) + return output, graphemes - stream.feed(text) - - buf = sorted(screen.buffer.items(), key=lambda x: x[0]) - buf = [[x[1] for x in sorted(line[1].items(), key=lambda x: x[0])] for line in buf] - - gen_term(png_file, buf, options=options) # # wttr.in related functions # -def parse_wttrin_png_name(name): +def _parse_wttrin_png_name(name): """ Parse the PNG filename and return the result as a dictionary. For example: @@ -252,9 +374,8 @@ def parse_wttrin_png_name(name): return parsed -def make_wttrin_query(parsed): - """ - Convert parsed data into query name +def _make_wttrin_query(parsed): + """Convert parsed data into query name """ for key in ['width', 'height', 'filetype']: @@ -281,43 +402,3 @@ def make_wttrin_query(parsed): url += "?%s" % ("&".join(args)) return url - - -def make_wttr_in_png(png_name, options=None): - """ - The function saves the weather report in the file and returns None. - The weather query is coded in filename (in the shortname). - """ - - parsed = parse_wttrin_png_name(png_name) - - # if location is MyLocation it should be overriden - # with autodetected location (from options) - if parsed.get('location', 'MyLocation') == 'MyLocation' or not parsed.get('location', ''): - del parsed['location'] - - if options is not None: - for key, val in options.items(): - if key not in parsed: - parsed[key] = val - url = make_wttrin_query(parsed) - - timestamp = time.strftime("%Y%m%d%H", time.localtime()) - cached_basename = url[14:].replace('/','_') - - cached_png_file = "%s/%s/%s.png" % (PNG_CACHE, timestamp, cached_basename) - - dirname = os.path.dirname(cached_png_file) - if not os.path.exists(dirname): - os.makedirs(dirname) - - if os.path.exists(cached_png_file): - return cached_png_file - - headers = {'X-PNG-Query-For': options.get('ip_addr', '1.1.1.1')} - text = requests.get(url, headers=headers).text.replace('\n', '\r\n') - curl_output = text.encode('utf-8') - - typescript_to_one_frame(cached_png_file, curl_output, options=parsed) - - return cached_png_file diff --git a/lib/unicodedata2.py b/lib/format/unicodedata2.py similarity index 99% rename from lib/unicodedata2.py rename to lib/format/unicodedata2.py index ed9070e..45e7ec4 100644 --- a/lib/unicodedata2.py +++ b/lib/format/unicodedata2.py @@ -1,3 +1,9 @@ +# downloaded from https://gist.github.com/2204527 +# described/recommended here: +# +# http://stackoverflow.com/questions/9868792/find-out-the-unicode-script-of-a-character +# + from __future__ import print_function from unicodedata import * From bb17342b063afea42df5f567f6ac88a27f4dd974 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 4 Apr 2020 14:22:26 +0200 Subject: [PATCH 10/73] import view, import format --- lib/wttr.py | 4 ++-- lib/wttr_srv.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/wttr.py b/lib/wttr.py index a1982dc..c9c33c8 100644 --- a/lib/wttr.py +++ b/lib/wttr.py @@ -194,7 +194,7 @@ def get_wetter(location, ip, html=False, lang=None, query=None, location_name=No cmd += ["--bg=dark"] p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE ) - stdout, stderr = p.communicate(stdout) + stdout, stderr = p.communicate(stdout.encode("utf-8")) stdout = stdout.decode("utf-8") stderr = stderr.decode("utf-8") if p.returncode != 0: @@ -238,8 +238,8 @@ def get_moon(location, html=False, lang=None, query=None): if lang: env['LANG'] = lang p = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env) - stdout = stdout.decode("utf-8") stdout = p.communicate()[0] + stdout = stdout.decode("utf-8") if query.get('no-terminal', False): stdout = remove_ansi(stdout) diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index 802dafe..547b65f 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -10,7 +10,7 @@ import os import time from flask import render_template, send_file, make_response -import wttrin_png +import format.png import parse_query from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS from buttons import add_buttons @@ -23,7 +23,7 @@ from globals import get_help_file, log, \ from location import is_location_blocked, location_processing from limits import Limits from wttr import get_wetter, get_moon -from wttr_line import wttr_line +from view.line import wttr_line import cache @@ -266,7 +266,7 @@ def wttr(location, request): 'location': location} options.update(query) - cached_png_file = wttrin_png.make_wttr_in_png(png_filename, options=options) + cached_png_file = format.png.make_wttr_in_png(png_filename, options=options) response = make_response(send_file(cached_png_file, attachment_filename=png_filename, mimetype='image/png')) From 6b2577b745a4bb35d47d3bf6d0f000061c7964ee Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 4 Apr 2020 14:49:22 +0200 Subject: [PATCH 11/73] added share/emoji/ --- share/emoji/☀️.png | Bin 0 -> 2340 bytes share/emoji/☁️.png | Bin 0 -> 1901 bytes share/emoji/⛅️.png | Bin 0 -> 2456 bytes share/emoji/⛈.png | Bin 0 -> 2882 bytes share/emoji/✨.png | Bin 0 -> 1835 bytes share/emoji/❄️.png | Bin 0 -> 3389 bytes share/emoji/🌑.png | Bin 0 -> 1194 bytes share/emoji/🌒.png | Bin 0 -> 2473 bytes share/emoji/🌓.png | Bin 0 -> 2302 bytes share/emoji/🌔.png | Bin 0 -> 2585 bytes share/emoji/🌕.png | Bin 0 -> 2506 bytes share/emoji/🌖.png | Bin 0 -> 2525 bytes share/emoji/🌗.png | Bin 0 -> 2222 bytes share/emoji/🌘.png | Bin 0 -> 2463 bytes share/emoji/🌦.png | Bin 0 -> 2826 bytes share/emoji/🌧.png | Bin 0 -> 2657 bytes share/emoji/🌨.png | Bin 0 -> 2744 bytes share/emoji/🌩.png | Bin 0 -> 2164 bytes share/emoji/🌫.png | Bin 0 -> 725 bytes 19 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 share/emoji/☀️.png create mode 100644 share/emoji/☁️.png create mode 100644 share/emoji/⛅️.png create mode 100644 share/emoji/⛈.png create mode 100644 share/emoji/✨.png create mode 100644 share/emoji/❄️.png create mode 100644 share/emoji/🌑.png create mode 100644 share/emoji/🌒.png create mode 100644 share/emoji/🌓.png create mode 100644 share/emoji/🌔.png create mode 100644 share/emoji/🌕.png create mode 100644 share/emoji/🌖.png create mode 100644 share/emoji/🌗.png create mode 100644 share/emoji/🌘.png create mode 100644 share/emoji/🌦.png create mode 100644 share/emoji/🌧.png create mode 100644 share/emoji/🌨.png create mode 100644 share/emoji/🌩.png create mode 100644 share/emoji/🌫.png diff --git a/share/emoji/☀️.png b/share/emoji/☀️.png new file mode 100644 index 0000000000000000000000000000000000000000..183386f00e36feab2466af021e14467018dcbebb GIT binary patch literal 2340 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 CJxBlm literal 0 HcmV?d00001 diff --git a/share/emoji/☁️.png b/share/emoji/☁️.png new file mode 100644 index 0000000000000000000000000000000000000000..56606277d01efc52202e3015c722109754fe3ef7 GIT binary patch literal 1901 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 SdVT7rNC4O)Q8qOQOv*nAW1nRJ literal 0 HcmV?d00001 diff --git a/share/emoji/⛈.png b/share/emoji/⛈.png new file mode 100644 index 0000000000000000000000000000000000000000..ddacbf10bbc595878d7200ff6fa6b30c68dfe6ad GIT binary patch literal 2882 zcmZ`*c|6qJ_x}{i-e#T9WY022vZNUrOj)wbAZyA}jBPA~c!*>PFu#1AB1v5Dyv z_SR!E7>rI%WJH9cquW${x1RpxffNq2BqLoeHe}Pw@tigFt-wKmU!I?IAKV}U?2ORmv#FY5c zlID@efMQ7`uK?pNhL`i7(Saiu!93Ih31HQ9Pu}eOig91xWrmAO`1~aklP|KBy$^GB zuwMrr*%~*8(i8dKDwSsd7f~{)T08&+=z$^<q%Q772KvVh$DaDQqw z@*kL~sf1O$aXLvgB@>U&%*;wBfj3}p^hC6zrO%&dvDUlxCVYKusz*VYdg4@VVPt9Q zlqeN-u7R&KRVH;?I6m1%Hca2VJ$Zo;-xksvxV91)RCG&Wa#cZrhX*#a4Xd}-mfG4f7eMy=sDd;i z8%T#FK!`*k!MGpnqaKC#ynuwij=SQmH?>fSXRw^d7Ku`)ny$5v?a(K%wngl3J=bS^ zTvDD8AKVT|IE+LfqFDspOLWsC@U8n-tGRD(=y5iYm~&jb9Q_H`O5j=D2vX%w?AgxE z4QAH52_{=2M>bZ3Mh%CA5>-V-30L&7ceeVM>#?7~{M_T^=0XZa`pGgX{K9nHWFS<< zNY-73x=AEDR=)}=EAt(KK#bz%I+c^L5<<|6>zsAPbXZ}nEOi$O=4h{yl zP&`{yREC~*$F?g7sq#%mR1Y-$9VPm%YqWi z)|9P;X#+7UpETVR^Gr(70*Ox*{AY?RAX(@j1Vuer7C*LXZ7SfZM4VVcEs;pV*3 zGA~^@zuVURWNd4?B2YHI+;<q^^wWX6pJ7i_J+$-QcP2C z#ExlXrjYytTClb@$^tnTQCtJuAWw<&As&iZhmn&826cP}esX_D)un9~2YA`c`MSus z>d09QYGSb&b&!;_q|@83R9V ze7im#vJ^R2b?wA)w7p2mWvAe2*CwaB`&n6;x!h2qoRJ3_BUWuv<=n`M%5%L)BvS3s z5Oo_MVmrbnA_6!mOGTZX9FD1}#>vG(n6k3Aw$jZ}`UQ{Z_-?fHi3)l$sFE7|9n?R@yPElPOCg|e` zb497hIh=QyH;EKBezKygu`x}cxPVqh@?-a_T#sW=j?n44pd!dQo*{~HtaH96?_F-{ zFy$QoXI+22u@#ir3yliC^Lx157LG)PwV|a2qxy5^i>KO}>el*_8ygwRGtu)IS|-%s zsaKeh_fOz(r@9~WTvr}*bJavzC?0@d3{NA<svG^NN;$wsG^w_~Ou?bBXuwUO%dTZq%+RjGX#gh1_QA05YJ%nEo?9j zE^mXyVZo-@Kx`No8HDx6$b-W%F*o7zXQ5n@H<~TH57YX)-KYZ#n&Apk`YXuC6L-_l zJM1Qx0cW9Sp_d>=>P^jp4yyO1?1YvzJx1#@lKzxV(A{PKy MAZ^U*Ol~Cp5Ay<9sQ>@~ literal 0 HcmV?d00001 diff --git a/share/emoji/✨.png b/share/emoji/✨.png new file mode 100644 index 0000000000000000000000000000000000000000..f6419fa73dc2164748b3ea15d64de5323caf5fd2 GIT binary patch literal 1835 zcmZ{lc{CgN7RP^r+NzAk@|afW@D&dL zIu8Kp(4rP6Bkso;KU-@HF1faE-Wtb+00nc+Qh?3Jd;XG=cXs`K0Qi?M7N#z^jy7D7 z-J{#kD;v~RNvx;v-WN0eNk|R~=7_uwGhfF4okvvFWZ9&q$(|0(Ej|^pdZZy+CY0}~ zBkXr#OyI^Yz{(+=ehf|j_U7_^$3JI-R zc$7O;1$xxDs=CRU!csF9LmL|GlyknW=UVL8sO1Po8(qJy*VZB!&h*lhAU4TnSTrm> z&zEwk&cPP!MO9R|%30SsT+6|VrZ8Oc*Vc2LvIH!hpz+R#N07iK(r%w(s6j9aGZ}pM z>fm6L$G2@t%MPX7Nz=!C1cAU{1bYMIo`VXz@}J^|mbc*1+Ph9wkFoXNN4i9o)l}(Y zQ%1%1FPvX?&bPEoP7a-eLfOmYsZ?K?y`^y3|$VQq|%biE{Ju9{oK*%SD!aDbMRNL2C?d5 zm!+ju$J*2yeOw=j)MH1sOHa?wYj}z`ur6QV9cHjZ#m}OP!Ft>mCU4ebsJklmg_(lH z%@)dBjAe4SF3Qz&^;@)2L`2+FTg2*w8$@7n2e!&5PBLht-~2htgC4WGFyLLHAxM6r7sZ~r?ph?H*C7P}vr4in2(5{zdD zWTf<}y-Pc9oe4LHY2LP#bx}=*fbP>mfkc=fsM6E^34gdZ4+l3JT>DpK6l%v;`SXg4 z3q@E0kOUJIzSIgH!9-Zt(e|R6TS4Iy|EA9D?zaO3R-0Vu&DJZErlWA*BJp_njs+eY zrL8T!IviB8N?8e$4P-N%`8W5a5BGT1j+&n*RV**JiGcFR`@E&qqF0a44o^&c&6SL1e&Q!%GwjG2v4U_^^Ld{bAMdx%W4P=aHY3{fcMltDr+v+~37)x9-@ znt5Ohy;|2!9~#aCEv+5}JR_r{Bk-nYgRb71P@N&Zgaw)0EZFazopc4Fa-6hQiLpps z9a0B^H2gdx=IBmrwA_B7^1y6moBGQy1vl18H!{}N=<47}b*tR7#&0M02;RT3bBgHB z^*D_|QPBfN&IMRmT?RE`cLy|kY572mqPKSt?{;NxHIJ>t;xZO_sG)6#; zk3YTq$ET3V0A#3vx|C&22Pd}s;k+Ms5x)PyIK8v;dsJc|k=Xpwp)j9FeuInselqZ{ zn%pv*@M*ncG4X=DyrOhU^5((rQH%FsEZrQ}4x0E&svEDV{Bl+^BaR7}8Q7rcm8{o{qOV z&s36Vi?dtJUhxSDb8}5?HovJRy`RAObx9nHC^C5-8C#FlaF2j7#S%yH_YI{G$sxe# z*HGUO7=au@F^1cdDP)*AIhY&{v%Etl5#g|3iL_hBa2+Jq;8*60Dvh*$lwVPZk~HBq zy7;3}fG_11+CTgj7eEK8gVaFkX&`l7wDr-thG<=s8d3+1M4p?=yzxJRJLCW&A^QId T;@;|WxB`H&w6}O=hEM!2oJC7) literal 0 HcmV?d00001 diff --git a/share/emoji/❄️.png b/share/emoji/❄️.png new file mode 100644 index 0000000000000000000000000000000000000000..d4a3f9cf985fc4105ff1bf6a14f30dacc04102e2 GIT binary patch literal 3389 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 literal 0 HcmV?d00001 diff --git a/share/emoji/🌑.png b/share/emoji/🌑.png new file mode 100644 index 0000000000000000000000000000000000000000..18210596ecc94bfc314f7462d7934b9ea923dd50 GIT binary patch literal 1194 zcmeAS@N?(olHy`uVBq!ia0vp^av&@KBpCco>)HY-mUKs7M+SzC{oH>NS%G|oWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5Sk21sKVe}OR#%Cew6UFmfjPs|#WBRO)^5|RI;1DOldBkOn zn!={!!#Zh4ns{@5p5_s(KcJ+%tyq4V~ zH8NCb)v8r#9htW>YJP6oY%KFn#74rVcGs3EsjC-kAQosU zEZLiPe_rEeAJL0CU&IafoGm`C-7!bluy~zLwJpEHmVNV{GroxV5G;}-xGu}>N|q%% z!-1GkIR)R_-tNaYss_G3QGL!ThULNDj*iI4Du(ALI$45e9Nkmgyh868-&_M@;g6q{ z8!yLvUhyOHtgD-H+9H$CuMBBtRxNB#uQg4V)fYJVd4hPu3FeT{X^bJEkzEWG_w&>} zP2Mibbz%@;U0n5^(Pdw4bgXQ@b6x1fsZ$rc_<1NabT9uEhCSsB3~#?(UH!N)qkhA} zV)4$UtD-{FTvnxdGWe@W?@zTXo+H2Lf79XOX-_SU8$6bOemR+)L87-!zIyL2J~l-s zkZ6i_e#9^Hyqtgg?nw4-(%}$m3pVwfHa&W6`TMxNT3!Et z-WB-zdwbs3YZ0%kwjBFyqI;TO{@XXV#qs}EAF_K}rXTMY%y9MPm*3wnUp`SU_n&Fv z4tJHC4K2aIY-m^F8c~v*SWuFgpT}TRT#%Tjkd~iUVyonrUy`rjoS&0lq~MyHpOvYk zP@GwnYO7>q00c^QALjQ=w9yBtw_Ezp=m^jxRgg(3i6yC4$wjF^iowXh$UxV?MAyJL x#L(Qz*wV__OxwW7%D{l{VSgx!hTQy=%(P0}8tV6$zXWPv@O1TaS?83{1OTCM{@VZm literal 0 HcmV?d00001 diff --git a/share/emoji/🌒.png b/share/emoji/🌒.png new file mode 100644 index 0000000000000000000000000000000000000000..e7aa0ae6ba928fde88e9ba4b5283c8e15a70a432 GIT binary patch literal 2473 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 S>#cGLfQ^Ntc_Y^M{(k|%(}}zQ literal 0 HcmV?d00001 diff --git a/share/emoji/🌖.png b/share/emoji/🌖.png new file mode 100644 index 0000000000000000000000000000000000000000..b5c9791064d7478b1c250482e515c3681da04911 GIT binary patch literal 2525 zcmZ`*dpy(YAOEUhB&MiUlhLXrG*RY~%QP9yas3^WXt{0}W9D)g#&N$VO0p=|QZAwC z9Eanu91UGG_a&h?E?LWQ|9$=W`{Vb1y`JaudM@wJ^E{u=^Z7has-5kX{jx`80RY%f zu)6F3%Hz8Qwg;>~|2f?Z3W%>c(HsEkv-fS1CBZh*)5?Je08ys^ATALA*1@j0F8~mR z0f4z10AP3r01gKix7r(n15!6_u3QEsh&79su>b(AAzU_ha_MZkb1mo?PnqY-^jR7^ ziBEmxM^g9hs`GD3BEN^SQ8Wj*ISjdAapOd=Eb83afd$8XkOiB#2Q28oO!?L!i4_>C zF4MI>-mv-PT;5QF_HWvSFWo|(>Be9usM9vlsWE_j_+g`BVeuMA*Ei)&=ml#TMaVln zB3==KLuXE5yRn2%SVFWZl#NU)L#MI&(ErXd`euoaO2Ww5+LoF@VO!)ngB$Hv>dv_X zZ!M|RxHihKK#y}9n|}SaX!|(z-*);KCPYO*zt5~3r-V1R>>_ZqIdDS~cdvqMxBiW% z8uPqvxnnJKmfG|;qxum#DYrMZtfodojg5rDuN4HIV2FGkn!n0C*4qmY3K(n4q*uP1 z*O4+&*lbtWq_vDLFpoXX9PFk(6U>XFO!;Odh}FTLJy?;{x%6||ZrHH7@o*vQ7oQ3f zQ~k>MUvVbq2o0LOY~TglGb5*>S_3yo<-X>E>*4H`@bGNQwsTN>C0@QmZd%zRYE1?8 zwspBfwN)O9;P8iVX>}t*r+Y{j$1#ol@z2;;C5N@rlX~z<_HmPdqFcAR%20|pDC@PW zz4aNKqhpu)Y=b5Nn#pU+uAh3L-E9s_8rGN5k?X`{;7aXY%^KaetsEtSn)52-SUX#* z6Zh_K^>TK;^QL(IVs6n>|Du|_N*7g(ISi7R)`g1?b&=*Bk(VqLs>;E@BZMHJc56Eo z=t$k58NScsVQO=Od+g`sO56L!Bu&LaLTy2HNgJz)YT6H6Lp!F#we_Nz_c$+mR;AN?7 z;Z1e|CSvjld1!Tr!&j!34J=@kj!F&fBP(SXBpczNio0y#YK$=ax*%*kIbyL>p`Y_i zkgavxfI74PMB&5lk&`~PuC-2B50|PzET^cb_%i|pIS*9F6`5;_#B1x(Dj&$I4DHJK z{O!qvP~Taf)2Gw%B$ncf8{qwe^85SwCkb3*gC2wO#9))-*}SQGj?Nt^uRGKx*ZUvE@?SM9wF2rk`bs2nSE)a{UR8sC?JK zT4x3koh$AY{l(ui9a40N2YUV^^3^m&zK5p0ym?%&JYSYw@wmneBlXvY>988-u=bb? z8;Ow`-mBJkWbeQetA^jdbjBVvn=}+hxjK3!U&V_aL@at#Cr?%M!>15TV>6XZOVG0e zjl)r@dau#4otXP7Ra!=(_h-w{gkKIu6i56>@DXH%c7GgMjWNx7h?bL710x#>=+93* zhxeLgTTapQ7_H+^m-Kj7J`0TrVt!MkB{NI1h?X z=UOtOzUl$@bo!+C@;Zh@mM0?EI><#rp+m%;n_Ov?B=V~vs%H#8EL+vXO^;~)u~_u& z#o)-qkC{6E$GMNkynOJZ=G}~D^ z+wp;T=Kb`*;owr~fATk)vuz5WX_7t!V-c2$$WsKkl!PZ8fkz(b{)EjsvTu(t`QqT` zgoO!Z+(3wYdI<{I!xApw**^@koS3UyjH8bh;N%L0u{%^ z>a7^}CzwHEz#S*4MPDoMCVgld;fE!6VoNxjOI0;jvJfT#@TA;+|D)Yd^3@d0vuUWi zKJ`^K(<||Nmr?hmf4Ig?a8%#M%@)^pw9~PFM6b;`_x-Wc_QtGA(D(!yXEI!SY~6y` zP``_BIkJnQ{vGiWpRem{yHkNB!}skoun;I9ss`U8ay+md_u*p4hg@g8s$>k8lS=5A+l9uLjok7r@q7!7ORHrDTiW~hR#xKe1d6JqhAr6FmeJh^ljy3t~2dTdg zU>Xi%tVmirskdcZ4~Jco>btCzUlZAS?&NkL|V4go2%<94f;iQ&lI9>duymeCM8ErJ|7Y6w;=lKF`1`MX}i0*KXrO~{TfM!Zxqf9 zKO^2kTKTs;fiuAdGravJNypf)-{oY205rtypI6p|JBzRHrlzi0G&UAdM@vrkgfCsZ z2MaEH@%#8!JL+gwXQWaWJxW8u?qAdDK`@)va-?G92Na5GG-TDlx2&w9r=jdi6ymf@ zIlZ{Rwb)k@NLej({K;o$?*eE2S;iBIaPaeD!oa&QJjuZfN)Qb&4h<&LkUl{)#wB&z zAVv_pN954!(1a3J zVtb`>ewTzKDblj(;c*&yf1dx|Ki=>4x$o<9AFk{A+}HiR?)!I~yOW%>iZlQKa#&{v zJgC+78blQAzp%fyfl55w-pw8Wnsa5ggCxK)G{hP21^|gV0FX)tfGuz;^&0@h!2#fV zAOIkW0pPdjvQAGVxF8vP+Q|XbAU033QUGAz6|953mk;Nmj~`{~l%k~#=R{qp?!Y^yR5YYL)4J*<8;85H!Xv0h+C~m(U4L2>%Q@(T=MjeSh6MVv0XFI(XWCp=^Ucp zBImSZA`JdLq z!Y&U80+i}7{%+0D=vU&mK%=Zo-XxaMxB zc_h@sg_X_iu&#-Swvvb3zj28*9rd(Hc9kXQT5(4tFAVkc@cFzZN>qd|0%beAqS_Z@ zARJf<^(iCP^^|9AYk!SD zr3ik6w(9G*G}bfHjpK|taMyTxY-W&>%iZjs$l(j>q=&O5)lw>>>GlmJ#+XUX837U(iksCp~_w&ro-q;5?<=A6d&*IuRtu1jPA zC{EL*q8QIm<;oM6f5kjvu{t~R3aT5@g=8^Z8hC8gz*eYKI`o?|sw%v&Xb<7(zqVI) zcFyr{Sp8}_a=os;p88=dy(%BC)F(&97ZM4DB1YS4LsswhBuy`*BCY9Oaw3m%v0;&> z*Sm7|pw;8o4R)!zvSEZmqBbE>gDfU@>`}MjCX8XhqN6obpk#Erk&c4-td)aVOAR(K3Ny{4o4k}2 zdo9`vL`i&n?%K$?R#g#hpGAWm%5Zjy+YvRV$>bR@v{3XcfNU+A`kU4Om;8^c6E=qp zO-45_VDI@(r9-9W=OMup-{$Fjhy7;tVXZxQ>1!DfmwWF2#_rQ;axaWQrehaJjvf$` zHxeJ=_(}|@9#EDHxfw#BL!|dBqvBr}=k|-Q-4Dsg#ckF0-Qf!oo3u2UoFJ z+0il0kD61Fu+&nW6nZ-|ay_v(xZ{IhcQahm{#A7QBlsf3E~nP8U0l>j zw`Tdzm+fPk>o=mkxmQ3?ut;e*IN7caB1&iwxiK-gCV6L*qb&DG9@0a8_>!{%ssfnqn3C}Zb z|5Tu}izP*BIF+O|FkBsN_bQ~FITu}Ka8bp2E@3a{g~Sr3pPq$EiTA@xi24Wuvp;6E z1*yOJNe{<1cjKDNT*Fl2fWEM`n9G7+KeI^8rlv-zBuiHND;b6+wo;E}bOF46@AqlC zj9QTdXbdVBZ-2Z%<%UnAH;<;!Yi=krSxbtHph-Fh`?V4Mjy>3uZtBKY*OBFa-MhNA zxe#Ss)X$V+v)S9oK5X7`Q`upCM!hr!S?&^3R;w*Pdmd-!qo{#_;PYEs`*LlV__}cu z{`x*K=RVRP&U_XmP$X-)0+epJEC+u7eL2@eh}Ouzy1VmYm%Bt&#pE-a3=OHpb0(R& zB2{d(QBnnvkB-L~nGIBd_qm}RDu0}e@ao!QlX$QujQpZt}*E zec!t`E;<>vp@GOgz#)gA*qH-OU5Znk(y8`o^nx1|4G$}aDJ^@^VEicwXS3SQ~N{s;Vx(KJUNT1ZedjYNq8kg?H0QP40-6b+^6 zPN7ktCn;nK73vsCi6Ci0V@Zibl%^>hgeH3VUQQ9xcyB*Ch=Q8bRj%JNGuy6)$`KG#!l1pT{)`Zjd{ z05niuNPmzG{u$5>;QaSvVHZfsac+KY0MJg`v=*fTu8pu>{(b%p1$?;J?wAGxhex* zW}7|ekPW&tFVd@ef^xJGCHJN3puV*7zdE8OgiV-?NguX#eVRrP6R3>QVQ!w5ZOgE^q8-V4c zz&Y(!BONzu|3zwdfMt1x;j7ghT+~8DcdFRG{o7Pr&@GqM4+`mFXDwQ@|IpY8AO-wR=!Z9F*y!&*7%Dub6`bUZOH&xG^NfYsA3 zIMgr}ZySlF{kGC4pUspni~H9U0-CN~oCvLF=ZDczx#TQdEqh#7^p57*cGG;g_5f4c zc=12YrVxP+hck8nhNZNdq3C8?Q%rGBT00Y2p%+U@5V78gjCDoy7wPDa&vFr?rgry> z#A+)afAfnCywk7OvpNg8!`?TLe-3+h-)xz|-O?MsbW-~a!0kxpcO{dd?vU%dz}N8Z zi@3#;d@4hrgC+ug5%hXi#hbjFw(E>u95Vg~o47IU80NSQ^Ta6nf)p?(Hg=41zuFHz%}QFKoFEUD6uI-~ar+waPbY zKEMqV9ddaLIe6A-rp*Wrx2=KL)hzC>3|fXCvPWdaY1a?ur4aUYC+(>jp%zDkrZ09T z=?)_+W1UB7L`O&ie^IVdH}jDI)7RX6%7!HnQ+)<60nheSml-iU#^2m4D$?5n+h|ON z4}Pn7`RL6?r3oE!)|{>J1^0c`jl9Usv>v7h0~V!|xD9UVTKinvq!1=`Uf%3Hn$l3x zROZjf-^@7s#dMqPU4@JYyLQ0VEVq!_e~Ztmxrcv8m}AnUs%}AJxj>UD2t$UQzb_Xn@q^X zMr7+PeEm5QiCSvrSrRh@DKWd2%f*yrRvH@atz2-JMn6i7JfGi~?=ez;cPzdOE_#+R zHnG9&4T;+DvMafl$Da-jm)(DKjaOU!zG#~g$OgZo zleZ_YkmC=mTsPS8(qrjM=A5C0z#g|Q3cTY8TV7?T`WTy`Iw-|ft(Fw)Kpj6HroRQ| znb2mdcXRW;Dx>VQ0uu*o>fx!B81JzZ*)?*f{WZVC^mOVTO@jx$fPgpuhySUMl`5S5=yGO^cns zTYu$%z3Z)nmKO7zY^ZUIWnm8v2E}u&jOkBO+9V`5;KUA9)#p3!IYeJf9=LFbMtpsE zNW=C$tT^NL^w7Uh>M4@RSgg077TK4xO1=I3wrsNWc2Ve8Juj@N6$aceozYLy{vy>LxDRr$`fEeA(lV)CBDQk~ogamWJB2GN? zdrTzGMsgSb(_2&1+Dd;S?W*;Tv8mp;%Yhoxjcc70lVc2_I}SdDHUOwfrL;)RSN}rO z-u1>^tG#o5{3X4j+@l>~=Dvr0=xw{y9qh5xiNs0C1AItR0d$C7=G8dS9&DmTOS{rC zY=eCx9jJ>@%UL?AzI!0vZoBH59`d9HtIp1<+(+oY^7Yc((#IUPC~nI#y|=EeNbvp1Y)>sLwU+VpaFL0w z);khP+tXYUHKS}(Zw_%QMsoxYPMAmEVN{I9*-(U1u`RR8*W^)U)RU7GaZf#>DWOnX znH-rE@>2fY?Z?+ep-?8nvH9DX=S;@mMydS7$&}A*aa~?vR|EyA#?}>&7`t8e)QVMp zgo%S4Di-8q`-cp{{#aT6@L~C`gmLh=`NPSS`T11Gi7Qp8=JY;LEV~jr&#y~r@Lgv? zao9YFp#JzAXB~f{;&Mcmqi!$pv%J5iI{j^KGh;j;Jjm{+M`~oo&!PHQhN#Cal283v zxwp5oT9l~}$|T>{L8X@+WB2wRXHoYbqE#(B5W***p~00`e~req9f1xK!zSFPxq)E+ z=80HsIGhuj+0nm55>&gWt#1;+1Ex(=6)(W=hYP_on1GEsO-R7w0Oz#RQ8=SmJdSYC z1dS))jXdy4_*5g$WPDF*-Hq56U~TWT&(Ue${#^(gCj>(CE$!I<1SI356JkmKKj8d*dodUQ NpghsYHuuP!{{rl}q*MR^ literal 0 HcmV?d00001 diff --git a/share/emoji/🌦.png b/share/emoji/🌦.png new file mode 100644 index 0000000000000000000000000000000000000000..b5063f9f5fd2867761039b5a57ecdba088f1270c GIT binary patch literal 2826 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 PE&+hBakQ?t^uzxbMSVwg literal 0 HcmV?d00001 diff --git a/share/emoji/🌧.png b/share/emoji/🌧.png new file mode 100644 index 0000000000000000000000000000000000000000..b4352b78386ed2baf564002f89fe497e5e97000b GIT binary patch literal 2657 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 literal 0 HcmV?d00001 From 4ea5c74b03a54dfa2d22bb8c03bd22f60f79007e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 4 Apr 2020 14:49:53 +0200 Subject: [PATCH 12/73] extracted get_moon() to lib/view/moon.py --- lib/view/moon.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ lib/wttr.py | 42 +-------------------------------------- lib/wttr_srv.py | 5 +++-- 3 files changed, 55 insertions(+), 43 deletions(-) create mode 100644 lib/view/moon.py diff --git a/lib/view/moon.py b/lib/view/moon.py new file mode 100644 index 0000000..7d34713 --- /dev/null +++ b/lib/view/moon.py @@ -0,0 +1,51 @@ +import sys + +import os +import dateutil.parser + +from gevent.subprocess import Popen, PIPE + +sys.path.insert(0, "..") +import constants +import parse_query +import globals + +def get_moon(location, html=False, lang=None, query=None): + if query is None: + query = {} + + date = None + if '@' in location: + date = location[location.index('@')+1:] + location = location[:location.index('@')] + + cmd = [globals.PYPHOON] + if date: + try: + dateutil.parser.parse(date) + except Exception as e: + print("ERROR: %s" % e) + else: + cmd += [date] + + env = os.environ.copy() + if lang: + env['LANG'] = lang + p = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env) + stdout = p.communicate()[0] + stdout = stdout.decode("utf-8") + + if query.get('no-terminal', False): + stdout = globals.remove_ansi(stdout) + + if html: + p = Popen( + ["bash", globals.ANSI2HTML, "--palette=solarized", "--bg=dark"], + stdin=PIPE, stdout=PIPE, stderr=PIPE) + stdout, stderr = p.communicate(stdout.encode("utf-8")) + stdout = stdout.decode("utf-8") + stderr = stderr.decode("utf-8") + if p.returncode != 0: + globals.error(stdout + stderr) + + return stdout diff --git a/lib/wttr.py b/lib/wttr.py index c9c33c8..85cee4b 100644 --- a/lib/wttr.py +++ b/lib/wttr.py @@ -12,10 +12,9 @@ import sys import os import re import time -import dateutil.parser from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS -from globals import WEGO, PYPHOON, CACHEDIR, ANSI2HTML, \ +from globals import WEGO, CACHEDIR, \ NOT_FOUND_LOCATION, DEFAULT_LOCATION, TEST_FILE, \ log, error @@ -215,42 +214,3 @@ def get_wetter(location, ip, html=False, lang=None, query=None, location_name=No filename += '.html' return open(filename).read() - -def get_moon(location, html=False, lang=None, query=None): - if query is None: - query = {} - - date = None - if '@' in location: - date = location[location.index('@')+1:] - location = location[:location.index('@')] - - cmd = [PYPHOON] - if date: - try: - dateutil.parser.parse(date) - except Exception as e: - print("ERROR: %s" % e) - else: - cmd += [date] - - env = os.environ.copy() - if lang: - env['LANG'] = lang - p = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env) - stdout = p.communicate()[0] - stdout = stdout.decode("utf-8") - - if query.get('no-terminal', False): - stdout = remove_ansi(stdout) - - if html: - p = Popen(["bash", ANSI2HTML, "--palette=solarized", "--bg=dark"], stdin=PIPE, stdout=PIPE, stderr=PIPE) - stdout, stderr = p.communicate(stdout) - stdout = stdout.decode("utf-8") - stderr = stderr.decode("utf-8") - if p.returncode != 0: - error(stdout + stderr) - - return stdout - diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index 547b65f..19c895a 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -22,14 +22,15 @@ from globals import get_help_file, log, \ MY_EXTERNAL_IP, QUERY_LIMITS from location import is_location_blocked, location_processing from limits import Limits -from wttr import get_wetter, get_moon +from wttr import get_wetter +from view.moon import get_moon from view.line import wttr_line import cache if not os.path.exists(os.path.dirname(LOG_FILE)): os.makedirs(os.path.dirname(LOG_FILE)) -logging.basicConfig(filename=LOG_FILE, level=logging.DEBUG, format='%(asctime)s %(message)s') +logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format='%(asctime)s %(message)s') LIMITS = Limits(whitelist=[MY_EXTERNAL_IP], limits=QUERY_LIMITS) From 826cedf1f051ff5433cf33b2776bf1bdb9d5bdd2 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 4 Apr 2020 18:04:27 +0200 Subject: [PATCH 13/73] moved wttr.py to lib/view/ --- lib/globals.py | 4 ++++ lib/{ => view}/wttr.py | 7 ++----- 2 files changed, 6 insertions(+), 5 deletions(-) rename lib/{ => view}/wttr.py (97%) diff --git a/lib/globals.py b/lib/globals.py index 444e97a..b24d791 100644 --- a/lib/globals.py +++ b/lib/globals.py @@ -122,3 +122,7 @@ def get_help_file(lang): if os.path.exists(help_file): return help_file return HELP_FILE + +def remove_ansi(sometext): + ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]') + return ansi_escape.sub('', sometext) diff --git a/lib/wttr.py b/lib/view/wttr.py similarity index 97% rename from lib/wttr.py rename to lib/view/wttr.py index 85cee4b..e8eeb70 100644 --- a/lib/wttr.py +++ b/lib/view/wttr.py @@ -13,19 +13,16 @@ import os import re import time +sys.path.insert(0, "..") from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS from globals import WEGO, CACHEDIR, \ NOT_FOUND_LOCATION, DEFAULT_LOCATION, TEST_FILE, \ - log, error + log, error, remove_ansi def _is_invalid_location(location): if '.png' in location: return True -def remove_ansi(sometext): - ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]') - return ansi_escape.sub('', sometext) - def get_wetter(location, ip, html=False, lang=None, query=None, location_name=None, full_address=None, url=None): local_url = url From e2cebf74f287164b9aa445c561df82afb1910211 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 5 Apr 2020 11:50:58 +0200 Subject: [PATCH 14/73] store big lru cache objects in files --- lib/cache.py | 61 +++++++++++++++++++++++++++++++++++++++++++++----- lib/globals.py | 2 ++ 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/lib/cache.py b/lib/cache.py index 8f923b4..b7c5179 100644 --- a/lib/cache.py +++ b/lib/cache.py @@ -5,12 +5,21 @@ LRU-Cache implementation for formatted (`format=`) answers import datetime import re import time -import pylru +import os +import hashlib + import pytz +import pylru + +from globals import LRU_CACHE CACHE_SIZE = 10000 CACHE = pylru.lrucache(CACHE_SIZE) +# strings longer than this are stored not in ram +# but in the file cache +MIN_SIZE_FOR_FILECACHE = 80 + def _update_answer(answer): def _now_in_tz(timezone): return datetime.datetime.now(pytz.timezone(timezone)).strftime("%H:%M:%S%z") @@ -26,9 +35,10 @@ def get_signature(user_agent, query_string, client_ip_address, lang): `lang`, and `client_ip_address` """ - timestamp = int(time.time()) / 1000 + timestamp = int(time.time() / 1000) signature = "%s:%s:%s:%s:%s" % \ (user_agent, query_string, client_ip_address, lang, timestamp) + print(signature) return signature def get(signature): @@ -38,13 +48,54 @@ def get(signature): the `_update_answer` function. """ - if signature in CACHE: - return _update_answer(CACHE[signature]) + value = CACHE.get(signature) + if value: + if value.startswith("file:"): + sighash = value[5:] + value = _read_from_file(signature, sighash=sighash) + if not value: + return None + return _update_answer(value) return None def store(signature, value): """ Store in cache `value` for `signature` """ - CACHE[signature] = value + if len(value) < MIN_SIZE_FOR_FILECACHE: + CACHE[signature] = value + else: + sighash = _store_in_file(signature, value) + CACHE[signature] = "file:%s" % sighash return _update_answer(value) + +def _hash(signature): + return hashlib.md5(signature.encode("utf-8")).hexdigest() + +def _store_in_file(signature, value): + """Store `value` for `signature` in cache file. + Return file name (signature_hash) as the result. + """ + + signature_hash = _hash(signature) + filename = os.path.join(LRU_CACHE, signature_hash) + if not os.path.exists(LRU_CACHE): + os.makedirs(LRU_CACHE) + with open(filename, "w") as f_cache: + f_cache.write(value) + return signature_hash + +def _read_from_file(signature, sighash=None): + """Read value for `signature` from cache file, + or return None if file is not found. + If `sighash` is specified, do not calculate file name + from signature, but use `sighash` instead. + """ + + signature_hash = sighash or _hash(signature) + filename = os.path.join(LRU_CACHE, signature_hash) + if not os.path.exists(filename): + return None + + with open(filename, "r") as f_cache: + return f_cache.read() diff --git a/lib/globals.py b/lib/globals.py index b24d791..0664d4a 100644 --- a/lib/globals.py +++ b/lib/globals.py @@ -14,6 +14,7 @@ from __future__ import print_function import logging import os +import re MYDIR = os.path.abspath(os.path.dirname(os.path.dirname('__file__'))) @@ -31,6 +32,7 @@ _LOGDIR = "/wttr.in/log" CACHEDIR = os.path.join(_DATADIR, "cache/wego/") IP2LCACHE = os.path.join(_DATADIR, "cache/ip2l/") PNG_CACHE = os.path.join(_DATADIR, "cache/png") +LRU_CACHE = os.path.join(_DATADIR, "cache/lru") LOG_FILE = os.path.join(_LOGDIR, 'main.log') From 0ac4790007601f1629875627079af7b1f9dac40d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 5 Apr 2020 11:51:36 +0200 Subject: [PATCH 15/73] lib/wttr_srv.py refactoring --- lib/wttr_srv.py | 312 ++++++++++++++++++++++++++++-------------------- 1 file changed, 182 insertions(+), 130 deletions(-) diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index 19c895a..f20b161 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -10,11 +10,12 @@ import os import time from flask import render_template, send_file, make_response -import format.png +import fmt.png + import parse_query from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS from buttons import add_buttons -from globals import get_help_file, log, \ +from globals import get_help_file, \ BASH_FUNCTION_FILE, TRANSLATION_FILE, LOG_FILE, \ NOT_FOUND_LOCATION, \ MALFORMED_RESPONSE_HTML_PAGE, \ @@ -22,7 +23,7 @@ from globals import get_help_file, log, \ MY_EXTERNAL_IP, QUERY_LIMITS from location import is_location_blocked, location_processing from limits import Limits -from wttr import get_wetter +from view.wttr import get_wetter from view.moon import get_moon from view.line import wttr_line @@ -52,10 +53,8 @@ def show_text_file(name, lang): .replace('SUPPORTED_LANGUAGES', ' '.join(SUPPORTED_LANGS)) return text -def client_ip_address(request): - """ - Return client ip address for `request`. - Flask related +def _client_ip_address(request): + """Return client ip address for flask `request`. """ if request.headers.getlist("X-PNG-Query-For"): @@ -111,25 +110,25 @@ def _parse_language_header(header): elif lang == 'en': yield None, lang_tuple[1] try: - return max(supported_langs(), key=lambda lang_tuple:lang_tuple[1])[0] + return max(supported_langs(), key=lambda lang_tuple: lang_tuple[1])[0] except ValueError: return None return _find_supported_language(_parse_accept_language(header)) -def get_answer_language_and_format(request): +def get_answer_language_and_view(request): """ Return preferred answer language based on domain name, query arguments and headers """ lang = None - fmt = None + view_name = None hostname = request.headers['Host'] if hostname != 'wttr.in' and hostname.endswith('.wttr.in'): lang = hostname[:-8] if lang == "v2": - fmt = "v2" + view_name = "v2" lang = None if 'lang' in request.args: @@ -139,7 +138,7 @@ def get_answer_language_and_format(request): if lang is None and header_accept_language: lang = _parse_language_header(header_accept_language) - return lang, fmt + return lang, view_name def get_output_format(request, query): """ @@ -150,16 +149,14 @@ def get_output_format(request, query): if 'format' in query: return False - # FIXME user_agent = request.headers.get('User-Agent', '').lower() if query.get('force-ansi'): return False html_output = not any(agent in user_agent for agent in PLAIN_TEXT_AGENTS) return html_output -def cyclic_location_selection(locations, period): - """ - Return one of `locations` (: separated list) +def _cyclic_location_selection(locations, period): + """Return one of `locations` (: separated list) basing on the current time and query interval `period` """ @@ -172,24 +169,167 @@ def cyclic_location_selection(locations, period): except ValueError: period = 1 - index = int(time.time())/period % len(locations) + index = int(time.time()/period) % len(locations) return locations[index] -def wttr(location, request): +def _response(parsed_query, query, fast_mode=False): + """Create response text based on `parsed_query` and `query` data. + If `fast_mode` is True, process only requests that can + be handled very fast (cached and static files). """ - Main rendering function, it processes incoming weather queries. - Depending on user agent it returns output in HTML or ANSI format. + + answer = None + cache_signature = cache.get_signature( + parsed_query["user_agent"], + parsed_query["request_url"], + parsed_query["ip_addr"], + parsed_query["lang"]) + answer = cache.get(cache_signature) + + if parsed_query['orig_location'] in PLAIN_TEXT_PAGES: + answer = show_text_file(parsed_query['orig_location'], parsed_query['lang']) + if parsed_query['html_output']: + answer = render_template('index.html', body=answer) + + if answer or fast_mode: + return answer + + # at this point, we could not handle the query fast, + # so we handle it with all available logic + + if parsed_query["view"] or 'format' in query: + response_text = wttr_line( + parsed_query['location'], + parsed_query['override_location_name'], + parsed_query['full_address'], + query, + parsed_query['lang'], + parsed_query['view']) + return cache.store(cache_signature, response_text) + + + if parsed_query.get('png_filename'): + options = { + 'ip_addr': parsed_query['ip_addr'], + 'lang': parsed_query['lang'], + 'location': parsed_query['location']} + options.update(query) + + cached_png_file = fmt.png.make_wttr_in_png( + parsed_query['png_filename'], options=options) + response = make_response(send_file( + cached_png_file, + attachment_filename=parsed_query['png_filename'], + mimetype='image/png')) + + for key, value in { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }.items(): + response.headers[key] = value + + return response + + orig_location = parsed_query['orig_location'] + if orig_location and \ + (orig_location.lower() == 'moon' or \ + orig_location.lower().startswith('moon@')): + output = get_moon( + parsed_query['orig_location'], + html=parsed_query['html_output'], + lang=parsed_query['lang'], + query=query) + else: + output = get_wetter( + parsed_query['location'], + parsed_query['ip_addr'], + html=parsed_query['html_output'], + lang=parsed_query['lang'], + query=query, + location_name=parsed_query['override_location_name'], + full_address=parsed_query['full_address'], + url=parsed_query['request_url'],) + + if query.get('days', '3') != '0' and not query.get('no-follow-line'): + if parsed_query['html_output']: + output = add_buttons(output) + else: + output += '\n' + get_message('FOLLOW_ME', parsed_query['lang']) + '\n' + + return cache.store(cache_signature, output) + # return output + +def parse_request(location, request, query, fast_mode=False): + """Parse request and provided extended information for the query, + including location data, language, output format, view, etc. Incoming data: - request.args - request.headers - request.remote_addr - request.referrer - request.query_string + + `location` location name extracted from the query url + `request.args` + `request.headers` + `request.remote_addr` + `request.referrer` + `request.query_string` + `query` parsed command line arguments + + Return: dictionary with parsed parameters + """ + + png_filename = None + if location is not None and location.lower().endswith(".png"): + png_filename = location + location = location[:-4] + if location and ':' in location and location[0] != ":": + location = _cyclic_location_selection(location, query.get('period', 1)) + + lang, _view = get_answer_language_and_view(request) + html_output = get_output_format(request, query) + + ip_addr = _client_ip_address(request) + parsed_query = { + 'ip_addr': ip_addr, + 'user_agent': request.headers.get('User-Agent', '').lower(), + 'lang': lang, + 'view': _view, + 'html_output': html_output, + 'orig_location': location, + 'location': location, + 'request_url': request.url, + } + + if not png_filename and not fast_mode: + location, override_location_name, full_address, country, query_source_location = \ + location_processing(location, ip_addr) + + us_ip = query_source_location[1] == 'United States' \ + and 'slack' not in parsed_query['user_agent'] + query = parse_query.metric_or_imperial(query, lang, us_ip=us_ip) + + if country and location != NOT_FOUND_LOCATION: + location = "%s,%s" % (location, country) + + parsed_query.update({ + 'location': location, + 'override_location_name': override_location_name, + 'full_address': full_address, + 'country': country, + 'query_source_location': query_source_location}) + + return parsed_query + + +def wttr(location, request): + """Main rendering function, it processes incoming weather queries, + and depending on the User-Agent string and other paramters of the query + it returns output in HTMLi, ANSI or other format. """ def _wrap_response(response_text, html_output): + if not isinstance(response_text, str): + return response_text response = make_response(response_text) response.mimetype = 'text/html' if html_output else 'text/plain' return response @@ -197,120 +337,32 @@ def wttr(location, request): if is_location_blocked(location): return "" - ip_addr = client_ip_address(request) - try: - LIMITS.check_ip(ip_addr) + LIMITS.check_ip(_client_ip_address(request)) except RuntimeError as exception: return str(exception) - png_filename = None - if location is not None and location.lower().endswith(".png"): - png_filename = location - location = location[:-4] - - 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() - # generating cache signature - cache_signature = cache.get_signature(user_agent, request.url, ip_addr, lang) - answer = cache.get(cache_signature) - if answer: - return _wrap_response(answer, html_output) + # first, we try to process the query as fast as possible + # (using the cache and static files), + # and only if "fast_mode" was unsuccessful, + # use the full track + parsed_query = parse_request(location, request, query, fast_mode=True) + response = _response(parsed_query, query, fast_mode=True) - if location in PLAIN_TEXT_PAGES: - help_ = show_text_file(location, lang) - if html_output: - return _wrap_response(render_template('index.html', body=help_), html_output) - return _wrap_response(help_, html_output) - - if location and ':' in location: - location = cyclic_location_selection(location, query.get('period', 1)) - - orig_location = location - - if not png_filename: - location, override_location_name, full_address, country, query_source_location = \ - location_processing(location, ip_addr) - - us_ip = query_source_location[1] == 'United States' and 'slack' not in user_agent - query = parse_query.metric_or_imperial(query, lang, us_ip=us_ip) - - # logging query - orig_location_utf8 = (orig_location or "") - location_utf8 = location - use_imperial = query.get('use_imperial', False) - log(" ".join(map(str, - [ip_addr, user_agent, orig_location_utf8, location_utf8, use_imperial, lang]))) - - if country and location != NOT_FOUND_LOCATION: - location = "%s,%s" % (location, country) - - # We are ready to return the answer try: - if fmt or 'format' in query: - response_text = wttr_line( - location, override_location_name, full_address, query, lang, fmt) - fmt = fmt or query.get('format') - response_text = cache.store(cache_signature, response_text) - - return _wrap_response( - response_text, - html_output) - - if png_filename: - options = { - 'ip_addr': ip_addr, - 'lang': lang, - 'location': location} - options.update(query) - - cached_png_file = format.png.make_wttr_in_png(png_filename, options=options) - response = make_response(send_file(cached_png_file, - attachment_filename=png_filename, - mimetype='image/png')) - for key, value in { - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0', - }.items(): - response.headers[key] = value - - # Trying to disable github caching - return response - - if orig_location and (orig_location.lower() == 'moon' or orig_location.lower().startswith('moon@')): - output = get_moon(orig_location, html=html_output, lang=lang, query=query) - else: - output = get_wetter(location, ip_addr, - html=html_output, - lang=lang, - query=query, - location_name=override_location_name, - full_address=full_address, - url=request.url, - ) - - if query.get('days', '3') != '0' and not query.get('no-follow-line'): - if html_output: - output = add_buttons(output) - else: - #output += '\n' + get_message('NEW_FEATURE', lang).encode('utf-8') - output += '\n' + get_message('FOLLOW_ME', lang) + '\n' - - return _wrap_response(output, html_output) - + if not response: + parsed_query = parse_request(location, request, query) + response = _response(parsed_query, query) + # pylint: disable=broad-except except Exception as exception: - # if 'Malformed response' in str(exception) \ - # or 'API key has reached calls per day allowed limit' in str(exception): - logging.error("Exception has occured", exc_info=1) - if html_output: - return _wrap_response(MALFORMED_RESPONSE_HTML_PAGE, html_output) - return _wrap_response(get_message('CAPACITY_LIMIT_REACHED', lang), html_output) - # logging.error("Exception has occured", exc_info=1) - # return "ERROR" + logging.error("Exception has occured", exc_info=1) + if parsed_query['html_output']: + response = MALFORMED_RESPONSE_HTML_PAGE + else: + response = get_message('CAPACITY_LIMIT_REACHED', parsed_query['lang']) + return _wrap_response(response, parsed_query['html_output']) if __name__ == "__main__": import doctest From a85cebaa157d0e7d9e068e29e485d0b815e9e4f8 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 5 Apr 2020 13:48:16 +0200 Subject: [PATCH 16/73] lib/cache.py: cache binary objects --- lib/cache.py | 41 +++++++++++++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/lib/cache.py b/lib/cache.py index b7c5179..7ba41e4 100644 --- a/lib/cache.py +++ b/lib/cache.py @@ -24,7 +24,7 @@ def _update_answer(answer): def _now_in_tz(timezone): return datetime.datetime.now(pytz.timezone(timezone)).strftime("%H:%M:%S%z") - if "%{{NOW(" in answer: + if isinstance(answer, str) and "%{{NOW(" in answer: answer = re.sub(r"%{{NOW\(([^}]*)\)}}", lambda x: _now_in_tz(x.group(1)), answer) return answer @@ -50,9 +50,8 @@ def get(signature): value = CACHE.get(signature) if value: - if value.startswith("file:"): - sighash = value[5:] - value = _read_from_file(signature, sighash=sighash) + if value.startswith("file:") or value.startswith("bfile:"): + value = _read_from_file(signature, sighash=value) if not value: return None return _update_answer(value) @@ -65,8 +64,7 @@ def store(signature, value): if len(value) < MIN_SIZE_FOR_FILECACHE: CACHE[signature] = value else: - sighash = _store_in_file(signature, value) - CACHE[signature] = "file:%s" % sighash + CACHE[signature] = _store_in_file(signature, value) return _update_answer(value) def _hash(signature): @@ -75,13 +73,24 @@ def _hash(signature): def _store_in_file(signature, value): """Store `value` for `signature` in cache file. Return file name (signature_hash) as the result. + `value` can be string as well as bytes. + Returned filename is prefixed with "file:" (for text files) + or "bfile:" (for binary files). """ signature_hash = _hash(signature) filename = os.path.join(LRU_CACHE, signature_hash) if not os.path.exists(LRU_CACHE): os.makedirs(LRU_CACHE) - with open(filename, "w") as f_cache: + + if isinstance(value, bytes): + mode = "wb" + signature_hash = "bfile:%s" % signature_hash + else: + mode = "w" + signature_hash = "file:%s" % signature_hash + + with open(filename, mode) as f_cache: f_cache.write(value) return signature_hash @@ -90,12 +99,24 @@ def _read_from_file(signature, sighash=None): or return None if file is not found. If `sighash` is specified, do not calculate file name from signature, but use `sighash` instead. + + `sigash` can be prefixed with "file:" (for text files) + or "bfile:" (for binary files). """ - signature_hash = sighash or _hash(signature) - filename = os.path.join(LRU_CACHE, signature_hash) + mode = "r" + if sighash: + if sighash.startswith("file:"): + sighash = sighash[5:] + elif sighash.startswith("bfile:"): + sighash = sighash[6:] + mode = "rb" + else: + sighash = _hash(signature) + + filename = os.path.join(LRU_CACHE, sighash) if not os.path.exists(filename): return None - with open(filename, "r") as f_cache: + with open(filename, mode) as f_cache: return f_cache.read() From f4c74632090a7d799cc07c8d414d1e4c4fee8e23 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 5 Apr 2020 13:51:54 +0200 Subject: [PATCH 17/73] lib/fmt/png.py: removed internal caching --- lib/fmt/png.py | 394 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 lib/fmt/png.py diff --git a/lib/fmt/png.py b/lib/fmt/png.py new file mode 100644 index 0000000..802525a --- /dev/null +++ b/lib/fmt/png.py @@ -0,0 +1,394 @@ +#!/usr/bin/python +#vim: encoding=utf-8 + +""" +This module is used to generate png-files for wttr.in queries. +The only exported function are: + +* render_ansi(png_file, text, options=None) +* make_wttr_in_png(png_file) + +`render_ansi` is the main function of the module, +which does rendering of stream into a PNG-file. + +The `make_wttr_in_png` function is a temporary helper function +which is a wraper around `render_ansi` and handles +such tasks as caching, name parsing etc. + +`make_wttr_in_png` parses `png_file` name (the shortname) and extracts +the weather query from it. It saves the weather report into the specified file. + +The module uses PIL for graphical tasks, and pyte for rendering +of ANSI stream into terminal representation. + +TODO: + + * remove make_wttr_in_png + * remove functions specific for wttr.in +""" + +from __future__ import print_function + +import sys +import io +import os +import re +import glob + +from PIL import Image, ImageFont, ImageDraw +import pyte.screens +import emoji +import grapheme + +import requests + +from . import unicodedata2 + +sys.path.insert(0, "..") +import constants +import parse_query +import globals + +COLS = 180 +ROWS = 100 +CHAR_WIDTH = 9 +CHAR_HEIGHT = 18 +FONT_SIZE = 15 +FONT_CAT = { + 'default': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + 'Cyrillic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + 'Greek': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + 'Arabic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + 'Hebrew': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", + 'Han': "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", + 'Hiragana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf", + 'Katakana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf", + 'Hangul': "/usr/share/fonts/truetype/lexi/LexiGulim.ttf", + 'Braille': "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf", + 'Emoji': "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf", +} + +# +# How to find font for non-standard scripts: +# +# $ fc-list :lang=ja +# +# GNU/Debian packages, that the fonts come from: +# +# * fonts-dejavu-core +# * fonts-wqy-zenhei (Han) +# * fonts-motoya-l-cedar (Hiragana/Katakana) +# * fonts-lexi-gulim (Hangul) +# * fonts-symbola (Braille/Emoji) +# + +def make_wttr_in_png(png_name, options=None): + """ The function saves the weather report in the file and returns None. + The weather query is coded in filename (in the shortname). + """ + + parsed = _parse_wttrin_png_name(png_name) + + # if location is MyLocation it should be overriden + # with autodetected location (from options) + if parsed.get('location', 'MyLocation') == 'MyLocation' \ + or not parsed.get('location', ''): + del parsed['location'] + + if options is not None: + for key, val in options.items(): + if key not in parsed: + parsed[key] = val + url = _make_wttrin_query(parsed) + + headers = {'X-PNG-Query-For': options.get('ip_addr', '1.1.1.1')} + text = requests.get(url, headers=headers).text + + return render_ansi(text, options=parsed) + +def render_ansi(text, options=None): + """Render `text` (terminal sequence) in a PNG file + paying attention to passed command line `options`. + + Return: file content + """ + + screen = pyte.screens.Screen(COLS, ROWS) + screen.set_mode(pyte.modes.LNM) + stream = pyte.Stream(screen) + + text, graphemes = _fix_graphemes(text) + stream.feed(text) + + buf = sorted(screen.buffer.items(), key=lambda x: x[0]) + buf = [[x[1] for x in sorted(line[1].items(), key=lambda x: x[0])] for line in buf] + + return _gen_term(buf, graphemes, options=options) + +def _color_mapping(color): + """Convert pyte color to PIL color + + Return: tuple of color values (R,G,B) + """ + + if color == 'default': + return 'lightgray' + if color in ['green', 'black', 'cyan', 'blue', 'brown']: + return color + try: + return ( + int(color[0:2], 16), + int(color[2:4], 16), + int(color[4:6], 16)) + except (ValueError, IndexError): + # if we do not know this color and it can not be decoded as RGB, + # print it and return it as it is (will be displayed as black) + # print color + return color + return color + +def _strip_buf(buf): + """Strips empty spaces from behind and from the right side. + (from the right side is not yet implemented) + """ + + def empty_line(line): + "Returns True if the line consists from spaces" + return all(x.data == ' ' for x in line) + + def line_len(line): + "Returns len of the line excluding spaces from the right" + + last_pos = len(line) + while last_pos > 0 and line[last_pos-1].data == ' ': + last_pos -= 1 + return last_pos + + number_of_lines = 0 + for line in buf[::-1]: + if not empty_line(line): + break + number_of_lines += 1 + + if number_of_lines: + buf = buf[:-number_of_lines] + + max_len = max(line_len(x) for x in buf) + buf = [line[:max_len] for line in buf] + + return buf + +def _script_category(char): + """Returns category of a Unicode character + + Possible values: + default, Cyrillic, Greek, Han, Hiragana + """ + + if char in emoji.UNICODE_EMOJI: + return "Emoji" + + cat = unicodedata2.script_cat(char)[0] + if char == u':': + return 'Han' + if cat in ['Latin', 'Common']: + return 'default' + return cat + +def _load_emojilib(): + """Load known emojis from a directory, and return dictionary + of PIL Image objects correspodent to the loaded emojis. + Each emoji is resized to the CHAR_HEIGHT size. + """ + + emojilib = {} + for filename in glob.glob("share/emoji/*.png"): + character = os.path.basename(filename)[:-4] + emojilib[character] = \ + Image.open(filename).resize((CHAR_HEIGHT, CHAR_HEIGHT)) + return emojilib + +def _gen_term(buf, graphemes, options=None): + """Renders rendered pyte buffer `buf` and list of workaround `graphemes` + to a PNG file, and return its content + """ + + if not options: + options = {} + + current_grapheme = 0 + + buf = _strip_buf(buf) + cols = max(len(x) for x in buf) + rows = len(buf) + + image = Image.new('RGB', (cols * CHAR_WIDTH, rows * CHAR_HEIGHT)) + + buf = buf[-ROWS:] + + draw = ImageDraw.Draw(image) + font = {} + for cat in FONT_CAT: + font[cat] = ImageFont.truetype(FONT_CAT[cat], FONT_SIZE) + + emojilib = _load_emojilib() + + x_pos = 0 + y_pos = 0 + for line in buf: + x_pos = 0 + for char in line: + current_color = _color_mapping(char.fg) + if char.bg != 'default': + draw.rectangle( + ((x_pos, y_pos), + (x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)), + fill=_color_mapping(char.bg)) + + if char.data == "!": + data = graphemes[current_grapheme] + current_grapheme += 1 + else: + data = char.data + + if data: + cat = _script_category(data[0]) + if cat not in font: + globals.log("Unknown font category: %s" % cat) + if cat == 'Emoji' and emojilib.get(data): + image.paste(emojilib.get(data), (x_pos, y_pos)) + else: + draw.text( + (x_pos, y_pos), + data, + font=font.get(cat, font.get('default')), + fill=current_color) + + x_pos += CHAR_WIDTH * constants.WEATHER_SYMBOL_WIDTH_VTE.get(data, 1) + y_pos += CHAR_HEIGHT + #sys.stdout.write('\n') + + if 'transparency' in options: + transparency = options.get('transparency', '255') + try: + transparency = int(transparency) + except ValueError: + transparency = 255 + + if transparency < 0: + transparency = 0 + + if transparency > 255: + transparency = 255 + + image = image.convert("RGBA") + datas = image.getdata() + + new_data = [] + for item in datas: + new_item = tuple(list(item[:3]) + [transparency]) + new_data.append(new_item) + + image.putdata(new_data) + + img_bytes = io.BytesIO() + image.save(img_bytes, format="png") + return img_bytes.getvalue() + +def _fix_graphemes(text): + """ + Extract long graphemes sequences that can't be handled + by pyte correctly because of the bug pyte#131. + Graphemes are omited and replaced with placeholders, + and returned as a list. + + Return: + text_without_graphemes, graphemes + """ + + output = "" + graphemes = [] + + for gra in grapheme.graphemes(text): + if len(gra) > 1: + character = "!" + graphemes.append(gra) + else: + character = gra + output += character + + return output, graphemes + + +# +# wttr.in related functions +# + +def _parse_wttrin_png_name(name): + """ + Parse the PNG filename and return the result as a dictionary. + For example: + input = City_200x_lang=ru.png + output = { + "lang": "ru", + "width": "200", + "filetype": "png", + "location": "City" + } + """ + + parsed = {} + to_be_parsed = {} + + if name.lower()[-4:] == '.png': + parsed['filetype'] = 'png' + name = name[:-4] + + parts = name.split('_') + parsed['location'] = parts[0] + + for part in parts[1:]: + if re.match('(?:[0-9]+)x', part): + parsed['width'] = part[:-1] + elif re.match('x(?:[0-9]+)', part): + parsed['height'] = part[1:] + elif re.match(part, '(?:[0-9]+)x(?:[0-9]+)'): + parsed['width'], parsed['height'] = part.split('x', 1) + elif '=' in part: + arg, val = part.split('=', 1) + to_be_parsed[arg] = val + else: + to_be_parsed[part] = '' + + parsed.update(parse_query.parse_query(to_be_parsed)) + + return parsed + +def _make_wttrin_query(parsed): + """Convert parsed data into query name + """ + + for key in ['width', 'height', 'filetype']: + if key in parsed: + del parsed[key] + + location = parsed['location'] + del parsed['location'] + + args = [] + if 'options' in parsed: + args = [parsed['options']] + del parsed['options'] + else: + args = [] + + for key, val in parsed.items(): + args.append('%s=%s' % (key, val)) + + args.append('filetype=png') + + url = "http://wttr.in/%s" % location + if args != []: + url += "?%s" % ("&".join(args)) + + return url From 8855e494cd93d397fee9d1b29613289944ba9ef2 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 5 Apr 2020 13:52:25 +0200 Subject: [PATCH 18/73] cache png using cache.py --- lib/{format => fmt}/__init__.py | 0 lib/{format => fmt}/unicodedata2.py | 0 lib/format/png.py | 404 ---------------------------- lib/view/wttr.py | 2 +- lib/wttr_srv.py | 99 +++---- 5 files changed, 55 insertions(+), 450 deletions(-) rename lib/{format => fmt}/__init__.py (100%) rename lib/{format => fmt}/unicodedata2.py (100%) delete mode 100644 lib/format/png.py diff --git a/lib/format/__init__.py b/lib/fmt/__init__.py similarity index 100% rename from lib/format/__init__.py rename to lib/fmt/__init__.py diff --git a/lib/format/unicodedata2.py b/lib/fmt/unicodedata2.py similarity index 100% rename from lib/format/unicodedata2.py rename to lib/fmt/unicodedata2.py diff --git a/lib/format/png.py b/lib/format/png.py deleted file mode 100644 index 58465a3..0000000 --- a/lib/format/png.py +++ /dev/null @@ -1,404 +0,0 @@ -#!/usr/bin/python -#vim: encoding=utf-8 - -""" -This module is used to generate png-files for wttr.in queries. -The only exported function are: - -* render_ansi(png_file, text, options=None) -* make_wttr_in_png(png_file) - -`render_ansi` is the main function of the module, -which does rendering of stream into a PNG-file. - -The `make_wttr_in_png` function is a temporary helper function -which is a wraper around `render_ansi` and handles -such tasks as caching, name parsing etc. - -`make_wttr_in_png` parses `png_file` name (the shortname) and extracts -the weather query from it. It saves the weather report into the specified file. - -The module uses PIL for graphical tasks, and pyte for rendering -of ANSI stream into terminal representation. - -TODO: - - * remove make_wttr_in_png - * remove functions specific for wttr.in -""" - -from __future__ import print_function - -import sys -import os -import re -import time -import glob - -from PIL import Image, ImageFont, ImageDraw -import pyte.screens -import emoji -import grapheme - -import requests - -from . import unicodedata2 - -sys.path.insert(0, "..") -import constants -import parse_query -import globals - -COLS = 180 -ROWS = 100 -CHAR_WIDTH = 9 -CHAR_HEIGHT = 18 -FONT_SIZE = 15 -FONT_CAT = { - 'default': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", - 'Cyrillic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", - 'Greek': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", - 'Arabic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", - 'Hebrew': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", - 'Han': "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc", - 'Hiragana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf", - 'Katakana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf", - 'Hangul': "/usr/share/fonts/truetype/lexi/LexiGulim.ttf", - 'Braille': "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf", - 'Emoji': "/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf", -} - -# -# How to find font for non-standard scripts: -# -# $ fc-list :lang=ja -# -# GNU/Debian packages, that the fonts come from: -# -# * fonts-dejavu-core -# * fonts-wqy-zenhei (Han) -# * fonts-motoya-l-cedar (Hiragana/Katakana) -# * fonts-lexi-gulim (Hangul) -# * fonts-symbola (Braille/Emoji) -# - -def make_wttr_in_png(png_name, options=None): - """ The function saves the weather report in the file and returns None. - The weather query is coded in filename (in the shortname). - """ - - parsed = _parse_wttrin_png_name(png_name) - - # if location is MyLocation it should be overriden - # with autodetected location (from options) - if parsed.get('location', 'MyLocation') == 'MyLocation' or not parsed.get('location', ''): - del parsed['location'] - - if options is not None: - for key, val in options.items(): - if key not in parsed: - parsed[key] = val - url = _make_wttrin_query(parsed) - - timestamp = time.strftime("%Y%m%d%H", time.localtime()) - cached_basename = url[14:].replace('/', '_') - - cached_png_file = "%s/%s/%s.png" % (globals.PNG_CACHE, timestamp, cached_basename) - - dirname = os.path.dirname(cached_png_file) - if not os.path.exists(dirname): - os.makedirs(dirname) - - if os.path.exists(cached_png_file): - return cached_png_file - - headers = {'X-PNG-Query-For': options.get('ip_addr', '1.1.1.1')} - text = requests.get(url, headers=headers).text - - render_ansi(cached_png_file, text, options=parsed) - - return cached_png_file - -def render_ansi(png_file, text, options=None): - """Render `text` (terminal sequence) in `png_file` - paying attention to passed command line `options` - """ - - screen = pyte.screens.Screen(COLS, ROWS) - screen.set_mode(pyte.modes.LNM) - stream = pyte.Stream(screen) - - text, graphemes = _fix_graphemes(text) - stream.feed(text) - - buf = sorted(screen.buffer.items(), key=lambda x: x[0]) - buf = [[x[1] for x in sorted(line[1].items(), key=lambda x: x[0])] for line in buf] - - _gen_term(png_file, buf, graphemes, options=options) - -def _color_mapping(color): - """Convert pyte color to PIL color - - Return: tuple of color values (R,G,B) - """ - - if color == 'default': - return 'lightgray' - if color in ['green', 'black', 'cyan', 'blue', 'brown']: - return color - try: - return ( - int(color[0:2], 16), - int(color[2:4], 16), - int(color[4:6], 16)) - except (ValueError, IndexError): - # if we do not know this color and it can not be decoded as RGB, - # print it and return it as it is (will be displayed as black) - # print color - return color - return color - -def _strip_buf(buf): - """Strips empty spaces from behind and from the right side. - (from the right side is not yet implemented) - """ - - def empty_line(line): - "Returns True if the line consists from spaces" - return all(x.data == ' ' for x in line) - - def line_len(line): - "Returns len of the line excluding spaces from the right" - - last_pos = len(line) - while last_pos > 0 and line[last_pos-1].data == ' ': - last_pos -= 1 - return last_pos - - number_of_lines = 0 - for line in buf[::-1]: - if not empty_line(line): - break - number_of_lines += 1 - - if number_of_lines: - buf = buf[:-number_of_lines] - - max_len = max(line_len(x) for x in buf) - buf = [line[:max_len] for line in buf] - - return buf - -def _script_category(char): - """Returns category of a Unicode character - - Possible values: - default, Cyrillic, Greek, Han, Hiragana - """ - - if char in emoji.UNICODE_EMOJI: - return "Emoji" - - cat = unicodedata2.script_cat(char)[0] - if char == u':': - return 'Han' - if cat in ['Latin', 'Common']: - return 'default' - return cat - -def _load_emojilib(): - """Load known emojis from a directory, and return dictionary - of PIL Image objects correspodent to the loaded emojis. - Each emoji is resized to the CHAR_HEIGHT size. - """ - - emojilib = {} - for filename in glob.glob("share/emoji/*.png"): - character = os.path.basename(filename)[:-4] - emojilib[character] = \ - Image.open(filename).resize((CHAR_HEIGHT, CHAR_HEIGHT)) - return emojilib - -def _gen_term(filename, buf, graphemes, options=None): - """Renders rendered pyte buffer `buf` and list of workaround `graphemes` - to a PNG file `filename`. - """ - - if not options: - options = {} - - current_grapheme = 0 - - buf = _strip_buf(buf) - cols = max(len(x) for x in buf) - rows = len(buf) - - image = Image.new('RGB', (cols * CHAR_WIDTH, rows * CHAR_HEIGHT)) - - buf = buf[-ROWS:] - - draw = ImageDraw.Draw(image) - font = {} - for cat in FONT_CAT: - font[cat] = ImageFont.truetype(FONT_CAT[cat], FONT_SIZE) - - emojilib = _load_emojilib() - - x_pos = 0 - y_pos = 0 - for line in buf: - x_pos = 0 - for char in line: - current_color = _color_mapping(char.fg) - if char.bg != 'default': - draw.rectangle( - ((x_pos, y_pos), - (x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)), - fill=_color_mapping(char.bg)) - - if char.data == "!": - data = graphemes[current_grapheme] - current_grapheme += 1 - else: - data = char.data - - if data: - cat = _script_category(data[0]) - if cat not in font: - globals.log("Unknown font category: %s" % cat) - if cat == 'Emoji' and emojilib.get(data): - image.paste(emojilib.get(data), (x_pos, y_pos)) - else: - draw.text( - (x_pos, y_pos), - data, - font=font.get(cat, font.get('default')), - fill=current_color) - - x_pos += CHAR_WIDTH * constants.WEATHER_SYMBOL_WIDTH_VTE.get(data, 1) - y_pos += CHAR_HEIGHT - #sys.stdout.write('\n') - - if 'transparency' in options: - transparency = options.get('transparency', '255') - try: - transparency = int(transparency) - except ValueError: - transparency = 255 - - if transparency < 0: - transparency = 0 - - if transparency > 255: - transparency = 255 - - image = image.convert("RGBA") - datas = image.getdata() - - new_data = [] - for item in datas: - new_item = tuple(list(item[:3]) + [transparency]) - new_data.append(new_item) - - image.putdata(new_data) - - - image.save(filename) - -def _fix_graphemes(text): - """ - Extract long graphemes sequences that can't be handled - by pyte correctly because of the bug pyte#131. - Graphemes are omited and replaced with placeholders, - and returned as a list. - - Return: - text_without_graphemes, graphemes - """ - - output = "" - graphemes = [] - - for gra in grapheme.graphemes(text): - if len(gra) > 1: - character = "!" - graphemes.append(gra) - else: - character = gra - output += character - - return output, graphemes - - -# -# wttr.in related functions -# - -def _parse_wttrin_png_name(name): - """ - Parse the PNG filename and return the result as a dictionary. - For example: - input = City_200x_lang=ru.png - output = { - "lang": "ru", - "width": "200", - "filetype": "png", - "location": "City" - } - """ - - parsed = {} - to_be_parsed = {} - - if name.lower()[-4:] == '.png': - parsed['filetype'] = 'png' - name = name[:-4] - - parts = name.split('_') - parsed['location'] = parts[0] - - for part in parts[1:]: - if re.match('(?:[0-9]+)x', part): - parsed['width'] = part[:-1] - elif re.match('x(?:[0-9]+)', part): - parsed['height'] = part[1:] - elif re.match(part, '(?:[0-9]+)x(?:[0-9]+)'): - parsed['width'], parsed['height'] = part.split('x', 1) - elif '=' in part: - arg, val = part.split('=', 1) - to_be_parsed[arg] = val - else: - to_be_parsed[part] = '' - - parsed.update(parse_query.parse_query(to_be_parsed)) - - return parsed - -def _make_wttrin_query(parsed): - """Convert parsed data into query name - """ - - for key in ['width', 'height', 'filetype']: - if key in parsed: - del parsed[key] - - location = parsed['location'] - del parsed['location'] - - args = [] - if 'options' in parsed: - args = [parsed['options']] - del parsed['options'] - else: - args = [] - - for key, val in parsed.items(): - args.append('%s=%s' % (key, val)) - - args.append('filetype=png') - - url = "http://wttr.in/%s" % location - if args != []: - url += "?%s" % ("&".join(args)) - - return url diff --git a/lib/view/wttr.py b/lib/view/wttr.py index e8eeb70..78e52d2 100644 --- a/lib/view/wttr.py +++ b/lib/view/wttr.py @@ -16,7 +16,7 @@ import time sys.path.insert(0, "..") from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS from globals import WEGO, CACHEDIR, \ - NOT_FOUND_LOCATION, DEFAULT_LOCATION, TEST_FILE, \ + NOT_FOUND_LOCATION, DEFAULT_LOCATION, TEST_FILE, ANSI2HTML, \ log, error, remove_ansi def _is_invalid_location(location): diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index f20b161..d6a3078 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -6,6 +6,7 @@ Main wttr.in rendering function implementation """ import logging +import io import os import time from flask import render_template, send_file, make_response @@ -208,7 +209,6 @@ def _response(parsed_query, query, fast_mode=False): parsed_query['view']) return cache.store(cache_signature, response_text) - if parsed_query.get('png_filename'): options = { 'ip_addr': parsed_query['ip_addr'], @@ -216,50 +216,36 @@ def _response(parsed_query, query, fast_mode=False): 'location': parsed_query['location']} options.update(query) - cached_png_file = fmt.png.make_wttr_in_png( + output = fmt.png.make_wttr_in_png( parsed_query['png_filename'], options=options) - response = make_response(send_file( - cached_png_file, - attachment_filename=parsed_query['png_filename'], - mimetype='image/png')) - - for key, value in { - 'Cache-Control': 'no-cache, no-store, must-revalidate', - 'Pragma': 'no-cache', - 'Expires': '0', - }.items(): - response.headers[key] = value - - return response - - orig_location = parsed_query['orig_location'] - if orig_location and \ - (orig_location.lower() == 'moon' or \ - orig_location.lower().startswith('moon@')): - output = get_moon( - parsed_query['orig_location'], - html=parsed_query['html_output'], - lang=parsed_query['lang'], - query=query) else: - output = get_wetter( - parsed_query['location'], - parsed_query['ip_addr'], - html=parsed_query['html_output'], - lang=parsed_query['lang'], - query=query, - location_name=parsed_query['override_location_name'], - full_address=parsed_query['full_address'], - url=parsed_query['request_url'],) - - if query.get('days', '3') != '0' and not query.get('no-follow-line'): - if parsed_query['html_output']: - output = add_buttons(output) + orig_location = parsed_query['orig_location'] + if orig_location and \ + (orig_location.lower() == 'moon' or \ + orig_location.lower().startswith('moon@')): + output = get_moon( + parsed_query['orig_location'], + html=parsed_query['html_output'], + lang=parsed_query['lang'], + query=query) else: - output += '\n' + get_message('FOLLOW_ME', parsed_query['lang']) + '\n' + output = get_wetter( + parsed_query['location'], + parsed_query['ip_addr'], + html=parsed_query['html_output'], + lang=parsed_query['lang'], + query=query, + location_name=parsed_query['override_location_name'], + full_address=parsed_query['full_address'], + url=parsed_query['request_url'],) + + if query.get('days', '3') != '0' and not query.get('no-follow-line'): + if parsed_query['html_output']: + output = add_buttons(output) + else: + output += '\n' + get_message('FOLLOW_ME', parsed_query['lang']) + '\n' return cache.store(cache_signature, output) - # return output def parse_request(location, request, query, fast_mode=False): """Parse request and provided extended information for the query, @@ -300,6 +286,9 @@ def parse_request(location, request, query, fast_mode=False): 'request_url': request.url, } + if png_filename: + parsed_query["png_filename"] = png_filename + if not png_filename and not fast_mode: location, override_location_name, full_address, country, query_source_location = \ location_processing(location, ip_addr) @@ -327,11 +316,26 @@ def wttr(location, request): it returns output in HTMLi, ANSI or other format. """ - def _wrap_response(response_text, html_output): - if not isinstance(response_text, str): + def _wrap_response(response_text, html_output, png_filename=None): + if not isinstance(response_text, str) and \ + not isinstance(response_text, bytes): return response_text - response = make_response(response_text) - response.mimetype = 'text/html' if html_output else 'text/plain' + + if png_filename: + response = make_response(send_file( + io.BytesIO(response_text), + attachment_filename=png_filename, + mimetype='image/png')) + + for key, value in { + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0', + }.items(): + response.headers[key] = value + else: + response = make_response(response_text) + response.mimetype = 'text/html' if html_output else 'text/plain' return response if is_location_blocked(location): @@ -362,7 +366,12 @@ def wttr(location, request): response = MALFORMED_RESPONSE_HTML_PAGE else: response = get_message('CAPACITY_LIMIT_REACHED', parsed_query['lang']) - return _wrap_response(response, parsed_query['html_output']) + + # if exception is occured, we return not a png file but text + del parsed_query["png_filename"] + return _wrap_response( + response, parsed_query['html_output'], + png_filename=parsed_query.get('png_filename')) if __name__ == "__main__": import doctest From 7565baaa8504a1fa54c1d26d2a3bf86e2ed50404 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 5 Apr 2020 14:18:53 +0200 Subject: [PATCH 19/73] use (query, parsed_query) args for views --- lib/view/line.py | 7 ++++++- lib/view/moon.py | 7 ++++++- lib/view/wttr.py | 10 +++++++++- lib/wttr_srv.py | 30 +++++------------------------- 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/lib/view/line.py b/lib/view/line.py index 1306051..1f020fd 100644 --- a/lib/view/line.py +++ b/lib/view/line.py @@ -260,11 +260,16 @@ def format_weather_data(format_line, location, override_location, full_address, output = render_line(format_line, current_condition, query) return output -def wttr_line(location, override_location_name, full_address, query, lang, fmt): +def wttr_line(query, parsed_query): """ Return 1line weather information for `location` in format `line_format` """ + location = parsed_query['location'] + override_location_name = parsed_query['override_location_name'] + full_address = parsed_query['full_address'] + lang = parsed_query['lang'] + fmt = parsed_query['view'] format_line = query.get('format', fmt or '') diff --git a/lib/view/moon.py b/lib/view/moon.py index 7d34713..2aa5da4 100644 --- a/lib/view/moon.py +++ b/lib/view/moon.py @@ -10,7 +10,12 @@ import constants import parse_query import globals -def get_moon(location, html=False, lang=None, query=None): +def get_moon(query, parsed_query): + + location = parsed_query['orig_location'] + html = parsed_query['html_output'] + lang = parsed_query['lang'] + if query is None: query = {} diff --git a/lib/view/wttr.py b/lib/view/wttr.py index 78e52d2..150d543 100644 --- a/lib/view/wttr.py +++ b/lib/view/wttr.py @@ -23,7 +23,15 @@ def _is_invalid_location(location): if '.png' in location: return True -def get_wetter(location, ip, html=False, lang=None, query=None, location_name=None, full_address=None, url=None): +def get_wetter(query, parsed_query): + + location = parsed_query['location'] + ip = parsed_query['ip_addr'] + html = parsed_query['html_output'] + lang = parsed_query['lang'] + location_name = parsed_query['override_location_name'] + full_address = parsed_query['full_address'] + url = parsed_query['request_url'] local_url = url local_location = location diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index d6a3078..8345f7d 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -200,13 +200,7 @@ def _response(parsed_query, query, fast_mode=False): # so we handle it with all available logic if parsed_query["view"] or 'format' in query: - response_text = wttr_line( - parsed_query['location'], - parsed_query['override_location_name'], - parsed_query['full_address'], - query, - parsed_query['lang'], - parsed_query['view']) + response_text = wttr_line(query, parsed_query) return cache.store(cache_signature, response_text) if parsed_query.get('png_filename'): @@ -219,25 +213,11 @@ def _response(parsed_query, query, fast_mode=False): output = fmt.png.make_wttr_in_png( parsed_query['png_filename'], options=options) else: - orig_location = parsed_query['orig_location'] - if orig_location and \ - (orig_location.lower() == 'moon' or \ - orig_location.lower().startswith('moon@')): - output = get_moon( - parsed_query['orig_location'], - html=parsed_query['html_output'], - lang=parsed_query['lang'], - query=query) + loc = (parsed_query['orig_location'] or "").lower() + if loc == 'moon' or loc.startswith('moon@'): + output = get_moon(query, parsed_query) else: - output = get_wetter( - parsed_query['location'], - parsed_query['ip_addr'], - html=parsed_query['html_output'], - lang=parsed_query['lang'], - query=query, - location_name=parsed_query['override_location_name'], - full_address=parsed_query['full_address'], - url=parsed_query['request_url'],) + output = get_wetter(query, parsed_query) if query.get('days', '3') != '0' and not query.get('no-follow-line'): if parsed_query['html_output']: From d9ebb8a0260d2c5b5cd8b9defffe05c25433a340 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 24 Apr 2020 00:16:11 +0200 Subject: [PATCH 20/73] initial implementation of go web frontend --- cmd/srv.go | 145 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 cmd/srv.go diff --git a/cmd/srv.go b/cmd/srv.go new file mode 100644 index 0000000..371cf21 --- /dev/null +++ b/cmd/srv.go @@ -0,0 +1,145 @@ +package main + +import ( + "context" + "fmt" + "io/ioutil" + "log" + "net" + "net/http" + "time" + + "github.com/hashicorp/golang-lru" +) + +var lruCache *lru.Cache + +type ResponseWithHeader struct { + Body []byte + Header http.Header + StatusCode int // e.g. 200 + +} + +func init() { + var err error + lruCache, err = lru.New(12800) + if err != nil { + panic(err) + } + + dialer := &net.Dialer{ + Timeout: 30 * time.Second, + KeepAlive: 30 * time.Second, + DualStack: true, + } + + http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) { + addr = "127.0.0.1:8002" + return dialer.DialContext(ctx, network, addr) + } + +} + +func readUserIP(r *http.Request) string { + IPAddress := r.Header.Get("X-Real-Ip") + if IPAddress == "" { + IPAddress = r.Header.Get("X-Forwarded-For") + } + if IPAddress == "" { + IPAddress = r.RemoteAddr + var err error + IPAddress, _, err = net.SplitHostPort(IPAddress) + if err != nil { + fmt.Printf("userip: %q is not IP:port\n", IPAddress) + } + } + return IPAddress +} + +// implementation of the cache.get_signature of original wttr.in +func findCacheDigest(req *http.Request) string { + + userAgent := req.Header.Get("User-Agent") + + queryHost := req.Host + queryString := req.RequestURI + + clientIpAddress := readUserIP(req) + + lang := req.Header.Get("Accept-Language") + + now := time.Now() + secs := now.Unix() + timestamp := secs / 1000 + + return fmt.Sprintf("%s:%s%s:%s:%s:%d", userAgent, queryHost, queryString, clientIpAddress, lang, timestamp) +} + +func get(req *http.Request) ResponseWithHeader { + + client := &http.Client{} + + queryURL := fmt.Sprintf("http://%s%s", req.Host, req.RequestURI) + + proxyReq, err := http.NewRequest(req.Method, queryURL, req.Body) + if err != nil { + // handle error + } + + // proxyReq.Header.Set("Host", req.Host) + // proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr) + + for header, values := range req.Header { + for _, value := range values { + proxyReq.Header.Add(header, value) + } + } + + res, err := client.Do(proxyReq) + + if err != nil { + panic(err) + } + + body, err := ioutil.ReadAll(res.Body) + + return ResponseWithHeader{ + Body: body, + Header: res.Header, + StatusCode: res.StatusCode, + } +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +func main() { + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + var response ResponseWithHeader + + cacheDigest := findCacheDigest(r) + cacheBody, ok := lruCache.Get(cacheDigest) + if ok { + response = cacheBody.(ResponseWithHeader) + } else { + fmt.Println(cacheDigest) + response = get(r) + if response.StatusCode == 200 { + lruCache.Add(cacheDigest, response) + } + } + copyHeader(w.Header(), response.Header) + w.WriteHeader(response.StatusCode) + w.Write(response.Body) + }) + + log.Fatal(http.ListenAndServe(":8081", nil)) + +} From 7a8327c13f7dfa27faba43851ba634e274955480 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 24 Apr 2020 21:58:47 +0200 Subject: [PATCH 21/73] moved parse_wttrin_png_name() to lib/parse_query.py --- lib/parse_query.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/lib/parse_query.py b/lib/parse_query.py index 0cdf958..120e6b8 100644 --- a/lib/parse_query.py +++ b/lib/parse_query.py @@ -1,3 +1,5 @@ +import re + def metric_or_imperial(query, lang, us_ip=False): """ """ @@ -86,3 +88,42 @@ def parse_query(args): return result +def parse_wttrin_png_name(name): + """ + Parse the PNG filename and return the result as a dictionary. + For example: + input = City_200x_lang=ru.png + output = { + "lang": "ru", + "width": "200", + "filetype": "png", + "location": "City" + } + """ + + parsed = {} + to_be_parsed = {} + + if name.lower()[-4:] == '.png': + parsed['filetype'] = 'png' + name = name[:-4] + + parts = name.split('_') + parsed['location'] = parts[0] + + for part in parts[1:]: + if re.match('(?:[0-9]+)x', part): + parsed['width'] = part[:-1] + elif re.match('x(?:[0-9]+)', part): + parsed['height'] = part[1:] + elif re.match(part, '(?:[0-9]+)x(?:[0-9]+)'): + parsed['width'], parsed['height'] = part.split('x', 1) + elif '=' in part: + arg, val = part.split('=', 1) + to_be_parsed[arg] = val + else: + to_be_parsed[part] = '' + + parsed.update(parse_query(to_be_parsed)) + + return parsed From bbd031cb7b7db67639b506a2d6048870f0aeeb58 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 24 Apr 2020 22:02:14 +0200 Subject: [PATCH 22/73] removed make_wttr_in_png() and other wttr.in specific functions --- lib/fmt/png.py | 125 +++---------------------------------------------- 1 file changed, 7 insertions(+), 118 deletions(-) diff --git a/lib/fmt/png.py b/lib/fmt/png.py index 802525a..1449bd6 100644 --- a/lib/fmt/png.py +++ b/lib/fmt/png.py @@ -1,30 +1,18 @@ #!/usr/bin/python #vim: encoding=utf-8 +# pylint: disable=wrong-import-position,wrong-import-order,redefined-builtin """ This module is used to generate png-files for wttr.in queries. -The only exported function are: +The only exported function is: * render_ansi(png_file, text, options=None) -* make_wttr_in_png(png_file) `render_ansi` is the main function of the module, which does rendering of stream into a PNG-file. -The `make_wttr_in_png` function is a temporary helper function -which is a wraper around `render_ansi` and handles -such tasks as caching, name parsing etc. - -`make_wttr_in_png` parses `png_file` name (the shortname) and extracts -the weather query from it. It saves the weather report into the specified file. - The module uses PIL for graphical tasks, and pyte for rendering of ANSI stream into terminal representation. - -TODO: - - * remove make_wttr_in_png - * remove functions specific for wttr.in """ from __future__ import print_function @@ -32,7 +20,6 @@ from __future__ import print_function import sys import io import os -import re import glob from PIL import Image, ImageFont, ImageDraw @@ -40,13 +27,10 @@ import pyte.screens import emoji import grapheme -import requests - from . import unicodedata2 sys.path.insert(0, "..") import constants -import parse_query import globals COLS = 180 @@ -82,30 +66,6 @@ FONT_CAT = { # * fonts-symbola (Braille/Emoji) # -def make_wttr_in_png(png_name, options=None): - """ The function saves the weather report in the file and returns None. - The weather query is coded in filename (in the shortname). - """ - - parsed = _parse_wttrin_png_name(png_name) - - # if location is MyLocation it should be overriden - # with autodetected location (from options) - if parsed.get('location', 'MyLocation') == 'MyLocation' \ - or not parsed.get('location', ''): - del parsed['location'] - - if options is not None: - for key, val in options.items(): - if key not in parsed: - parsed[key] = val - url = _make_wttrin_query(parsed) - - headers = {'X-PNG-Query-For': options.get('ip_addr', '1.1.1.1')} - text = requests.get(url, headers=headers).text - - return render_ansi(text, options=parsed) - def render_ansi(text, options=None): """Render `text` (terminal sequence) in a PNG file paying attention to passed command line `options`. @@ -208,6 +168,7 @@ def _load_emojilib(): Image.open(filename).resize((CHAR_HEIGHT, CHAR_HEIGHT)) return emojilib +# pylint: disable=too-many-locals,too-many-branches,too-many-statements def _gen_term(buf, graphemes, options=None): """Renders rendered pyte buffer `buf` and list of workaround `graphemes` to a PNG file, and return its content @@ -246,7 +207,10 @@ def _gen_term(buf, graphemes, options=None): fill=_color_mapping(char.bg)) if char.data == "!": - data = graphemes[current_grapheme] + try: + data = graphemes[current_grapheme] + except IndexError: + pass current_grapheme += 1 else: data = char.data @@ -266,7 +230,6 @@ def _gen_term(buf, graphemes, options=None): x_pos += CHAR_WIDTH * constants.WEATHER_SYMBOL_WIDTH_VTE.get(data, 1) y_pos += CHAR_HEIGHT - #sys.stdout.write('\n') if 'transparency' in options: transparency = options.get('transparency', '255') @@ -318,77 +281,3 @@ def _fix_graphemes(text): output += character return output, graphemes - - -# -# wttr.in related functions -# - -def _parse_wttrin_png_name(name): - """ - Parse the PNG filename and return the result as a dictionary. - For example: - input = City_200x_lang=ru.png - output = { - "lang": "ru", - "width": "200", - "filetype": "png", - "location": "City" - } - """ - - parsed = {} - to_be_parsed = {} - - if name.lower()[-4:] == '.png': - parsed['filetype'] = 'png' - name = name[:-4] - - parts = name.split('_') - parsed['location'] = parts[0] - - for part in parts[1:]: - if re.match('(?:[0-9]+)x', part): - parsed['width'] = part[:-1] - elif re.match('x(?:[0-9]+)', part): - parsed['height'] = part[1:] - elif re.match(part, '(?:[0-9]+)x(?:[0-9]+)'): - parsed['width'], parsed['height'] = part.split('x', 1) - elif '=' in part: - arg, val = part.split('=', 1) - to_be_parsed[arg] = val - else: - to_be_parsed[part] = '' - - parsed.update(parse_query.parse_query(to_be_parsed)) - - return parsed - -def _make_wttrin_query(parsed): - """Convert parsed data into query name - """ - - for key in ['width', 'height', 'filetype']: - if key in parsed: - del parsed[key] - - location = parsed['location'] - del parsed['location'] - - args = [] - if 'options' in parsed: - args = [parsed['options']] - del parsed['options'] - else: - args = [] - - for key, val in parsed.items(): - args.append('%s=%s' % (key, val)) - - args.append('filetype=png') - - url = "http://wttr.in/%s" % location - if args != []: - url += "?%s" % ("&".join(args)) - - return url From 4d9a4138aaa3f5e68f81ea064cd7cafa069c140f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 25 Apr 2020 18:19:00 +0200 Subject: [PATCH 23/73] lib/wttr_srv.py clean up --- lib/wttr_srv.py | 66 +++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 35 deletions(-) diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index 8345f7d..3949577 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -128,12 +128,14 @@ def get_answer_language_and_view(request): hostname = request.headers['Host'] if hostname != 'wttr.in' and hostname.endswith('.wttr.in'): lang = hostname[:-8] - if lang == "v2": - view_name = "v2" + if lang.startswith("v2"): + view_name = lang lang = None if 'lang' in request.args: lang = request.args.get('lang') + if lang.lower() == 'none': + lang = None header_accept_language = request.headers.get('Accept-Language', '') if lang is None and header_accept_language: @@ -141,18 +143,19 @@ def get_answer_language_and_view(request): return lang, view_name -def get_output_format(request, query): +def get_output_format(query, parsed_query): """ Return preferred output format: ansi, text, html or png based on arguments and headers in `request`. Return new location (can be rewritten) """ - if 'format' in query: - return False - user_agent = request.headers.get('User-Agent', '').lower() - if query.get('force-ansi'): + if ('format' in query and not query["format"].startswith("v2")) \ + or parsed_query.get("png_filename") \ + or query.get('force-ansi'): return False + + user_agent = parsed_query.get("user_agent", "").lower() html_output = not any(agent in user_agent for agent in PLAIN_TEXT_AGENTS) return html_output @@ -199,26 +202,18 @@ def _response(parsed_query, query, fast_mode=False): # at this point, we could not handle the query fast, # so we handle it with all available logic + loc = (parsed_query['orig_location'] or "").lower() if parsed_query["view"] or 'format' in query: - response_text = wttr_line(query, parsed_query) - return cache.store(cache_signature, response_text) + output = wttr_line(query, parsed_query) + elif loc == 'moon' or loc.startswith('moon@'): + output = get_moon(query, parsed_query) + else: + output = get_wetter(query, parsed_query) if parsed_query.get('png_filename'): - options = { - 'ip_addr': parsed_query['ip_addr'], - 'lang': parsed_query['lang'], - 'location': parsed_query['location']} - options.update(query) - - output = fmt.png.make_wttr_in_png( - parsed_query['png_filename'], options=options) + output = fmt.png.render_ansi( + output, options=parsed_query) else: - loc = (parsed_query['orig_location'] or "").lower() - if loc == 'moon' or loc.startswith('moon@'): - output = get_moon(query, parsed_query) - else: - output = get_wetter(query, parsed_query) - if query.get('days', '3') != '0' and not query.get('no-follow-line'): if parsed_query['html_output']: output = add_buttons(output) @@ -251,27 +246,28 @@ def parse_request(location, request, query, fast_mode=False): if location and ':' in location and location[0] != ":": location = _cyclic_location_selection(location, query.get('period', 1)) - lang, _view = get_answer_language_and_view(request) - html_output = get_output_format(request, query) - - ip_addr = _client_ip_address(request) parsed_query = { - 'ip_addr': ip_addr, + 'ip_addr': _client_ip_address(request), 'user_agent': request.headers.get('User-Agent', '').lower(), - 'lang': lang, - 'view': _view, - 'html_output': html_output, - 'orig_location': location, - 'location': location, 'request_url': request.url, } if png_filename: parsed_query["png_filename"] = png_filename + parsed_query.update(parse_query.parse_wttrin_png_name(png_filename)) - if not png_filename and not fast_mode: + lang, _view = get_answer_language_and_view(request) + + parsed_query["view"] = parsed_query.get("view", _view) + parsed_query["location"] = parsed_query.get("location", location) + parsed_query["orig_location"] = parsed_query["location"] + parsed_query["lang"] = parsed_query.get("lang", lang) + + parsed_query["html_output"] = get_output_format(query, parsed_query) + + if not fast_mode: # not png_filename and not fast_mode: location, override_location_name, full_address, country, query_source_location = \ - location_processing(location, ip_addr) + location_processing(parsed_query["location"], parsed_query["ip_addr"]) us_ip = query_source_location[1] == 'United States' \ and 'slack' not in parsed_query['user_agent'] From d5fae22cde3a24d5066527b60397963ee5150e2c Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 25 Apr 2020 18:56:27 +0200 Subject: [PATCH 24/73] view/wttr.py: fixed padding --- lib/view/wttr.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/view/wttr.py b/lib/view/wttr.py index 150d543..b94de17 100644 --- a/lib/view/wttr.py +++ b/lib/view/wttr.py @@ -187,9 +187,9 @@ def get_wetter(query, parsed_query): if query.get('padding', False): lines = [x.rstrip() for x in stdout.splitlines()] - max_l = max(len(remove_ansi(x).decode('utf8')) for x in lines) + max_l = max(len(remove_ansi(x)) for x in lines) last_line = " "*max_l + " .\n" - stdout = " \n" + "\n".join(" %s " %x for x in lines) + "\n" + last_line + stdout = " \n" + "\n".join(" %s " %x for x in lines) + "\n" + last_line open(filename, 'w').write(stdout) From 094a506d519d46a1160c87dffbe6bcf5e15dd41e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 25 Apr 2020 18:57:11 +0200 Subject: [PATCH 25/73] lib/view/line.py cleanup --- lib/view/line.py | 43 +++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/lib/view/line.py b/lib/view/line.py index 1f020fd..24ba323 100644 --- a/lib/view/line.py +++ b/lib/view/line.py @@ -237,26 +237,27 @@ def render_json(data): return output -def format_weather_data(format_line, location, override_location, full_address, data, query): +def format_weather_data(query, parsed_query, data): """ Format information about current weather `data` for `location` with specified in `format_line` format """ if 'data' not in data: - return 'Unknown location; please try ~%s' % location + return 'Unknown location; please try ~%s' % parsed_query["location"] + + format_line = query.get('format', parsed_query.get("view", "")) + if format_line in PRECONFIGURED_FORMAT: + format_line = PRECONFIGURED_FORMAT[format_line] if format_line == "j1": return render_json(data['data']) if format_line[:2] == "v2": - return v2.main(location, - override_location=override_location, - full_address=full_address, data=data, - view=format_line) + return v2.main(query, parsed_query, data) current_condition = data['data']['current_condition'][0] - current_condition['location'] = location - current_condition['override_location'] = override_location + current_condition['location'] = parsed_query["location"] + current_condition['override_location'] = parsed_query["override_location"] output = render_line(format_line, current_condition, query) return output @@ -266,23 +267,11 @@ def wttr_line(query, parsed_query): in format `line_format` """ location = parsed_query['location'] - override_location_name = parsed_query['override_location_name'] - full_address = parsed_query['full_address'] lang = parsed_query['lang'] - fmt = parsed_query['view'] - 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, full_address, - weather_data, query) - output = output.rstrip("\n")+"\n" - return output + data = get_weather_data(location, lang) + output = format_weather_data(query, parsed_query, data) + return output.rstrip("\n")+"\n" def main(): """ @@ -293,8 +282,14 @@ def main(): query = { 'line': sys.argv[2], } + parsed_query = { + "location": location, + "orig_location": location, + "language": "en", + "format": "v2", + } - sys.stdout.write(wttr_line(location, location, None, query, 'en', "v1")) + sys.stdout.write(wttr_line(query, parsed_query)) if __name__ == '__main__': main() From 9f5edb963e7c460d6d11ad076d82f00fc6b56367 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 25 Apr 2020 19:36:44 +0200 Subject: [PATCH 26/73] v2 html --- lib/view/v2.py | 65 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 22 deletions(-) diff --git a/lib/view/v2.py b/lib/view/v2.py index acb2151..8154559 100644 --- a/lib/view/v2.py +++ b/lib/view/v2.py @@ -89,8 +89,11 @@ def jq_query(query, data_parsed): # }}} # utils {{{ -def colorize(string, color_code): - return "\033[%sm%s\033[0m" % (color_code, string) +def colorize(string, color_code, html_output=False): + if html_output: + return "%s" % (string) + else: + return "\033[%sm%s\033[0m" % (color_code, string) # }}} # draw_spark {{{ @@ -359,9 +362,9 @@ def add_frame(output, width, config): output = "\n".join(u"│"+(x or empty_line)+u"│" for x in output.splitlines()) + "\n" weather_report = \ - translations.CAPTION[config["lang"]] \ + translations.CAPTION[config.get("lang") or "en"] \ + " " \ - + (config["override_location"] or config["location"]) + + (config["override_location_name"] or config["location"]) caption = u"┤ " + " " + weather_report + " " + u" ├" output = u"┌" + caption + u"─"*(width-len(caption)) + u"┐\n" \ @@ -424,7 +427,7 @@ def generate_panel(data_parsed, geo_data, config): # }}} # textual information {{{ -def textual_information(data_parsed, geo_data, config): +def textual_information(data_parsed, geo_data, config, html_output=False): """ Add textual information about current weather and astronomical conditions @@ -447,7 +450,9 @@ def textual_information(data_parsed, geo_data, config): output += "," + word return output - + + def _colorize(text, color): + return colorize(text, color, html_output=html_output) city = LocationInfo() city.latitude = geo_data["latitude"] @@ -484,7 +489,7 @@ def textual_information(data_parsed, geo_data, config): tmp_output.append('Sunset: %s' % local_time_of("sunset")) tmp_output.append('Dusk: %s' % local_time_of("dusk")) tmp_output = [ - re.sub("^([A-Za-z]*:)", lambda m: colorize(m.group(1), "2"), x) + re.sub("^([A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"), x) for x in tmp_output] output.append( @@ -512,9 +517,9 @@ def textual_information(data_parsed, geo_data, config): )) 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))) + 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) @@ -526,24 +531,40 @@ def get_geodata(location): return json.loads(text) # }}} -def main(location, override_location=None, data=None, full_address=None, view=None): - config = { - "lang": "en", - "locale": "en_US", - "location": location, - "override_location": override_location, - "full_address": full_address, - "view": view, - } +def main(query, parsed_query, data): + parsed_query["locale"] = "en_US" + + location = parsed_query["location"] + html_output = parsed_query["html_output"] geo_data = get_geodata(location) if data is None: - data_parsed = get_data(config) + data_parsed = get_data(parsed_query) else: data_parsed = data - output = generate_panel(data_parsed, geo_data, config) - output += textual_information(data_parsed, geo_data, config) + if html_output: + output = """ + + +Weather report for {orig_location} + + + + +
+{textual_information}
+
+ + +""".format( + orig_location=parsed_query["orig_location"], + textual_information=textual_information( + data_parsed, geo_data, parsed_query, html_output=True)) + else: + output = generate_panel(data_parsed, geo_data, parsed_query) + if query.get('text') != "no": + output += textual_information(data_parsed, geo_data, parsed_query) return output if __name__ == '__main__': From fb21802982cb20636b9840b777fdc831f1f26ea4 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 25 Apr 2020 22:27:58 +0200 Subject: [PATCH 27/73] fmt/png.py: font-size --- lib/fmt/png.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/fmt/png.py b/lib/fmt/png.py index 1449bd6..6f7f063 100644 --- a/lib/fmt/png.py +++ b/lib/fmt/png.py @@ -35,9 +35,9 @@ import globals COLS = 180 ROWS = 100 -CHAR_WIDTH = 9 -CHAR_HEIGHT = 18 -FONT_SIZE = 15 +CHAR_WIDTH = 8 +CHAR_HEIGHT = 14 +FONT_SIZE = 13 FONT_CAT = { 'default': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", 'Cyrillic': "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf", From 71b3a70e02a2a2245ed1e750d13482eedeb9f200 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sat, 25 Apr 2020 22:28:28 +0200 Subject: [PATCH 28/73] view=format --- lib/parse_query.py | 11 ++++++++++- lib/view/line.py | 2 +- lib/view/v2.py | 6 +++--- lib/view/wttr.py | 2 +- lib/wttr_srv.py | 18 ++++++++++++++---- 5 files changed, 29 insertions(+), 10 deletions(-) diff --git a/lib/parse_query.py b/lib/parse_query.py index 120e6b8..44be016 100644 --- a/lib/parse_query.py +++ b/lib/parse_query.py @@ -31,7 +31,6 @@ def parse_query(args): result = {} reserved_args = ["lang"] - #q = "&".join(x for x in args.keys() if x not in reserved_args) q = "" @@ -86,6 +85,11 @@ def parse_query(args): val = False result[key] = val + # currently `view` is alias for `format` + if "format" in result and not result.get("view"): + result["view"] = result["format"] + del result["format"] + return result def parse_wttrin_png_name(name): @@ -126,4 +130,9 @@ def parse_wttrin_png_name(name): parsed.update(parse_query(to_be_parsed)) + # currently `view` is alias for `format` + if "format" in parsed and not parsed.get("view"): + parsed["view"] = parsed["format"] + del parsed["format"] + return parsed diff --git a/lib/view/line.py b/lib/view/line.py index 24ba323..52c98a2 100644 --- a/lib/view/line.py +++ b/lib/view/line.py @@ -246,7 +246,7 @@ def format_weather_data(query, parsed_query, data): if 'data' not in data: return 'Unknown location; please try ~%s' % parsed_query["location"] - format_line = query.get('format', parsed_query.get("view", "")) + format_line = parsed_query.get("view", "") if format_line in PRECONFIGURED_FORMAT: format_line = PRECONFIGURED_FORMAT[format_line] diff --git a/lib/view/v2.py b/lib/view/v2.py index 8154559..c2d0707 100644 --- a/lib/view/v2.py +++ b/lib/view/v2.py @@ -91,7 +91,7 @@ def jq_query(query, data_parsed): # utils {{{ def colorize(string, color_code, html_output=False): if html_output: - return "%s" % (string) + return "%s" % (string) else: return "\033[%sm%s\033[0m" % (color_code, string) # }}} @@ -551,7 +551,7 @@ def main(query, parsed_query, data): - +
 {textual_information}
 
@@ -563,7 +563,7 @@ def main(query, parsed_query, data): data_parsed, geo_data, parsed_query, html_output=True)) else: output = generate_panel(data_parsed, geo_data, parsed_query) - if query.get('text') != "no": + if query.get("text") != "no" and parsed_query.get("text") != "no": output += textual_information(data_parsed, geo_data, parsed_query) return output diff --git a/lib/view/wttr.py b/lib/view/wttr.py index b94de17..ccd57a0 100644 --- a/lib/view/wttr.py +++ b/lib/view/wttr.py @@ -165,7 +165,7 @@ def get_wetter(query, parsed_query): separator = u':' if separator: - first = first.split(separator,1)[1] + first = first.split(separator, 1)[1] stdout = "\n".join([first.strip()] + rest) + "\n" if query.get('no-terminal', False): diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index 3949577..be2afa6 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -150,7 +150,7 @@ def get_output_format(query, parsed_query): Return new location (can be rewritten) """ - if ('format' in query and not query["format"].startswith("v2")) \ + if ('view' in query and not query["view"].startswith("v2")) \ or parsed_query.get("png_filename") \ or query.get('force-ansi'): return False @@ -202,8 +202,11 @@ def _response(parsed_query, query, fast_mode=False): # at this point, we could not handle the query fast, # so we handle it with all available logic + import json + print(json.dumps(parsed_query, indent=4)) + loc = (parsed_query['orig_location'] or "").lower() - if parsed_query["view"] or 'format' in query: + if parsed_query.get("view"): output = wttr_line(query, parsed_query) elif loc == 'moon' or loc.startswith('moon@'): output = get_moon(query, parsed_query) @@ -236,6 +239,13 @@ def parse_request(location, request, query, fast_mode=False): `request.query_string` `query` parsed command line arguments + Parameters priorities (from low to high): + + * HTTP-header + * Domain name + * URL + * Filename + Return: dictionary with parsed parameters """ @@ -258,7 +268,7 @@ def parse_request(location, request, query, fast_mode=False): lang, _view = get_answer_language_and_view(request) - parsed_query["view"] = parsed_query.get("view", _view) + parsed_query["view"] = parsed_query.get("view", query.get("view", _view)) parsed_query["location"] = parsed_query.get("location", location) parsed_query["orig_location"] = parsed_query["location"] parsed_query["lang"] = parsed_query.get("lang", lang) @@ -289,7 +299,7 @@ def parse_request(location, request, query, fast_mode=False): def wttr(location, request): """Main rendering function, it processes incoming weather queries, and depending on the User-Agent string and other paramters of the query - it returns output in HTMLi, ANSI or other format. + it returns output in HTML, ANSI or other format. """ def _wrap_response(response_text, html_output, png_filename=None): From 8695664f570a2636cd0fc30d93ebde53a9e189b0 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 26 Apr 2020 19:35:10 +0200 Subject: [PATCH 29/73] added some tests --- test/proxy-data/data1 | 1 + test/proxy-data/data1.headers | 1 + test/query.sh | 66 ++++++++++++++++++++++++++++++++++ test/test-data/signatures | 68 +++++++++++++++++++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 test/proxy-data/data1 create mode 100644 test/proxy-data/data1.headers create mode 100644 test/query.sh create mode 100644 test/test-data/signatures diff --git a/test/proxy-data/data1 b/test/proxy-data/data1 new file mode 100644 index 0000000..8e1f651 --- /dev/null +++ b/test/proxy-data/data1 @@ -0,0 +1 @@ +{"data":{"request":[{"type":"LatLon","query":"Lat 27.64 and Lon -80.40"}],"nearest_area":[{"areaName":[{"value":"Vero Beach"}],"country":[{"value":"United States of America"}],"region":[{"value":"Florida"}],"latitude":"27.638","longitude":"-80.398","population":"17262","weatherUrl":[{"value":"https://www.worldweatheronline.com/v2/weather.aspx?q=27.6387163,-80.3975399"}]}],"current_condition":[{"observation_time":"10:24 AM","localObsDateTime":"2020-04-26 06:24 AM","temp_C":"22","temp_F":"72","weatherCode":"143","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0006_mist.png"}],"weatherDesc":[{"value":"Mist"}],"windspeedMiles":"4","windspeedKmph":"6","winddirDegree":"240","winddir16Point":"WSW","precipMM":"0.4","humidity":"94","visibility":"10","pressure":"1015","cloudcover":"75","FeelsLikeC":"25","FeelsLikeF":"76","uvIndex":1}],"weather":[{"date":"2020-04-26","astronomy":[{"sunrise":"06:46 AM","sunset":"07:53 PM","moonrise":"09:12 AM","moonset":"11:17 PM","moon_phase":"Waxing Crescent","moon_illumination":"22"}],"maxtempC":"30","maxtempF":"87","mintempC":"23","mintempF":"73","totalSnow_cm":"0.0","sunHour":"6.5","uvIndex":"9","hourly":[{"time":"0","tempC":"25","tempF":"77","windspeedMiles":"7","windspeedKmph":"12","winddirDegree":"235","winddir16Point":"SW","weatherCode":"176","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0025_light_rain_showers_night.png"}],"weatherDesc":[{"value":"Patchy rain possible"}],"precipMM":"0.4","humidity":"85","visibility":"9","pressure":"1015","cloudcover":"86","HeatIndexC":"28","HeatIndexF":"82","DewPointC":"22","DewPointF":"72","WindChillC":"25","WindChillF":"77","WindGustMiles":"14","WindGustKmph":"22","FeelsLikeC":"28","FeelsLikeF":"82","chanceofrain":"76","chanceofremdry":"0","chanceofwindy":"0","chanceofovercast":"89","chanceofsunshine":"0","chanceoffrost":"0","chanceofhightemp":"71","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"},{"time":"300","tempC":"24","tempF":"76","windspeedMiles":"7","windspeedKmph":"12","winddirDegree":"242","winddir16Point":"WSW","weatherCode":"386","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0032_thundery_showers_night.png"}],"weatherDesc":[{"value":"Patchy light rain with thunder"}],"precipMM":"0.4","humidity":"88","visibility":"9","pressure":"1015","cloudcover":"81","HeatIndexC":"27","HeatIndexF":"80","DewPointC":"22","DewPointF":"72","WindChillC":"24","WindChillF":"76","WindGustMiles":"13","WindGustKmph":"21","FeelsLikeC":"27","FeelsLikeF":"80","chanceofrain":"70","chanceofremdry":"0","chanceofwindy":"0","chanceofovercast":"87","chanceofsunshine":"0","chanceoffrost":"0","chanceofhightemp":"13","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"},{"time":"600","tempC":"24","tempF":"75","windspeedMiles":"7","windspeedKmph":"12","winddirDegree":"246","winddir16Point":"WSW","weatherCode":"386","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0032_thundery_showers_night.png"}],"weatherDesc":[{"value":"Patchy light rain with thunder"}],"precipMM":"0.6","humidity":"89","visibility":"10","pressure":"1015","cloudcover":"74","HeatIndexC":"26","HeatIndexF":"79","DewPointC":"22","DewPointF":"72","WindChillC":"24","WindChillF":"75","WindGustMiles":"12","WindGustKmph":"20","FeelsLikeC":"26","FeelsLikeF":"79","chanceofrain":"77","chanceofremdry":"0","chanceofwindy":"0","chanceofovercast":"87","chanceofsunshine":"0","chanceoffrost":"0","chanceofhightemp":"12","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"},{"time":"900","tempC":"26","tempF":"78","windspeedMiles":"7","windspeedKmph":"12","winddirDegree":"246","winddir16Point":"WSW","weatherCode":"386","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0016_thundery_showers.png"}],"weatherDesc":[{"value":"Patchy light rain with thunder"}],"precipMM":"1.1","humidity":"83","visibility":"10","pressure":"1017","cloudcover":"80","HeatIndexC":"28","HeatIndexF":"83","DewPointC":"22","DewPointF":"72","WindChillC":"26","WindChillF":"78","WindGustMiles":"11","WindGustKmph":"17","FeelsLikeC":"28","FeelsLikeF":"83","chanceofrain":"73","chanceofremdry":"0","chanceofwindy":"0","chanceofovercast":"88","chanceofsunshine":"0","chanceoffrost":"0","chanceofhightemp":"42","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"9"},{"time":"1200","tempC":"29","tempF":"84","windspeedMiles":"7","windspeedKmph":"12","winddirDegree":"247","winddir16Point":"WSW","weatherCode":"353","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0009_light_rain_showers.png"}],"weatherDesc":[{"value":"Light rain shower"}],"precipMM":"0.7","humidity":"68","visibility":"10","pressure":"1017","cloudcover":"76","HeatIndexC":"32","HeatIndexF":"90","DewPointC":"22","DewPointF":"72","WindChillC":"29","WindChillF":"84","WindGustMiles":"9","WindGustKmph":"15","FeelsLikeC":"32","FeelsLikeF":"90","chanceofrain":"74","chanceofremdry":"0","chanceofwindy":"0","chanceofovercast":"85","chanceofsunshine":"0","chanceoffrost":"0","chanceofhightemp":"95","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"31","uvIndex":"9"},{"time":"1500","tempC":"30","tempF":"86","windspeedMiles":"7","windspeedKmph":"11","winddirDegree":"259","winddir16Point":"WSW","weatherCode":"386","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0016_thundery_showers.png"}],"weatherDesc":[{"value":"Patchy light rain with thunder"}],"precipMM":"1.0","humidity":"63","visibility":"10","pressure":"1016","cloudcover":"62","HeatIndexC":"33","HeatIndexF":"92","DewPointC":"22","DewPointF":"72","WindChillC":"30","WindChillF":"86","WindGustMiles":"9","WindGustKmph":"14","FeelsLikeC":"33","FeelsLikeF":"92","chanceofrain":"76","chanceofremdry":"0","chanceofwindy":"0","chanceofovercast":"87","chanceofsunshine":"0","chanceoffrost":"0","chanceofhightemp":"96","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"87","uvIndex":"9"},{"time":"1800","tempC":"28","tempF":"82","windspeedMiles":"6","windspeedKmph":"9","winddirDegree":"259","winddir16Point":"WSW","weatherCode":"386","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0016_thundery_showers.png"}],"weatherDesc":[{"value":"Patchy light rain with thunder"}],"precipMM":"1.5","humidity":"70","visibility":"10","pressure":"1015","cloudcover":"72","HeatIndexC":"31","HeatIndexF":"88","DewPointC":"22","DewPointF":"71","WindChillC":"28","WindChillF":"82","WindGustMiles":"9","WindGustKmph":"15","FeelsLikeC":"31","FeelsLikeF":"88","chanceofrain":"72","chanceofremdry":"0","chanceofwindy":"0","chanceofovercast":"86","chanceofsunshine":"0","chanceoffrost":"0","chanceofhightemp":"93","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"48","uvIndex":"9"},{"time":"2100","tempC":"25","tempF":"77","windspeedMiles":"7","windspeedKmph":"11","winddirDegree":"243","winddir16Point":"WSW","weatherCode":"176","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0025_light_rain_showers_night.png"}],"weatherDesc":[{"value":"Patchy rain possible"}],"precipMM":"0.2","humidity":"74","visibility":"10","pressure":"1016","cloudcover":"75","HeatIndexC":"27","HeatIndexF":"81","DewPointC":"20","DewPointF":"68","WindChillC":"25","WindChillF":"77","WindGustMiles":"13","WindGustKmph":"21","FeelsLikeC":"27","FeelsLikeF":"81","chanceofrain":"81","chanceofremdry":"0","chanceofwindy":"0","chanceofovercast":"90","chanceofsunshine":"0","chanceoffrost":"0","chanceofhightemp":"61","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"}]},{"date":"2020-04-27","astronomy":[{"sunrise":"06:45 AM","sunset":"07:54 PM","moonrise":"09:58 AM","moonset":"No moonset","moon_phase":"Waxing Crescent","moon_illumination":"30"}],"maxtempC":"24","maxtempF":"76","mintempC":"19","mintempF":"66","totalSnow_cm":"0.0","sunHour":"11.6","uvIndex":"11","hourly":[{"time":"0","tempC":"22","tempF":"71","windspeedMiles":"11","windspeedKmph":"17","winddirDegree":"304","winddir16Point":"NW","weatherCode":"176","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0025_light_rain_showers_night.png"}],"weatherDesc":[{"value":"Patchy rain possible"}],"precipMM":"0.1","humidity":"67","visibility":"10","pressure":"1018","cloudcover":"72","HeatIndexC":"23","HeatIndexF":"74","DewPointC":"15","DewPointF":"60","WindChillC":"22","WindChillF":"71","WindGustMiles":"19","WindGustKmph":"31","FeelsLikeC":"22","FeelsLikeF":"71","chanceofrain":"77","chanceofremdry":"0","chanceofwindy":"0","chanceofovercast":"92","chanceofsunshine":"0","chanceoffrost":"0","chanceofhightemp":"0","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"},{"time":"300","tempC":"20","tempF":"67","windspeedMiles":"13","windspeedKmph":"20","winddirDegree":"323","winddir16Point":"NW","weatherCode":"113","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0008_clear_sky_night.png"}],"weatherDesc":[{"value":"Clear"}],"precipMM":"0.1","humidity":"59","visibility":"10","pressure":"1017","cloudcover":"49","HeatIndexC":"20","HeatIndexF":"67","DewPointC":"12","DewPointF":"53","WindChillC":"20","WindChillF":"67","WindGustMiles":"22","WindGustKmph":"36","FeelsLikeC":"20","FeelsLikeF":"67","chanceofrain":"51","chanceofremdry":"30","chanceofwindy":"0","chanceofovercast":"63","chanceofsunshine":"29","chanceoffrost":"0","chanceofhightemp":"0","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"},{"time":"600","tempC":"19","tempF":"67","windspeedMiles":"15","windspeedKmph":"24","winddirDegree":"340","winddir16Point":"NNW","weatherCode":"116","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png"}],"weatherDesc":[{"value":"Partly cloudy"}],"precipMM":"0.0","humidity":"54","visibility":"10","pressure":"1018","cloudcover":"1","HeatIndexC":"19","HeatIndexF":"67","DewPointC":"10","DewPointF":"49","WindChillC":"19","WindChillF":"67","WindGustMiles":"24","WindGustKmph":"39","FeelsLikeC":"19","FeelsLikeF":"67","chanceofrain":"0","chanceofremdry":"88","chanceofwindy":"0","chanceofovercast":"13","chanceofsunshine":"83","chanceoffrost":"0","chanceofhightemp":"0","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"},{"time":"900","tempC":"20","tempF":"68","windspeedMiles":"14","windspeedKmph":"22","winddirDegree":"339","winddir16Point":"NNW","weatherCode":"113","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0001_sunny.png"}],"weatherDesc":[{"value":"Sunny"}],"precipMM":"0.0","humidity":"51","visibility":"10","pressure":"1020","cloudcover":"1","HeatIndexC":"21","HeatIndexF":"69","DewPointC":"9","DewPointF":"49","WindChillC":"20","WindChillF":"68","WindGustMiles":"19","WindGustKmph":"31","FeelsLikeC":"20","FeelsLikeF":"68","chanceofrain":"0","chanceofremdry":"87","chanceofwindy":"0","chanceofovercast":"26","chanceofsunshine":"80","chanceoffrost":"0","chanceofhightemp":"0","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"11"},{"time":"1200","tempC":"23","tempF":"73","windspeedMiles":"13","windspeedKmph":"21","winddirDegree":"230","winddir16Point":"SW","weatherCode":"113","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0001_sunny.png"}],"weatherDesc":[{"value":"Sunny"}],"precipMM":"0.0","humidity":"43","visibility":"10","pressure":"1020","cloudcover":"0","HeatIndexC":"24","HeatIndexF":"76","DewPointC":"9","DewPointF":"49","WindChillC":"23","WindChillF":"73","WindGustMiles":"15","WindGustKmph":"24","FeelsLikeC":"24","FeelsLikeF":"76","chanceofrain":"0","chanceofremdry":"88","chanceofwindy":"0","chanceofovercast":"0","chanceofsunshine":"88","chanceoffrost":"0","chanceofhightemp":"5","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"11"},{"time":"1500","tempC":"24","tempF":"74","windspeedMiles":"12","windspeedKmph":"20","winddirDegree":"12","winddir16Point":"NNE","weatherCode":"113","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0001_sunny.png"}],"weatherDesc":[{"value":"Sunny"}],"precipMM":"0.0","humidity":"41","visibility":"10","pressure":"1019","cloudcover":"0","HeatIndexC":"25","HeatIndexF":"76","DewPointC":"10","DewPointF":"49","WindChillC":"24","WindChillF":"74","WindGustMiles":"14","WindGustKmph":"23","FeelsLikeC":"25","FeelsLikeF":"76","chanceofrain":"0","chanceofremdry":"83","chanceofwindy":"0","chanceofovercast":"0","chanceofsunshine":"86","chanceoffrost":"0","chanceofhightemp":"11","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"11"},{"time":"1800","tempC":"22","tempF":"71","windspeedMiles":"13","windspeedKmph":"21","winddirDegree":"33","winddir16Point":"NNE","weatherCode":"113","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0001_sunny.png"}],"weatherDesc":[{"value":"Sunny"}],"precipMM":"0.0","humidity":"53","visibility":"10","pressure":"1019","cloudcover":"0","HeatIndexC":"23","HeatIndexF":"74","DewPointC":"12","DewPointF":"53","WindChillC":"22","WindChillF":"71","WindGustMiles":"16","WindGustKmph":"26","FeelsLikeC":"22","FeelsLikeF":"71","chanceofrain":"0","chanceofremdry":"86","chanceofwindy":"0","chanceofovercast":"0","chanceofsunshine":"86","chanceoffrost":"0","chanceofhightemp":"0","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"11"},{"time":"2100","tempC":"20","tempF":"68","windspeedMiles":"11","windspeedKmph":"17","winddirDegree":"38","winddir16Point":"NE","weatherCode":"113","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0008_clear_sky_night.png"}],"weatherDesc":[{"value":"Clear"}],"precipMM":"0.0","humidity":"60","visibility":"10","pressure":"1020","cloudcover":"0","HeatIndexC":"20","HeatIndexF":"68","DewPointC":"12","DewPointF":"54","WindChillC":"20","WindChillF":"68","WindGustMiles":"18","WindGustKmph":"29","FeelsLikeC":"20","FeelsLikeF":"68","chanceofrain":"0","chanceofremdry":"81","chanceofwindy":"0","chanceofovercast":"0","chanceofsunshine":"88","chanceoffrost":"0","chanceofhightemp":"0","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"}]},{"date":"2020-04-28","astronomy":[{"sunrise":"06:44 AM","sunset":"07:54 PM","moonrise":"10:50 AM","moonset":"12:13 AM","moon_phase":"Waxing Crescent","moon_illumination":"37"}],"maxtempC":"25","maxtempF":"76","mintempC":"19","mintempF":"67","totalSnow_cm":"0.0","sunHour":"10.3","uvIndex":"9","hourly":[{"time":"0","tempC":"20","tempF":"68","windspeedMiles":"10","windspeedKmph":"16","winddirDegree":"34","winddir16Point":"NNE","weatherCode":"113","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0008_clear_sky_night.png"}],"weatherDesc":[{"value":"Clear"}],"precipMM":"0.0","humidity":"61","visibility":"10","pressure":"1021","cloudcover":"0","HeatIndexC":"20","HeatIndexF":"68","DewPointC":"12","DewPointF":"54","WindChillC":"20","WindChillF":"68","WindGustMiles":"18","WindGustKmph":"28","FeelsLikeC":"20","FeelsLikeF":"68","chanceofrain":"0","chanceofremdry":"82","chanceofwindy":"0","chanceofovercast":"0","chanceofsunshine":"89","chanceoffrost":"0","chanceofhightemp":"0","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"},{"time":"300","tempC":"20","tempF":"67","windspeedMiles":"9","windspeedKmph":"15","winddirDegree":"35","winddir16Point":"NE","weatherCode":"113","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0008_clear_sky_night.png"}],"weatherDesc":[{"value":"Clear"}],"precipMM":"0.0","humidity":"61","visibility":"10","pressure":"1020","cloudcover":"0","HeatIndexC":"20","HeatIndexF":"67","DewPointC":"12","DewPointF":"53","WindChillC":"20","WindChillF":"67","WindGustMiles":"17","WindGustKmph":"27","FeelsLikeC":"20","FeelsLikeF":"67","chanceofrain":"0","chanceofremdry":"85","chanceofwindy":"0","chanceofovercast":"0","chanceofsunshine":"89","chanceoffrost":"0","chanceofhightemp":"0","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"},{"time":"600","tempC":"20","tempF":"68","windspeedMiles":"9","windspeedKmph":"14","winddirDegree":"53","winddir16Point":"NE","weatherCode":"116","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0004_black_low_cloud.png"}],"weatherDesc":[{"value":"Partly cloudy"}],"precipMM":"0.0","humidity":"62","visibility":"10","pressure":"1020","cloudcover":"5","HeatIndexC":"20","HeatIndexF":"68","DewPointC":"13","DewPointF":"55","WindChillC":"20","WindChillF":"68","WindGustMiles":"15","WindGustKmph":"25","FeelsLikeC":"20","FeelsLikeF":"68","chanceofrain":"0","chanceofremdry":"86","chanceofwindy":"0","chanceofovercast":"13","chanceofsunshine":"84","chanceoffrost":"0","chanceofhightemp":"0","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"},{"time":"900","tempC":"22","tempF":"71","windspeedMiles":"10","windspeedKmph":"16","winddirDegree":"69","winddir16Point":"ENE","weatherCode":"116","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0002_sunny_intervals.png"}],"weatherDesc":[{"value":"Partly cloudy"}],"precipMM":"0.0","humidity":"60","visibility":"10","pressure":"1021","cloudcover":"15","HeatIndexC":"22","HeatIndexF":"72","DewPointC":"14","DewPointF":"57","WindChillC":"22","WindChillF":"71","WindGustMiles":"14","WindGustKmph":"22","FeelsLikeC":"22","FeelsLikeF":"71","chanceofrain":"0","chanceofremdry":"87","chanceofwindy":"0","chanceofovercast":"43","chanceofsunshine":"74","chanceoffrost":"0","chanceofhightemp":"3","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"9"},{"time":"1200","tempC":"24","tempF":"75","windspeedMiles":"12","windspeedKmph":"19","winddirDegree":"77","winddir16Point":"ENE","weatherCode":"119","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0003_white_cloud.png"}],"weatherDesc":[{"value":"Cloudy"}],"precipMM":"0.0","humidity":"53","visibility":"10","pressure":"1022","cloudcover":"37","HeatIndexC":"25","HeatIndexF":"78","DewPointC":"14","DewPointF":"57","WindChillC":"24","WindChillF":"75","WindGustMiles":"14","WindGustKmph":"22","FeelsLikeC":"25","FeelsLikeF":"78","chanceofrain":"0","chanceofremdry":"86","chanceofwindy":"0","chanceofovercast":"63","chanceofsunshine":"56","chanceoffrost":"0","chanceofhightemp":"12","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"9"},{"time":"1500","tempC":"25","tempF":"76","windspeedMiles":"11","windspeedKmph":"18","winddirDegree":"83","winddir16Point":"E","weatherCode":"116","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0002_sunny_intervals.png"}],"weatherDesc":[{"value":"Partly cloudy"}],"precipMM":"0.0","humidity":"52","visibility":"10","pressure":"1020","cloudcover":"65","HeatIndexC":"26","HeatIndexF":"78","DewPointC":"14","DewPointF":"57","WindChillC":"25","WindChillF":"76","WindGustMiles":"13","WindGustKmph":"21","FeelsLikeC":"26","FeelsLikeF":"78","chanceofrain":"0","chanceofremdry":"86","chanceofwindy":"0","chanceofovercast":"72","chanceofsunshine":"35","chanceoffrost":"0","chanceofhightemp":"15","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"9"},{"time":"1800","tempC":"23","tempF":"74","windspeedMiles":"9","windspeedKmph":"15","winddirDegree":"88","winddir16Point":"E","weatherCode":"116","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0002_sunny_intervals.png"}],"weatherDesc":[{"value":"Partly cloudy"}],"precipMM":"0.0","humidity":"56","visibility":"10","pressure":"1019","cloudcover":"16","HeatIndexC":"25","HeatIndexF":"77","DewPointC":"14","DewPointF":"57","WindChillC":"23","WindChillF":"74","WindGustMiles":"13","WindGustKmph":"20","FeelsLikeC":"25","FeelsLikeF":"77","chanceofrain":"0","chanceofremdry":"91","chanceofwindy":"0","chanceofovercast":"35","chanceofsunshine":"82","chanceoffrost":"0","chanceofhightemp":"10","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"9"},{"time":"2100","tempC":"22","tempF":"71","windspeedMiles":"8","windspeedKmph":"13","winddirDegree":"90","winddir16Point":"E","weatherCode":"113","weatherIconUrl":[{"value":"http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0008_clear_sky_night.png"}],"weatherDesc":[{"value":"Clear"}],"precipMM":"0.0","humidity":"61","visibility":"10","pressure":"1020","cloudcover":"6","HeatIndexC":"25","HeatIndexF":"76","DewPointC":"14","DewPointF":"57","WindChillC":"22","WindChillF":"71","WindGustMiles":"14","WindGustKmph":"23","FeelsLikeC":"22","FeelsLikeF":"71","chanceofrain":"0","chanceofremdry":"89","chanceofwindy":"0","chanceofovercast":"28","chanceofsunshine":"84","chanceoffrost":"0","chanceofhightemp":"0","chanceoffog":"0","chanceofsnow":"0","chanceofthunder":"0","uvIndex":"1"}]}]}} \ No newline at end of file diff --git a/test/proxy-data/data1.headers b/test/proxy-data/data1.headers new file mode 100644 index 0000000..9b1ac5b --- /dev/null +++ b/test/proxy-data/data1.headers @@ -0,0 +1 @@ +{"Content-Type": "application/json"} \ No newline at end of file diff --git a/test/query.sh b/test/query.sh new file mode 100644 index 0000000..7d1eb60 --- /dev/null +++ b/test/query.sh @@ -0,0 +1,66 @@ +queries=( + / + /Kiev + /Kiev.png + /?T + /Киев + /Kiev?2 + "/Kiev?format=1" + "/Kiev?format=2" + "/Kiev?format=3" + "/Kiev?format=4" + "/Kiev?format=v2" + "/:help" + "/Kiev?T" + "/Kiev?p" + "/Kiev?q" + "/Kiev?Q" + "/Kiev_text=no_view=v2.png" +) + +options=$(cat < test-data/signatures +fi + +result_tmp=$(mktemp wttrin-test-XXXXX) + +while read -r -a args +do + for q in "${queries[@]}"; do + signature=$(echo "${args[@]}" "$q" | sha1sum | awk '{print $1}') + curl -ks "${args[@]}" "$server$q" > "$result_tmp" + + result=$(sha1sum "$result_tmp" | awk '{print $1}') + + # this must be moved to the server + # but for the moment we just clean up + # the cache after each call + rm -rf "/wttr.in/cache" + + if grep -Eq "(we are running out of queries|500 Internal Server Error)" "$result_tmp"; then + echo "$q" + fi + + if [[ $UPDATE = yes ]]; then + printf "%s %s %s\\n" "$signature" "$result" "${args[*]} $q" >> test-data/signatures + elif ! grep -q "$signature $result" test-data/signatures; then + echo "FAILED: curl -ks ${args[*]} $server$q" + fi + done +done <<< "${options}" + +rm "$result_tmp" diff --git a/test/test-data/signatures b/test/test-data/signatures new file mode 100644 index 0000000..edb43a2 --- /dev/null +++ b/test/test-data/signatures @@ -0,0 +1,68 @@ +8f27084b6294ddbe28dbcbf98f798730e8a79289 371718f3918eb35adeb2cfe8f6fbc52adef39520 / +ae537911bb7b0568f478073e661abee1cb4ff941 d123e570da22dee9798d353c4281cb5a2bdbaeac /Kiev +4dc586807c16020b9f4dbb705326c698bea41665 a186d89e95061a7887c005ffa8bd1e29362de2da /Kiev.png +3db1938bedc0ee0047bf3b043ddaf0aba1912f13 b85ed0c0c0214016eabdcc0283e7b8f4f682f5a9 /?T +2cc0ba7a57a6342e72fd7142ca18dbb0eae69416 ce7fb7a88cab697f5280ddabf344f0d397888956 /Киев +928142e88da142ea8075cbfe09bfef349e72dbb1 0f86f59a45b4485fea1375ca945503d9abb9a96d /Kiev?2 +4f6f0a16ff415fad1c102c8023c5d8365ef63402 16d85d2b01441f40b1a60f45603800923580b971 /Kiev?format=1 +c99903b86971ccccfcca4f13e6fca72776b4fbcf bd4c56b36d0c86e805702b5e6c71ba2b1cbf93c1 /Kiev?format=2 +2a0d6cd8d30a84328580611ca6dd6bed1d805a04 b9a5df200b9dd035d2fee4add1a938f621b2aa98 /Kiev?format=3 +4e4e15eeb6a8b6ddb5d00591c4b5e9b74a13e6bc c31ccd5f70ef1f317ff4ad99de5b6b9affaceb39 /Kiev?format=4 +a27d3e4ad7f820124ef57c9299715bc61cb71387 18afd7989100da758f68add862b0a2730e0c79f2 /Kiev?format=v2 +83cc0ef08c24ad7ecc81d1a6cbd693bb06214ece 212431752d0b0d21fabf073e98efa3f1bc24e76e /:help +310b64f65fc9f66a5142bf6104f4f9b9d5eef0ea b0bd07f0c87aae9464c091ccb955f41ec6973098 /Kiev?T +9bd1b460d4927df24724f45f69bd3132f3de8e04 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 /Kiev?p +3ee1a25d436799804d7ebd8371d8022fa55a71d7 80f18be012d0471dce9fcd2b500f482bcd635347 /Kiev?q +e0e8e7eca16bfac88503ac6d19a7a6c8b469c0fd d5d070c98237f0dffc82b176039f90a15f03a667 /Kiev?Q +d08d1fa2546fee0717d1eb663cf63cd1505b8885 5ccbb4c950bac7f33f1996af856f9678a4a47199 /Kiev_text=no_view=v2.png +3e1be80e942a2ea5450c60e1c0ebfb154aca3da1 76bdcb958640233fa0048bb14c9245f18b14f7c4 -A firefox / +ee6bf0665c2719cda3ec1fbdb80413d821c99b8e 3f9c5091269ece259cce13fc842265019001ed54 -A firefox /Kiev +98ef11678b7fd33425f97eeee70e00cd96206539 a186d89e95061a7887c005ffa8bd1e29362de2da -A firefox /Kiev.png +ecbcf2cb9004a754c4559ce7e92fead68f71721a bfd43c9cb44736e26752741ab2f500f9ca06da1e -A firefox /?T +74206d869128383dba2d840b848b90eb376fd851 7c6ce53ff25d91a5f46baa30077b69e0f09f2571 -A firefox /Киев +91b89025b5acd56ca475924e0eb559a9734f3333 dbd49d93eff2b2cf82f7d266f90de950207a0561 -A firefox /Kiev?2 +e6cb82dab95e05167ffbcb90a10d6cb03cd02ec1 16d85d2b01441f40b1a60f45603800923580b971 -A firefox /Kiev?format=1 +e65bc57e8d1df26c442a9ecf45afee390ff331a3 bd4c56b36d0c86e805702b5e6c71ba2b1cbf93c1 -A firefox /Kiev?format=2 +d743b331d5f4c81bbc8b168ce84a99ab22dc70cf b9a5df200b9dd035d2fee4add1a938f621b2aa98 -A firefox /Kiev?format=3 +bf359ee92690c3a3061542dc6e78cb42ca837412 c31ccd5f70ef1f317ff4ad99de5b6b9affaceb39 -A firefox /Kiev?format=4 +cb875772a6610c991b95b3fbfa22fc7192e25843 9b8e68e9701097316bdacfb13b0c07df23fc0aee -A firefox /Kiev?format=v2 +d520af45b491689d53024c696955db8b1e4eaa87 a76e0c26bac17e43af879db6cb11a3cd9eda2344 -A firefox /:help +6b80492b79a4cc510cb4a9654cb6ff085cdc1943 801d4c6d6837c9604944168a3930dfe05b0e9f5d -A firefox /Kiev?T +e13d7449ce756e55ba3e84e4b7e601b36d0044b6 c5a87804710ab70b8798f56579d276f4ce1806bf -A firefox /Kiev?p +3f6c192a6da5b79ea59ef94e99b9cbf4b0e7ede2 372aca50f441920ad623d62ee8fcde46d609f6f9 -A firefox /Kiev?q +f28ca2a7a47f4859eac7d0307ec7ac67a40e0adf 1b4b66d58bd7e27abeeca45581e686f12fefe76a -A firefox /Kiev?Q +62c5029cf297b1434c57228dc8c8cdfb5e68285d 5ccbb4c950bac7f33f1996af856f9678a4a47199 -A firefox /Kiev_text=no_view=v2.png +ed573b89ca5522d6ab69dc1686b98b00391076bd 9cd1b364c12acd74899ae34bfb9731f70b0b9b68 -H Accept-Language:ru / +b879673f66235bbf1913ff9abc58aff2fb8962d1 00a96a5d83608c2dad7921862bb3f244775f6b19 -H Accept-Language:ru /Kiev +83d99896cf866ecbaa6d2c64c12bd31bc7b35068 92dc07acb93633974eaff19e8c1a99e590e140d9 -H Accept-Language:ru /Kiev.png +9cbb6aa3e0b46e78229a32688db1cced9a44271d 1f82fb278d40520b4c1ef7378e1a5abc868c1f3e -H Accept-Language:ru /?T +095d8d38c667923131801595b903e007b5f902f3 4ede3397f9def696adc7ecf3ffd46a59b8fb25cb -H Accept-Language:ru /Киев +4e6cdfc38c9d9f2436438b345776c42cb8cab8a5 1b00c96a05f9daea8248a8e063d990797be933ad -H Accept-Language:ru /Kiev?2 +b7d8d0f0bb4c38aabac468c9a354bb4e2b401893 c97ca64c11eaa714b472bf723a3353abd4c5bd09 -H Accept-Language:ru /Kiev?format=1 +8f3bbfc9be6418e82edacecc54a9f3e9f26b7fbf 8a29f5bf1e412e2942a701b88dfcf382072b3f34 -H Accept-Language:ru /Kiev?format=2 +f1d4178892fd3dc38e9f966112d317859acc9122 8181bd5a0d7fff5b420d480160b9ea0e14d45aeb -H Accept-Language:ru /Kiev?format=3 +cf44e154504d9bc2b9b6066bbb0f5d52fc12f13e daca309c8ef168b97998e86dbbaab2d3963495b8 -H Accept-Language:ru /Kiev?format=4 +4955c849f67da53203b8c96b15a0bf0a4a471bc6 03afac96b43c7f1b2622b1015b8a4a3cb721c50a -H Accept-Language:ru /Kiev?format=v2 +3f69f4a605ce88643b4e0d62a588c92625d41aea 237e5cf3a0f4737df49d8382fc8a84f41603cbe5 -H Accept-Language:ru /:help +08553ca4bf71c738c4321fe7d84b4e6ff830956f 016fc03b18a8902f838719bbc171184603c08b60 -H Accept-Language:ru /Kiev?T +b70f8b3fc8aee126c04b27b0d3b4c503b4292cbf b60b68a9e77275884812f7e52b06f6012ba5682a -H Accept-Language:ru /Kiev?p +400efdba61125f8cb850d7c33caf4fc2739a960b 5ee4a043a91509ef57aec46a14a0c24f09e8ec47 -H Accept-Language:ru /Kiev?q +b9fd454e73343f262a6d99dd80487495bd647c6f a10718896a07baadb87adf2bf0026b1f00252213 -H Accept-Language:ru /Kiev?Q +8fed034e57624d0e0b33140673094e56e04087bc c7367485784883041c62f4c284aeacf690df71b4 -H Accept-Language:ru /Kiev_text=no_view=v2.png +3ce3dd46413f236244410f142a4b44356a0cedf9 1c2eea391b35c8bfed2541435e1788307aa06bc1 -H X-Forwarded-For:1.1.1.1 / +89be0a5787592298ce34f10b36da7ee87d1a1353 d123e570da22dee9798d353c4281cb5a2bdbaeac -H X-Forwarded-For:1.1.1.1 /Kiev +a9977eadc628b1ede5d4f91ee103dfb740caa2b1 a186d89e95061a7887c005ffa8bd1e29362de2da -H X-Forwarded-For:1.1.1.1 /Kiev.png +eec20c6be5e528967cddf6d0b72c84dbda553d43 9a39dbafa7e1550d374e38059c0f4b8f437e1739 -H X-Forwarded-For:1.1.1.1 /?T +e304153f0e1e9b41781bf4eb6fb6c4a5b7513aec ce7fb7a88cab697f5280ddabf344f0d397888956 -H X-Forwarded-For:1.1.1.1 /Киев +98f0b3a28863a861c6ac6d89ee5d49adb7f3f518 0f86f59a45b4485fea1375ca945503d9abb9a96d -H X-Forwarded-For:1.1.1.1 /Kiev?2 +cf04d7fe2cf36eba8d7fb4fa6def1c9015036456 16d85d2b01441f40b1a60f45603800923580b971 -H X-Forwarded-For:1.1.1.1 /Kiev?format=1 +67fbe9168566709450eb35d36c60c27105335a7e bd4c56b36d0c86e805702b5e6c71ba2b1cbf93c1 -H X-Forwarded-For:1.1.1.1 /Kiev?format=2 +b2604348bf39774c85b7c18ae7b51f63a2c9f31a b9a5df200b9dd035d2fee4add1a938f621b2aa98 -H X-Forwarded-For:1.1.1.1 /Kiev?format=3 +cf012400156c842e569b6a9f05b094e6b75348cd c31ccd5f70ef1f317ff4ad99de5b6b9affaceb39 -H X-Forwarded-For:1.1.1.1 /Kiev?format=4 +1f4981348cab19df9846cd3b3923ee7a972ff9fa 18afd7989100da758f68add862b0a2730e0c79f2 -H X-Forwarded-For:1.1.1.1 /Kiev?format=v2 +767a7407c14049fd77a6a2fedd1d8b35f6e47e0d 212431752d0b0d21fabf073e98efa3f1bc24e76e -H X-Forwarded-For:1.1.1.1 /:help +10631d55b42e7bc5ec15ffc5cddae712785eb354 b0bd07f0c87aae9464c091ccb955f41ec6973098 -H X-Forwarded-For:1.1.1.1 /Kiev?T +031478f562663eb9f577b04032993e2f098146f6 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 -H X-Forwarded-For:1.1.1.1 /Kiev?p +e106cd21a6b67196159c2baa023142e3a8859612 80f18be012d0471dce9fcd2b500f482bcd635347 -H X-Forwarded-For:1.1.1.1 /Kiev?q +1a16c9b52ba90cb7ad3dd8902bf41b31a287d49e d5d070c98237f0dffc82b176039f90a15f03a667 -H X-Forwarded-For:1.1.1.1 /Kiev?Q +83bd9cc6a646e44b75524474dd32f0fd1f5c5a39 5ccbb4c950bac7f33f1996af856f9678a4a47199 -H X-Forwarded-For:1.1.1.1 /Kiev_text=no_view=v2.png From 87d7e3c8637be480a91894495797145b9a837296 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 26 Apr 2020 19:36:47 +0200 Subject: [PATCH 30/73] added WTTRIN_TEST mode for proxy.py --- bin/proxy.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/bin/proxy.py b/bin/proxy.py index ddb9642..59a6fad 100644 --- a/bin/proxy.py +++ b/bin/proxy.py @@ -7,6 +7,10 @@ The proxy server acts as a backend for the wttr.in service. It caches the answers and handles various data sources transforming their answers into format supported by the wttr.in service. +If WTTRIN_TEST is specified, it works in a special test mode: +it does not fetch and does not store the data in the cache, +but is using the fake data from "test/proxy-data". + """ from __future__ import print_function @@ -35,7 +39,10 @@ from globals import PROXY_CACHEDIR, PROXY_HOST, PROXY_PORT from translations import PROXY_LANGS # pylint: enable=wrong-import-position +def is_testmode(): + """Server is running in the wttr.in test mode""" + return "WTTRIN_TEST" in os.environ def load_translations(): """ @@ -67,8 +74,11 @@ def _find_srv_for_query(path, query): # pylint: disable=unused-argument return 'http://api.worldweatheronline.com' def _load_content_and_headers(path, query): - timestamp = time.strftime("%Y%m%d%H", time.localtime()) - cache_file = os.path.join(PROXY_CACHEDIR, timestamp, path, query) + if is_testmode(): + cache_file = "test/proxy-data/data1" + else: + timestamp = time.strftime("%Y%m%d%H", time.localtime()) + cache_file = os.path.join(PROXY_CACHEDIR, timestamp, path, query) try: return (open(cache_file, 'r').read(), json.loads(open(cache_file+".headers", 'r').read())) @@ -97,7 +107,7 @@ def translate(text, lang): Translate `text` into `lang` """ translated = TRANSLATIONS.get(lang, {}).get(text, text) - if text.encode('utf-8') == translated: + if text == translated: print("%s: %s" % (lang, text)) return translated @@ -108,7 +118,7 @@ def cyr(to_translate): return cyrtranslit.to_cyrillic(to_translate) def _patch_greek(original): - return original.decode('utf-8').replace(u"Ηλιόλουστη/ο", u"Ηλιόλουστη").encode('utf-8') + return original.replace(u"Ηλιόλουστη/ο", u"Ηλιόλουστη") def add_translations(content, lang): """ @@ -132,16 +142,16 @@ def add_translations(content, lang): d['data']['current_condition'][0]['lang_%s' % lang] = \ [{'value': cyr( d['data']['current_condition'][0]['lang_%s' % lang][0]['value']\ - .encode('utf-8'))}] + )}] elif lang == 'el': d['data']['current_condition'][0]['lang_%s' % lang] = \ [{'value': _patch_greek( d['data']['current_condition'][0]['lang_%s' % lang][0]['value']\ - .encode('utf-8'))}] + )}] elif lang == 'sr-lat': d['data']['current_condition'][0]['lang_%s' % lang] = \ [{'value':d['data']['current_condition'][0]['lang_sr'][0]['value']\ - .encode('utf-8')}] + }] fixed_weather = [] for w in d['data']['weather']: # pylint: disable=invalid-name @@ -153,13 +163,13 @@ def add_translations(content, lang): [{'value': translate(weather_condition, lang)}] elif lang == 'sr': h['lang_%s' % lang] = \ - [{'value': cyr(h['lang_%s' % lang][0]['value'].encode('utf-8'))}] + [{'value': cyr(h['lang_%s' % lang][0]['value'])}] elif lang == 'el': h['lang_%s' % lang] = \ - [{'value': _patch_greek(h['lang_%s' % lang][0]['value'].encode('utf-8'))}] + [{'value': _patch_greek(h['lang_%s' % lang][0]['value'])}] elif lang == 'sr-lat': h['lang_%s' % lang] = \ - [{'value': h['lang_sr'][0]['value'].encode('utf-8')}] + [{'value': h['lang_sr'][0]['value']}] fixed_hourly.append(h) w['hourly'] = fixed_hourly fixed_weather.append(w) @@ -177,7 +187,7 @@ def proxy(path): """ lang = request.args.get('lang', 'en') - query_string = request.query_string + query_string = request.query_string.decode("utf-8") query_string = query_string.replace('sr-lat', 'sr') query_string = query_string.replace('lang=None', 'lang=en') query_string += "&extra=localObsTime" @@ -187,7 +197,6 @@ def proxy(path): if content is None: srv = _find_srv_for_query(path, query_string) url = '%s/%s?%s' % (srv, path, query_string) - print(url) attempts = 10 response = None @@ -208,10 +217,11 @@ def proxy(path): headers = {} headers['Content-Type'] = response.headers['content-type'] _save_content_and_headers(path, query_string, response.content, headers) - content = add_translations(response.content, lang) else: content = "{}" + content = add_translations(content, lang) + return content, 200, headers if __name__ == "__main__": From 16e9ba5740f40fa9777845c581912578932beda7 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 26 Apr 2020 19:37:41 +0200 Subject: [PATCH 31/73] added de and fr to PROXY_LANGS --- lib/translations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/translations.py b/lib/translations.py index 51c2497..254951a 100644 --- a/lib/translations.py +++ b/lib/translations.py @@ -17,13 +17,13 @@ PARTIAL_TRANSLATION = [ "hy", "is", "ja", "jv", "ka", "kk", "ko", "ky", "lt", "lv", "mk", "ml", "nl", "fy", "nn", "pt", "pt-br", "sk", "sl", "sr", "sr-lat", - "sv", "sw", "te", "uz", + "sv", "sw", "te", "uz", "zh", "zu", "he", ] PROXY_LANGS = [ "af", "az", "be", "bs", "ca", - "cy", "el", "eo", "et", "fa", + "cy", "de", "el", "eo", "et", "fa", "fr", "fy", "he", "hr", "hu", "hy", "ia", "id", "is", "it", "ja", "kk", "lv", "mk", "nb", "nn", "ro", From 40ad45ad039c2c59ee2a66e73c58decda4265712 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 26 Apr 2020 19:38:56 +0200 Subject: [PATCH 32/73] minor fixes --- lib/view/line.py | 2 +- lib/wttr_srv.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/view/line.py b/lib/view/line.py index 52c98a2..16db987 100644 --- a/lib/view/line.py +++ b/lib/view/line.py @@ -257,7 +257,7 @@ def format_weather_data(query, parsed_query, data): current_condition = data['data']['current_condition'][0] current_condition['location'] = parsed_query["location"] - current_condition['override_location'] = parsed_query["override_location"] + current_condition['override_location'] = parsed_query["override_location_name"] output = render_line(format_line, current_condition, query) return output diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index be2afa6..ac41f53 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -201,10 +201,6 @@ def _response(parsed_query, query, fast_mode=False): # at this point, we could not handle the query fast, # so we handle it with all available logic - - import json - print(json.dumps(parsed_query, indent=4)) - loc = (parsed_query['orig_location'] or "").lower() if parsed_query.get("view"): output = wttr_line(query, parsed_query) @@ -354,7 +350,8 @@ def wttr(location, request): response = get_message('CAPACITY_LIMIT_REACHED', parsed_query['lang']) # if exception is occured, we return not a png file but text - del parsed_query["png_filename"] + if "png_filename" in parsed_query: + del parsed_query["png_filename"] return _wrap_response( response, parsed_query['html_output'], png_filename=parsed_query.get('png_filename')) From df0b41fba5f2d64f3acb46e09ce2fbf2994e648c Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 26 Apr 2020 20:15:27 +0200 Subject: [PATCH 33/73] README.md: mentioned llvm and switched to python3 --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 18f9db2..87926ad 100644 --- a/README.md +++ b/README.md @@ -428,11 +428,15 @@ If you want to get weather reports as PNG files, you'll also need to install: You can install most of them using `pip`. +Some python package use LLVM, so install it first: + + $ apt-get install llvm-7 llvm-7-dev + If `virtualenv` is used: - $ virtualenv ve - $ ve/bin/pip install -r requirements.txt - $ ve/bin/python bin/srv.py + $ virtualenv -p python3 ve + $ ve/bin/pip3 install -r requirements.txt + $ ve/bin/python3 bin/srv.py Also, you need to install the geoip2 database. You can use a free database GeoLite2 that can be downloaded from (http://dev.maxmind.com/geoip/geoip2/geolite2/). From 26a5fa533ad09c86ee0a8df1e9edcddea8498202 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 27 Apr 2020 19:03:27 +0200 Subject: [PATCH 34/73] useragents fixed --- lib/globals.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/globals.py b/lib/globals.py index 0664d4a..7810fd6 100644 --- a/lib/globals.py +++ b/lib/globals.py @@ -79,7 +79,8 @@ PLAIN_TEXT_AGENTS = [ "lwp-request", "wget", "python-requests", - "OpenBSD ftp" + "openbsd ftp", + "powershell", ] PLAIN_TEXT_PAGES = [':help', ':bash.function', ':translation'] From 156ab97cf2eb71a8fd95241055f0816c0ab21de2 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 27 Apr 2020 19:03:58 +0200 Subject: [PATCH 35/73] do not add followme to json output --- lib/wttr_srv.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index ac41f53..62541ad 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -213,7 +213,9 @@ def _response(parsed_query, query, fast_mode=False): output = fmt.png.render_ansi( output, options=parsed_query) else: - if query.get('days', '3') != '0' and not query.get('no-follow-line'): + if query.get('days', '3') != '0' \ + and not query.get('no-follow-line') \ + and ((parsed_query.get("view") or "v2")[:2] in ["v2"]): if parsed_query['html_output']: output = add_buttons(output) else: From 2ef8435b3630b706202d401f723d9215e89ecaa4 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 27 Apr 2020 19:04:27 +0200 Subject: [PATCH 36/73] line.py: use new astral lib --- lib/view/line.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/lib/view/line.py b/lib/view/line.py index 16db987..09e7529 100644 --- a/lib/view/line.py +++ b/lib/view/line.py @@ -17,10 +17,8 @@ import sys import re import datetime import json -try: - from astral import Astral, Location -except ImportError: - pass + +from astral import moon from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION from weather_data import get_weather_data from . import v2 @@ -161,26 +159,32 @@ def render_location(data, query): location (l) """ - return (data['override_location'] or data['location']) # .title() + return (data['override_location'] or data['location']) def render_moonphase(_, query): - """ + """moonpahse(m) A symbol describing the phase of the moon """ - astral = Astral() - moon_index = int( - int(32.0*astral.moon_phase(date=datetime.datetime.today())/28+2)%32/4 - ) + moon_phase = moon.phase(date=datetime.datetime.today()) + moon_index = int(int(32.0*moon_phase/28+2)%32/4) return MOON_PHASES[moon_index] def render_moonday(_, query): - """ + """moonday(M) An number describing the phase of the moon (days after the New Moon) """ - astral = Astral() - return str(int(astral.moon_phase(date=datetime.datetime.today()))) + moon_phase = moon.phase(date=datetime.datetime.today()) + return str(int(moon_phase)) def render_sunset(data, query): + """ + sunset (s) + + NOT YET IMPLEMENTED + """ + + return "%s" + location = data['location'] city_name = location astral = Astral() From 0a8ab5e89f54338ace8210ce85003b257b8ec2dd Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 27 Apr 2020 19:19:41 +0200 Subject: [PATCH 37/73] one-letter options in png files (#436) --- lib/parse_query.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/parse_query.py b/lib/parse_query.py index 44be016..b8376c4 100644 --- a/lib/parse_query.py +++ b/lib/parse_query.py @@ -115,6 +115,7 @@ def parse_wttrin_png_name(name): parts = name.split('_') parsed['location'] = parts[0] + one_letter_options = "" for part in parts[1:]: if re.match('(?:[0-9]+)x', part): parsed['width'] = part[:-1] @@ -126,7 +127,10 @@ def parse_wttrin_png_name(name): arg, val = part.split('=', 1) to_be_parsed[arg] = val else: - to_be_parsed[part] = '' + one_letter_options += part + + for letter in one_letter_options: + to_be_parsed[letter] = '' parsed.update(parse_query(to_be_parsed)) From e041280f27a7d76f04b67eaef98ac05b310273b1 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 27 Apr 2020 20:00:26 +0200 Subject: [PATCH 38/73] do not pass query to view functions --- lib/parse_query.py | 5 ++--- lib/view/moon.py | 7 ++----- lib/view/wttr.py | 12 ++++++++---- lib/wttr_srv.py | 6 +++--- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/lib/parse_query.py b/lib/parse_query.py index b8376c4..f086c01 100644 --- a/lib/parse_query.py +++ b/lib/parse_query.py @@ -69,8 +69,6 @@ def parse_query(args): if days in q: result['days'] = days - result['no-caption'] = False - result['no-city'] = False if 'q' in q: result['no-caption'] = True if 'Q' in q: @@ -83,7 +81,8 @@ def parse_query(args): val = True if val == 'False': val = False - result[key] = val + if val: + result[key] = val # currently `view` is alias for `format` if "format" in result and not result.get("view"): diff --git a/lib/view/moon.py b/lib/view/moon.py index 2aa5da4..a4d8255 100644 --- a/lib/view/moon.py +++ b/lib/view/moon.py @@ -10,15 +10,12 @@ import constants import parse_query import globals -def get_moon(query, parsed_query): +def get_moon(parsed_query): location = parsed_query['orig_location'] html = parsed_query['html_output'] lang = parsed_query['lang'] - if query is None: - query = {} - date = None if '@' in location: date = location[location.index('@')+1:] @@ -40,7 +37,7 @@ def get_moon(query, parsed_query): stdout = p.communicate()[0] stdout = stdout.decode("utf-8") - if query.get('no-terminal', False): + if parsed_query.get('no-terminal', False): stdout = globals.remove_ansi(stdout) if html: diff --git a/lib/view/wttr.py b/lib/view/wttr.py index ccd57a0..4007776 100644 --- a/lib/view/wttr.py +++ b/lib/view/wttr.py @@ -12,6 +12,7 @@ import sys import os import re import time +import hashlib sys.path.insert(0, "..") from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS @@ -23,7 +24,7 @@ def _is_invalid_location(location): if '.png' in location: return True -def get_wetter(query, parsed_query): +def get_wetter(parsed_query): location = parsed_query['location'] ip = parsed_query['ip_addr'] @@ -83,7 +84,10 @@ def get_wetter(query, parsed_query): if location_name is None: location_name = "" - return "%s/%s/%s%s%s%s%s" % (CACHEDIR, location, timestamp, imperial_suffix, lang_suffix, query_line, location_name) + + filename = "".join([timestamp, imperial_suffix, lang_suffix, query_line, location_name]) + digest = hashlib.sha1(filename.encode('utf-8')).hexdigest() + return "%s/%s/%s" % (CACHEDIR, location, digest) def save_weather_data(location, filename, lang=None, query=None, location_name=None, full_address=None): @@ -212,9 +216,9 @@ def get_wetter(query, parsed_query): stdout = re.sub("", "" + title + opengraph, stdout) open(filename+'.html', 'w').write(stdout) - filename = get_filename(location, lang=lang, query=query, location_name=location_name) + filename = get_filename(location, lang=lang, query=parsed_query, location_name=location_name) if not os.path.exists(filename): - save_weather_data(location, filename, lang=lang, query=query, location_name=location_name, full_address=full_address) + save_weather_data(location, filename, lang=lang, query=parsed_query, location_name=location_name, full_address=full_address) if html: filename += '.html' diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index 62541ad..c5c5825 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -205,9 +205,9 @@ def _response(parsed_query, query, fast_mode=False): if parsed_query.get("view"): output = wttr_line(query, parsed_query) elif loc == 'moon' or loc.startswith('moon@'): - output = get_moon(query, parsed_query) + output = get_moon(parsed_query) else: - output = get_wetter(query, parsed_query) + output = get_wetter(parsed_query) if parsed_query.get('png_filename'): output = fmt.png.render_ansi( @@ -291,6 +291,7 @@ def parse_request(location, request, query, fast_mode=False): 'country': country, 'query_source_location': query_source_location}) + parsed_query.update(query) return parsed_query @@ -338,7 +339,6 @@ def wttr(location, request): # use the full track parsed_query = parse_request(location, request, query, fast_mode=True) response = _response(parsed_query, query, fast_mode=True) - try: if not response: parsed_query = parse_request(location, request, query) From cecde8fec8b7a0328bac07613cc11041b0b5e780 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 27 Apr 2020 20:40:13 +0200 Subject: [PATCH 39/73] fixed location for localhost --- lib/location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/location.py b/lib/location.py index f47e833..20f5a12 100644 --- a/lib/location.py +++ b/lib/location.py @@ -264,7 +264,7 @@ def location_processing(location, ip_addr): # if location is not None and location.upper() in IATA_CODES: # location = '~%s' % location - if location is not None and location.startswith('~'): + if location is not None and not location.startswith("~-,") and location.startswith('~'): geolocation = geolocator(location_canonical_name(location[1:])) if geolocation is not None: if not override_location_name: From 88955e70ee5fef3503b762a2abc63eb1927b0a68 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 27 Apr 2020 20:40:58 +0200 Subject: [PATCH 40/73] updated tests signatures --- test/test-data/signatures | 60 +++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/test/test-data/signatures b/test/test-data/signatures index edb43a2..54e1b22 100644 --- a/test/test-data/signatures +++ b/test/test-data/signatures @@ -1,68 +1,68 @@ -8f27084b6294ddbe28dbcbf98f798730e8a79289 371718f3918eb35adeb2cfe8f6fbc52adef39520 / +8f27084b6294ddbe28dbcbf98f798730e8a79289 4ee2dd9cf8f5818902647ff832ef40d690096bf1 / ae537911bb7b0568f478073e661abee1cb4ff941 d123e570da22dee9798d353c4281cb5a2bdbaeac /Kiev 4dc586807c16020b9f4dbb705326c698bea41665 a186d89e95061a7887c005ffa8bd1e29362de2da /Kiev.png -3db1938bedc0ee0047bf3b043ddaf0aba1912f13 b85ed0c0c0214016eabdcc0283e7b8f4f682f5a9 /?T +3db1938bedc0ee0047bf3b043ddaf0aba1912f13 febab92af9526163bc9e502ecd7fa4225345e6f6 /?T 2cc0ba7a57a6342e72fd7142ca18dbb0eae69416 ce7fb7a88cab697f5280ddabf344f0d397888956 /Киев 928142e88da142ea8075cbfe09bfef349e72dbb1 0f86f59a45b4485fea1375ca945503d9abb9a96d /Kiev?2 -4f6f0a16ff415fad1c102c8023c5d8365ef63402 16d85d2b01441f40b1a60f45603800923580b971 /Kiev?format=1 -c99903b86971ccccfcca4f13e6fca72776b4fbcf bd4c56b36d0c86e805702b5e6c71ba2b1cbf93c1 /Kiev?format=2 -2a0d6cd8d30a84328580611ca6dd6bed1d805a04 b9a5df200b9dd035d2fee4add1a938f621b2aa98 /Kiev?format=3 -4e4e15eeb6a8b6ddb5d00591c4b5e9b74a13e6bc c31ccd5f70ef1f317ff4ad99de5b6b9affaceb39 /Kiev?format=4 -a27d3e4ad7f820124ef57c9299715bc61cb71387 18afd7989100da758f68add862b0a2730e0c79f2 /Kiev?format=v2 +4f6f0a16ff415fad1c102c8023c5d8365ef63402 4a73927d12540efae05a0af874e63283d0d2c310 /Kiev?format=1 +c99903b86971ccccfcca4f13e6fca72776b4fbcf 390bbc1a1e11e99c5ef713484ee8f3e7c79d04a4 /Kiev?format=2 +2a0d6cd8d30a84328580611ca6dd6bed1d805a04 bd08c36eba08dcef794a95b1ac098d981de78ec0 /Kiev?format=3 +4e4e15eeb6a8b6ddb5d00591c4b5e9b74a13e6bc f64a3de698cedace25ca6834cf18094c65ac7855 /Kiev?format=4 +a27d3e4ad7f820124ef57c9299715bc61cb71387 e4fe976ce280eaad650391e93786995749a2d9ed /Kiev?format=v2 83cc0ef08c24ad7ecc81d1a6cbd693bb06214ece 212431752d0b0d21fabf073e98efa3f1bc24e76e /:help 310b64f65fc9f66a5142bf6104f4f9b9d5eef0ea b0bd07f0c87aae9464c091ccb955f41ec6973098 /Kiev?T 9bd1b460d4927df24724f45f69bd3132f3de8e04 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 /Kiev?p 3ee1a25d436799804d7ebd8371d8022fa55a71d7 80f18be012d0471dce9fcd2b500f482bcd635347 /Kiev?q e0e8e7eca16bfac88503ac6d19a7a6c8b469c0fd d5d070c98237f0dffc82b176039f90a15f03a667 /Kiev?Q -d08d1fa2546fee0717d1eb663cf63cd1505b8885 5ccbb4c950bac7f33f1996af856f9678a4a47199 /Kiev_text=no_view=v2.png -3e1be80e942a2ea5450c60e1c0ebfb154aca3da1 76bdcb958640233fa0048bb14c9245f18b14f7c4 -A firefox / +d08d1fa2546fee0717d1eb663cf63cd1505b8885 e380ef1a22f62a7fa1133d0e35d923c8587cb3ed /Kiev_text=no_view=v2.png +3e1be80e942a2ea5450c60e1c0ebfb154aca3da1 6a5bdefe64689f4d05128bd62a8118f4f2f52043 -A firefox / ee6bf0665c2719cda3ec1fbdb80413d821c99b8e 3f9c5091269ece259cce13fc842265019001ed54 -A firefox /Kiev 98ef11678b7fd33425f97eeee70e00cd96206539 a186d89e95061a7887c005ffa8bd1e29362de2da -A firefox /Kiev.png -ecbcf2cb9004a754c4559ce7e92fead68f71721a bfd43c9cb44736e26752741ab2f500f9ca06da1e -A firefox /?T +ecbcf2cb9004a754c4559ce7e92fead68f71721a 2ea6d52a2108a481cbc0f44a881eb88642d68e80 -A firefox /?T 74206d869128383dba2d840b848b90eb376fd851 7c6ce53ff25d91a5f46baa30077b69e0f09f2571 -A firefox /Киев 91b89025b5acd56ca475924e0eb559a9734f3333 dbd49d93eff2b2cf82f7d266f90de950207a0561 -A firefox /Kiev?2 -e6cb82dab95e05167ffbcb90a10d6cb03cd02ec1 16d85d2b01441f40b1a60f45603800923580b971 -A firefox /Kiev?format=1 -e65bc57e8d1df26c442a9ecf45afee390ff331a3 bd4c56b36d0c86e805702b5e6c71ba2b1cbf93c1 -A firefox /Kiev?format=2 -d743b331d5f4c81bbc8b168ce84a99ab22dc70cf b9a5df200b9dd035d2fee4add1a938f621b2aa98 -A firefox /Kiev?format=3 -bf359ee92690c3a3061542dc6e78cb42ca837412 c31ccd5f70ef1f317ff4ad99de5b6b9affaceb39 -A firefox /Kiev?format=4 -cb875772a6610c991b95b3fbfa22fc7192e25843 9b8e68e9701097316bdacfb13b0c07df23fc0aee -A firefox /Kiev?format=v2 +e6cb82dab95e05167ffbcb90a10d6cb03cd02ec1 4a73927d12540efae05a0af874e63283d0d2c310 -A firefox /Kiev?format=1 +e65bc57e8d1df26c442a9ecf45afee390ff331a3 390bbc1a1e11e99c5ef713484ee8f3e7c79d04a4 -A firefox /Kiev?format=2 +d743b331d5f4c81bbc8b168ce84a99ab22dc70cf bd08c36eba08dcef794a95b1ac098d981de78ec0 -A firefox /Kiev?format=3 +bf359ee92690c3a3061542dc6e78cb42ca837412 f64a3de698cedace25ca6834cf18094c65ac7855 -A firefox /Kiev?format=4 +cb875772a6610c991b95b3fbfa22fc7192e25843 ec26ea8a3d920e88b2f384a3ba53615eb7c517e9 -A firefox /Kiev?format=v2 d520af45b491689d53024c696955db8b1e4eaa87 a76e0c26bac17e43af879db6cb11a3cd9eda2344 -A firefox /:help 6b80492b79a4cc510cb4a9654cb6ff085cdc1943 801d4c6d6837c9604944168a3930dfe05b0e9f5d -A firefox /Kiev?T e13d7449ce756e55ba3e84e4b7e601b36d0044b6 c5a87804710ab70b8798f56579d276f4ce1806bf -A firefox /Kiev?p 3f6c192a6da5b79ea59ef94e99b9cbf4b0e7ede2 372aca50f441920ad623d62ee8fcde46d609f6f9 -A firefox /Kiev?q f28ca2a7a47f4859eac7d0307ec7ac67a40e0adf 1b4b66d58bd7e27abeeca45581e686f12fefe76a -A firefox /Kiev?Q -62c5029cf297b1434c57228dc8c8cdfb5e68285d 5ccbb4c950bac7f33f1996af856f9678a4a47199 -A firefox /Kiev_text=no_view=v2.png -ed573b89ca5522d6ab69dc1686b98b00391076bd 9cd1b364c12acd74899ae34bfb9731f70b0b9b68 -H Accept-Language:ru / +62c5029cf297b1434c57228dc8c8cdfb5e68285d e380ef1a22f62a7fa1133d0e35d923c8587cb3ed -A firefox /Kiev_text=no_view=v2.png +ed573b89ca5522d6ab69dc1686b98b00391076bd 5ce7ea58bf02bff008baa3193b2db498268a244b -H Accept-Language:ru / b879673f66235bbf1913ff9abc58aff2fb8962d1 00a96a5d83608c2dad7921862bb3f244775f6b19 -H Accept-Language:ru /Kiev 83d99896cf866ecbaa6d2c64c12bd31bc7b35068 92dc07acb93633974eaff19e8c1a99e590e140d9 -H Accept-Language:ru /Kiev.png -9cbb6aa3e0b46e78229a32688db1cced9a44271d 1f82fb278d40520b4c1ef7378e1a5abc868c1f3e -H Accept-Language:ru /?T +9cbb6aa3e0b46e78229a32688db1cced9a44271d b368cc8f39e7a7ced04e3f4e6506e1eb4551e904 -H Accept-Language:ru /?T 095d8d38c667923131801595b903e007b5f902f3 4ede3397f9def696adc7ecf3ffd46a59b8fb25cb -H Accept-Language:ru /Киев 4e6cdfc38c9d9f2436438b345776c42cb8cab8a5 1b00c96a05f9daea8248a8e063d990797be933ad -H Accept-Language:ru /Kiev?2 -b7d8d0f0bb4c38aabac468c9a354bb4e2b401893 c97ca64c11eaa714b472bf723a3353abd4c5bd09 -H Accept-Language:ru /Kiev?format=1 -8f3bbfc9be6418e82edacecc54a9f3e9f26b7fbf 8a29f5bf1e412e2942a701b88dfcf382072b3f34 -H Accept-Language:ru /Kiev?format=2 -f1d4178892fd3dc38e9f966112d317859acc9122 8181bd5a0d7fff5b420d480160b9ea0e14d45aeb -H Accept-Language:ru /Kiev?format=3 -cf44e154504d9bc2b9b6066bbb0f5d52fc12f13e daca309c8ef168b97998e86dbbaab2d3963495b8 -H Accept-Language:ru /Kiev?format=4 -4955c849f67da53203b8c96b15a0bf0a4a471bc6 03afac96b43c7f1b2622b1015b8a4a3cb721c50a -H Accept-Language:ru /Kiev?format=v2 +b7d8d0f0bb4c38aabac468c9a354bb4e2b401893 4a73927d12540efae05a0af874e63283d0d2c310 -H Accept-Language:ru /Kiev?format=1 +8f3bbfc9be6418e82edacecc54a9f3e9f26b7fbf 390bbc1a1e11e99c5ef713484ee8f3e7c79d04a4 -H Accept-Language:ru /Kiev?format=2 +f1d4178892fd3dc38e9f966112d317859acc9122 bd08c36eba08dcef794a95b1ac098d981de78ec0 -H Accept-Language:ru /Kiev?format=3 +cf44e154504d9bc2b9b6066bbb0f5d52fc12f13e f64a3de698cedace25ca6834cf18094c65ac7855 -H Accept-Language:ru /Kiev?format=4 +4955c849f67da53203b8c96b15a0bf0a4a471bc6 c6c09755bb4639c277b48c4ef8219d3a0e1d053a -H Accept-Language:ru /Kiev?format=v2 3f69f4a605ce88643b4e0d62a588c92625d41aea 237e5cf3a0f4737df49d8382fc8a84f41603cbe5 -H Accept-Language:ru /:help 08553ca4bf71c738c4321fe7d84b4e6ff830956f 016fc03b18a8902f838719bbc171184603c08b60 -H Accept-Language:ru /Kiev?T b70f8b3fc8aee126c04b27b0d3b4c503b4292cbf b60b68a9e77275884812f7e52b06f6012ba5682a -H Accept-Language:ru /Kiev?p 400efdba61125f8cb850d7c33caf4fc2739a960b 5ee4a043a91509ef57aec46a14a0c24f09e8ec47 -H Accept-Language:ru /Kiev?q b9fd454e73343f262a6d99dd80487495bd647c6f a10718896a07baadb87adf2bf0026b1f00252213 -H Accept-Language:ru /Kiev?Q -8fed034e57624d0e0b33140673094e56e04087bc c7367485784883041c62f4c284aeacf690df71b4 -H Accept-Language:ru /Kiev_text=no_view=v2.png +8fed034e57624d0e0b33140673094e56e04087bc 83d236565782aff7416bf526b38636148d6ba15a -H Accept-Language:ru /Kiev_text=no_view=v2.png 3ce3dd46413f236244410f142a4b44356a0cedf9 1c2eea391b35c8bfed2541435e1788307aa06bc1 -H X-Forwarded-For:1.1.1.1 / 89be0a5787592298ce34f10b36da7ee87d1a1353 d123e570da22dee9798d353c4281cb5a2bdbaeac -H X-Forwarded-For:1.1.1.1 /Kiev a9977eadc628b1ede5d4f91ee103dfb740caa2b1 a186d89e95061a7887c005ffa8bd1e29362de2da -H X-Forwarded-For:1.1.1.1 /Kiev.png eec20c6be5e528967cddf6d0b72c84dbda553d43 9a39dbafa7e1550d374e38059c0f4b8f437e1739 -H X-Forwarded-For:1.1.1.1 /?T e304153f0e1e9b41781bf4eb6fb6c4a5b7513aec ce7fb7a88cab697f5280ddabf344f0d397888956 -H X-Forwarded-For:1.1.1.1 /Киев 98f0b3a28863a861c6ac6d89ee5d49adb7f3f518 0f86f59a45b4485fea1375ca945503d9abb9a96d -H X-Forwarded-For:1.1.1.1 /Kiev?2 -cf04d7fe2cf36eba8d7fb4fa6def1c9015036456 16d85d2b01441f40b1a60f45603800923580b971 -H X-Forwarded-For:1.1.1.1 /Kiev?format=1 -67fbe9168566709450eb35d36c60c27105335a7e bd4c56b36d0c86e805702b5e6c71ba2b1cbf93c1 -H X-Forwarded-For:1.1.1.1 /Kiev?format=2 -b2604348bf39774c85b7c18ae7b51f63a2c9f31a b9a5df200b9dd035d2fee4add1a938f621b2aa98 -H X-Forwarded-For:1.1.1.1 /Kiev?format=3 -cf012400156c842e569b6a9f05b094e6b75348cd c31ccd5f70ef1f317ff4ad99de5b6b9affaceb39 -H X-Forwarded-For:1.1.1.1 /Kiev?format=4 -1f4981348cab19df9846cd3b3923ee7a972ff9fa 18afd7989100da758f68add862b0a2730e0c79f2 -H X-Forwarded-For:1.1.1.1 /Kiev?format=v2 +cf04d7fe2cf36eba8d7fb4fa6def1c9015036456 4a73927d12540efae05a0af874e63283d0d2c310 -H X-Forwarded-For:1.1.1.1 /Kiev?format=1 +67fbe9168566709450eb35d36c60c27105335a7e 390bbc1a1e11e99c5ef713484ee8f3e7c79d04a4 -H X-Forwarded-For:1.1.1.1 /Kiev?format=2 +b2604348bf39774c85b7c18ae7b51f63a2c9f31a bd08c36eba08dcef794a95b1ac098d981de78ec0 -H X-Forwarded-For:1.1.1.1 /Kiev?format=3 +cf012400156c842e569b6a9f05b094e6b75348cd f64a3de698cedace25ca6834cf18094c65ac7855 -H X-Forwarded-For:1.1.1.1 /Kiev?format=4 +1f4981348cab19df9846cd3b3923ee7a972ff9fa e4fe976ce280eaad650391e93786995749a2d9ed -H X-Forwarded-For:1.1.1.1 /Kiev?format=v2 767a7407c14049fd77a6a2fedd1d8b35f6e47e0d 212431752d0b0d21fabf073e98efa3f1bc24e76e -H X-Forwarded-For:1.1.1.1 /:help 10631d55b42e7bc5ec15ffc5cddae712785eb354 b0bd07f0c87aae9464c091ccb955f41ec6973098 -H X-Forwarded-For:1.1.1.1 /Kiev?T 031478f562663eb9f577b04032993e2f098146f6 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 -H X-Forwarded-For:1.1.1.1 /Kiev?p e106cd21a6b67196159c2baa023142e3a8859612 80f18be012d0471dce9fcd2b500f482bcd635347 -H X-Forwarded-For:1.1.1.1 /Kiev?q 1a16c9b52ba90cb7ad3dd8902bf41b31a287d49e d5d070c98237f0dffc82b176039f90a15f03a667 -H X-Forwarded-For:1.1.1.1 /Kiev?Q -83bd9cc6a646e44b75524474dd32f0fd1f5c5a39 5ccbb4c950bac7f33f1996af856f9678a4a47199 -H X-Forwarded-For:1.1.1.1 /Kiev_text=no_view=v2.png +83bd9cc6a646e44b75524474dd32f0fd1f5c5a39 e380ef1a22f62a7fa1133d0e35d923c8587cb3ed -H X-Forwarded-For:1.1.1.1 /Kiev_text=no_view=v2.png From 67d4f9bfd1be0ee21d4310bb5196105dbc0fdf2f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 27 Apr 2020 20:43:42 +0200 Subject: [PATCH 41/73] added tests for one-letter options in png files (#436) --- test/query.sh | 2 ++ test/test-data/signatures | 8 ++++++++ 2 files changed, 10 insertions(+) diff --git a/test/query.sh b/test/query.sh index 7d1eb60..6b7445a 100644 --- a/test/query.sh +++ b/test/query.sh @@ -16,6 +16,8 @@ queries=( "/Kiev?q" "/Kiev?Q" "/Kiev_text=no_view=v2.png" + "/Kiev.png?1nqF" + "/Kiev_1nqF.png" ) options=$(cat < Date: Wed, 29 Apr 2020 21:35:23 +0200 Subject: [PATCH 42/73] v2+png: new experimental query encoding --- lib/parse_query.py | 31 +++++++++++++++++++++++++++++++ lib/view/v2.py | 8 ++++++-- lib/wttr_srv.py | 6 ++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/lib/parse_query.py b/lib/parse_query.py index f086c01..6c45e56 100644 --- a/lib/parse_query.py +++ b/lib/parse_query.py @@ -1,4 +1,35 @@ import re +import json +import zlib +import base64 + +def serialize(parsed_query): + return base64.b64encode( + zlib.compress( + json.dumps(parsed_query).encode("utf-8")), + altchars=b"-_").decode("utf-8") + +def deserialize(url): + + string = url[2:] + + extension = None + if "." in string: + string, extension = string.split(".", 1) + + try: + result = json.loads( + zlib.decompress( + base64.b64decode(string, altchars=b"-_")).decode("utf-8")) + except zlib.error: + return None + + if extension == "png": + result["png_filename"] = url + result["html_output"] = False + + return result + def metric_or_imperial(query, lang, us_ip=False): """ diff --git a/lib/view/v2.py b/lib/view/v2.py index c2d0707..22a7518 100644 --- a/lib/view/v2.py +++ b/lib/view/v2.py @@ -42,6 +42,7 @@ from babel.dates import format_datetime from globals import WWO_KEY import constants import translations +import parse_query from . import line as wttr_line if not sys.version_info >= (3, 0): @@ -543,6 +544,9 @@ def main(query, parsed_query, data): else: data_parsed = data + parsed_query["text"] = "no" + filename = "b_" + parse_query.serialize(parsed_query) + ".png" + if html_output: output = """ @@ -551,14 +555,14 @@ def main(query, parsed_query, data): - +
 {textual_information}
 
""".format( - orig_location=parsed_query["orig_location"], + filename=filename, orig_location=parsed_query["orig_location"], textual_information=textual_information( data_parsed, geo_data, parsed_query, html_output=True)) else: diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index c5c5825..2ae69c1 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -247,6 +247,12 @@ def parse_request(location, request, query, fast_mode=False): Return: dictionary with parsed parameters """ + if location.startswith("b_"): + result = parse_query.deserialize(location) + result["request_url"] = request.url + if result: + return result + png_filename = None if location is not None and location.lower().endswith(".png"): png_filename = location From 192e6336070a52225a186348ed2d48f9d65d5d05 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 30 Apr 2020 11:20:32 +0200 Subject: [PATCH 43/73] cache.py: add timestamp to each entry --- lib/cache.py | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/lib/cache.py b/lib/cache.py index 7ba41e4..a8bec58 100644 --- a/lib/cache.py +++ b/lib/cache.py @@ -7,6 +7,7 @@ import re import time import os import hashlib +import random import pytz import pylru @@ -35,9 +36,8 @@ def get_signature(user_agent, query_string, client_ip_address, lang): `lang`, and `client_ip_address` """ - timestamp = int(time.time() / 1000) - signature = "%s:%s:%s:%s:%s" % \ - (user_agent, query_string, client_ip_address, lang, timestamp) + signature = "%s:%s:%s:%s" % \ + (user_agent, query_string, client_ip_address, lang) print(signature) return signature @@ -48,8 +48,13 @@ def get(signature): the `_update_answer` function. """ - value = CACHE.get(signature) - if value: + value_record = CACHE.get(signature) + if not value_record: + return None + + value = value_record["val"] + expiry = value_record["expiry"] + if value and time.time() < expiry: if value.startswith("file:") or value.startswith("bfile:"): value = _read_from_file(signature, sighash=value) if not value: @@ -57,14 +62,26 @@ def get(signature): return _update_answer(value) return None +def _randint(minimum, maximum): + return random.randrange(maximum - minimum) + def store(signature, value): """ Store in cache `value` for `signature` """ - if len(value) < MIN_SIZE_FOR_FILECACHE: - CACHE[signature] = value + + if len(value) >= MIN_SIZE_FOR_FILECACHE: + value_to_store = _store_in_file(signature, value) else: - CACHE[signature] = _store_in_file(signature, value) + value_to_store = value + + value_record = { + "val": value_to_store, + "expiry": time.time() + _randint(1000, 2000)*1000, + } + + CACHE[signature] = value_record + return _update_answer(value) def _hash(signature): From acb761be0dcbd703d3ca4f51a0ddeb6568f852b3 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 30 Apr 2020 11:21:00 +0200 Subject: [PATCH 44/73] v2 html: minor fix --- lib/view/v2.py | 5 ++--- lib/wttr_srv.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/view/v2.py b/lib/view/v2.py index 22a7518..6a9e122 100644 --- a/lib/view/v2.py +++ b/lib/view/v2.py @@ -544,10 +544,9 @@ def main(query, parsed_query, data): else: data_parsed = data - parsed_query["text"] = "no" - filename = "b_" + parse_query.serialize(parsed_query) + ".png" - if html_output: + parsed_query["text"] = "no" + filename = "b_" + parse_query.serialize(parsed_query) + ".png" output = """ diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index 2ae69c1..d8892e8 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -247,7 +247,7 @@ def parse_request(location, request, query, fast_mode=False): Return: dictionary with parsed parameters """ - if location.startswith("b_"): + if location and location.startswith("b_"): result = parse_query.deserialize(location) result["request_url"] = request.url if result: From 3dcfca45fe3c74e8f4c3783523b9495e1ce4fb71 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 30 Apr 2020 11:21:14 +0200 Subject: [PATCH 45/73] updated test for v2 html --- test/test-data/signatures | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test-data/signatures b/test/test-data/signatures index 9a41050..3e4c52c 100644 --- a/test/test-data/signatures +++ b/test/test-data/signatures @@ -27,7 +27,7 @@ e6cb82dab95e05167ffbcb90a10d6cb03cd02ec1 4a73927d12540efae05a0af874e63283d0d2c31 e65bc57e8d1df26c442a9ecf45afee390ff331a3 390bbc1a1e11e99c5ef713484ee8f3e7c79d04a4 -A firefox /Kiev?format=2 d743b331d5f4c81bbc8b168ce84a99ab22dc70cf bd08c36eba08dcef794a95b1ac098d981de78ec0 -A firefox /Kiev?format=3 bf359ee92690c3a3061542dc6e78cb42ca837412 f64a3de698cedace25ca6834cf18094c65ac7855 -A firefox /Kiev?format=4 -cb875772a6610c991b95b3fbfa22fc7192e25843 ec26ea8a3d920e88b2f384a3ba53615eb7c517e9 -A firefox /Kiev?format=v2 +cb875772a6610c991b95b3fbfa22fc7192e25843 9ff741ffc637f42dc38bddeb645c10cf3a45ef4f -A firefox /Kiev?format=v2 d520af45b491689d53024c696955db8b1e4eaa87 a76e0c26bac17e43af879db6cb11a3cd9eda2344 -A firefox /:help 6b80492b79a4cc510cb4a9654cb6ff085cdc1943 801d4c6d6837c9604944168a3930dfe05b0e9f5d -A firefox /Kiev?T e13d7449ce756e55ba3e84e4b7e601b36d0044b6 c5a87804710ab70b8798f56579d276f4ce1806bf -A firefox /Kiev?p From 8bcdcb88fe0c5b0ea537e536e45d2549a3e3517d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 30 Apr 2020 21:51:10 +0200 Subject: [PATCH 46/73] added share/static/example-tmux-status-line.png --- share/static/example-tmux-status-line.png | Bin 0 -> 15245 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 share/static/example-tmux-status-line.png diff --git a/share/static/example-tmux-status-line.png b/share/static/example-tmux-status-line.png new file mode 100644 index 0000000000000000000000000000000000000000..2f755f67e65ac6d8a0e3000412c2f02c919a1216 GIT binary patch literal 15245 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 Date: Thu, 30 Apr 2020 21:57:25 +0200 Subject: [PATCH 47/73] /Moon language fixed --- lib/view/moon.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/view/moon.py b/lib/view/moon.py index a4d8255..8fc8c18 100644 --- a/lib/view/moon.py +++ b/lib/view/moon.py @@ -22,6 +22,9 @@ def get_moon(parsed_query): location = location[:location.index('@')] cmd = [globals.PYPHOON] + if lang: + cmd += ["-l", lang] + if date: try: dateutil.parser.parse(date) @@ -30,10 +33,7 @@ def get_moon(parsed_query): else: cmd += [date] - env = os.environ.copy() - if lang: - env['LANG'] = lang - p = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env) + p = Popen(cmd, stdout=PIPE, stderr=PIPE) stdout = p.communicate()[0] stdout = stdout.decode("utf-8") From ea0786edbff2ffd7962376d96a577ff409eb4f22 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 1 May 2020 16:07:23 +0200 Subject: [PATCH 48/73] bin/proxy.py: smoothed cache load --- bin/proxy.py | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/bin/proxy.py b/bin/proxy.py index 59a6fad..88bff95 100644 --- a/bin/proxy.py +++ b/bin/proxy.py @@ -23,6 +23,7 @@ import sys import os import time import json +import hashlib import requests import cyrtranslit @@ -73,29 +74,44 @@ TRANSLATIONS = load_translations() def _find_srv_for_query(path, query): # pylint: disable=unused-argument return 'http://api.worldweatheronline.com' +def _cache_file(path, query): + """Return cache file name for specified `path` and `query` + and for the current time. + + Do smooth load on the server, expiration time + is slightly varied basing on the path+query sha1 hash digest. + """ + + digest = hashlib.sha1(("%s %s" % (path, query)).encode("utf-8")).hexdigest() + digest_number = ord(digest[0]) + expiry_interval = 60*(30+digest_number) + + timestamp = "%010d" % (int(time.time())//1000/expiry_interval*expiry_interval) + filename = os.path.join(PROXY_CACHEDIR, timestamp, path, query) + + return filename + + def _load_content_and_headers(path, query): if is_testmode(): cache_file = "test/proxy-data/data1" else: - timestamp = time.strftime("%Y%m%d%H", time.localtime()) - cache_file = os.path.join(PROXY_CACHEDIR, timestamp, path, query) + cache_file = _cache_file(path, query) try: return (open(cache_file, 'r').read(), json.loads(open(cache_file+".headers", 'r').read())) except IOError: return None, None -def _touch_empty_file(path, query, content, headers): - timestamp = time.strftime("%Y%m%d%H", time.localtime()) - cache_file = os.path.join(PROXY_CACHEDIR + ".empty", timestamp, path, query) +def _touch_empty_file(path, query): + cache_file = _cache_file(path, query) cache_dir = os.path.dirname(cache_file) if not os.path.exists(cache_dir): os.makedirs(cache_dir) open(cache_file, 'w').write("") def _save_content_and_headers(path, query, content, headers): - timestamp = time.strftime("%Y%m%d%H", time.localtime()) - cache_file = os.path.join(PROXY_CACHEDIR, timestamp, path, query) + cache_file = _cache_file(path, query) cache_dir = os.path.dirname(cache_file) if not os.path.exists(cache_dir): os.makedirs(cache_dir) @@ -212,7 +228,7 @@ def proxy(path): except ValueError: attempts -= 1 - _touch_empty_file(path, query_string, content, headers) + _touch_empty_file(path, query_string) if response: headers = {} headers['Content-Type'] = response.headers['content-type'] From b2550c9441b01f2af8fe548bebf316cf3d1e77e9 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 1 May 2020 17:49:41 +0200 Subject: [PATCH 49/73] bin/proxy.py: minor fixes --- bin/proxy.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/bin/proxy.py b/bin/proxy.py index 88bff95..7ec909a 100644 --- a/bin/proxy.py +++ b/bin/proxy.py @@ -116,7 +116,7 @@ def _save_content_and_headers(path, query, content, headers): if not os.path.exists(cache_dir): os.makedirs(cache_dir) open(cache_file + ".headers", 'w').write(json.dumps(headers)) - open(cache_file, 'w').write(content) + open(cache_file, 'wb').write(content) def translate(text, lang): """ @@ -141,13 +141,18 @@ def add_translations(content, lang): Add `lang` translation to `content` (JSON) returned by the data source """ + + if content is "{}": + return {} + languages_to_translate = TRANSLATIONS.keys() try: d = json.loads(content) # pylint: disable=invalid-name - except ValueError as exception: + except (ValueError, TypeError) as exception: print("---") print(exception) print("---") + return {} try: weather_condition = d['data']['current_condition'][0]['weatherDesc'][0]['value'] @@ -233,6 +238,7 @@ def proxy(path): headers = {} headers['Content-Type'] = response.headers['content-type'] _save_content_and_headers(path, query_string, response.content, headers) + content = response.content else: content = "{}" From 6c7600c309634b5418460336b7df04259f152227 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 1 May 2020 18:48:19 +0200 Subject: [PATCH 50/73] removed space before km/h (#433) --- lib/view/line.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/view/line.py b/lib/view/line.py index 09e7529..2f410be 100644 --- a/lib/view/line.py +++ b/lib/view/line.py @@ -143,13 +143,13 @@ def render_wind(data, query): wind_direction = "" if query.get('use_ms_for_wind', False): - unit = ' m/s' + unit = 'm/s' wind = u'%s%.1f%s' % (wind_direction, float(data['windspeedKmph'])/36.0*10.0, unit) elif query.get('use_imperial', False): - unit = ' mph' + unit = 'mph' wind = u'%s%s%s' % (wind_direction, data['windspeedMiles'], unit) else: - unit = ' km/h' + unit = 'km/h' wind = u'%s%s%s' % (wind_direction, data['windspeedKmph'], unit) return wind From 488160c6363f9065511ebeee973952599bb8e193 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 1 May 2020 18:53:47 +0200 Subject: [PATCH 51/73] gevent: patch httplib explicitly --- bin/proxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/proxy.py b/bin/proxy.py index 7ec909a..cd12900 100644 --- a/bin/proxy.py +++ b/bin/proxy.py @@ -16,7 +16,7 @@ from __future__ import print_function from gevent.pywsgi import WSGIServer from gevent.monkey import patch_all -patch_all() +patch_all(httplib=True) # pylint: disable=wrong-import-position,wrong-import-order import sys From 87c82ef596b3ecfe57116e8cf61a9071f71f25b3 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 1 May 2020 19:01:54 +0200 Subject: [PATCH 52/73] time of the day in Arabic (#430) --- share/we-lang/we-lang.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/we-lang/we-lang.go b/share/we-lang/we-lang.go index 9311622..71aa077 100644 --- a/share/we-lang/we-lang.go +++ b/share/we-lang/we-lang.go @@ -421,7 +421,7 @@ var ( daytimeTranslation = map[string][]string{ "af":{"Oggend","Middag", "Vroegaand", "Laatnag"}, - "ar":{"صباح", "ظهر", "مساء", "ليل" }, + "ar":{ "ﺎﻠﻠﻴﻟ", "ﺎﻠﻤﺳﺍﺀ", "ﺎﻠﻈﻫﺭ", "ﺎﻠﺼﺑﺎﺣ" }, "az":{"Səhər", "Gün", "Axşam", "Gecə" }, "be":{"Раніца", "Дзень", "Вечар", "Ноч" }, "bg":{"Сутрин", "Обяд", "Вечер", "Нощ" }, From 4f0eb49c37334ec94cb34b6081c764772f197d69 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 1 May 2020 19:02:27 +0200 Subject: [PATCH 53/73] zh-* date format fixed (#423) --- share/we-lang/we-lang.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/share/we-lang/we-lang.go b/share/we-lang/we-lang.go index 71aa077..467afa8 100644 --- a/share/we-lang/we-lang.go +++ b/share/we-lang/we-lang.go @@ -912,6 +912,9 @@ func printDay(w weather) (ret []string) { if config.Lang == "ko" { dateName = lctime.Strftime("%b %d일 %a", d) } + if config.Lang == "zh" || config.Lang == "zh-tw" || config.Lang == "zh-cn" { + dateName = lctime.Strftime("%b%d日%A", d) + } } // appendSide := 0 // // for utf8.RuneCountInString(dateName) <= dateWidth { From 221052047a11810d01cdfb6709550ce748a11da9 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 1 May 2020 19:08:12 +0200 Subject: [PATCH 54/73] added Basque translation to we-lang (#438) --- share/we-lang/we-lang.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/share/we-lang/we-lang.go b/share/we-lang/we-lang.go index 467afa8..aeab8fc 100644 --- a/share/we-lang/we-lang.go +++ b/share/we-lang/we-lang.go @@ -303,6 +303,7 @@ var ( "da": "da_DK", "de": "de_DE", "el": "el_GR", + "eu": "eu_ES", "eo": "eo", "es": "es_ES", "et": "et_EE", @@ -370,6 +371,7 @@ var ( "eo": "Veterprognozo por:", "es": "El tiempo en:", "et": "Ilmaprognoos:", + "eu": "Eguraldia:", "fa": "اوه و بآ تیعضو شرازگ", "fi": "Säätiedotus:", "fr": "Prévisions météo pour:", @@ -436,6 +438,7 @@ var ( "eo":{"Mateno", "Tago", "Vespero", "Nokto" }, "es":{"Mañana", "Día", "Tarde", "Noche" }, "et":{"Hommik", "Päev", "Õhtu", "Öösel" }, + "eu":{"Goiz", "Eguerdi", "Arratsalde", "Gau" }, "fa":{ "حبص", "رهظ", "رصع", "بش" }, "fi":{"Aamu", "Keskipäivä", "Ilta", "Yö" }, "fr":{"Matin", "Après-midi", "Soir", "Nuit" }, From 57f15b55c691b96a2094856bec044e9d870b5eb5 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 1 May 2020 19:09:11 +0200 Subject: [PATCH 55/73] enabled partial Basque translation (#438) --- lib/translations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/translations.py b/lib/translations.py index 7999bee..4d8dcc7 100644 --- a/lib/translations.py +++ b/lib/translations.py @@ -13,7 +13,7 @@ FULL_TRANSLATION = [ PARTIAL_TRANSLATION = [ "az", "bg", "bs", "cy", "cs", - "eo", "es", "fi", "ga", "hi", "hr", + "eo", "es", "eu", "fi", "ga", "hi", "hr", "hy", "is", "ja", "jv", "ka", "kk", "ko", "ky", "lt", "lv", "mk", "ml", "nl", "fy", "nn", "pt", "pt-br", "sk", "sl", "sr", "sr-lat", From 03835ff0d33a2901331d0ab0ffc686d061d7b85e Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Fri, 1 May 2020 19:09:50 +0200 Subject: [PATCH 56/73] es: s/Dia/Mediodia/ (#289) --- share/we-lang/we-lang.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/share/we-lang/we-lang.go b/share/we-lang/we-lang.go index aeab8fc..00a23c3 100644 --- a/share/we-lang/we-lang.go +++ b/share/we-lang/we-lang.go @@ -436,7 +436,7 @@ var ( "el":{"Πρωί", "Μεσημέρι", "Απόγευμα", "Βράδυ" }, "en":{"Morning","Noon", "Evening", "Night" }, "eo":{"Mateno", "Tago", "Vespero", "Nokto" }, - "es":{"Mañana", "Día", "Tarde", "Noche" }, + "es":{"Mañana", "Mediodía", "Tarde", "Noche" }, "et":{"Hommik", "Päev", "Õhtu", "Öösel" }, "eu":{"Goiz", "Eguerdi", "Arratsalde", "Gau" }, "fa":{ "حبص", "رهظ", "رصع", "بش" }, From 12d3ea2035a9747068e6676f95334e2a27ea25da Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 7 May 2020 09:37:36 +0200 Subject: [PATCH 57/73] view/wttr.py clean up --- lib/view/wttr.py | 342 +++++++++++++++++++---------------------------- 1 file changed, 140 insertions(+), 202 deletions(-) diff --git a/lib/view/wttr.py b/lib/view/wttr.py index 4007776..7d3c682 100644 --- a/lib/view/wttr.py +++ b/lib/view/wttr.py @@ -1,225 +1,163 @@ # vim: set encoding=utf-8 +# pylint: disable=wrong-import-position -from __future__ import print_function -import gevent -from gevent.pywsgi import WSGIServer -from gevent.queue import Queue -from gevent.monkey import patch_all -from gevent.subprocess import Popen, PIPE, STDOUT -patch_all() +""" +Main view (wttr.in) implementation. +The module is a wrapper for the modified Wego program. +""" import sys -import os import re -import time -import hashlib + +from gevent.subprocess import Popen, PIPE sys.path.insert(0, "..") -from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS -from globals import WEGO, CACHEDIR, \ - NOT_FOUND_LOCATION, DEFAULT_LOCATION, TEST_FILE, ANSI2HTML, \ - log, error, remove_ansi +from translations import get_message, SUPPORTED_LANGS +from globals import WEGO, NOT_FOUND_LOCATION, DEFAULT_LOCATION, ANSI2HTML, \ + error, remove_ansi -def _is_invalid_location(location): - if '.png' in location: - return True def get_wetter(parsed_query): location = parsed_query['location'] - ip = parsed_query['ip_addr'] html = parsed_query['html_output'] lang = parsed_query['lang'] - location_name = parsed_query['override_location_name'] - full_address = parsed_query['full_address'] - url = parsed_query['request_url'] - local_url = url - local_location = location + location_not_found = False + if location == NOT_FOUND_LOCATION: + location_not_found = True - def get_opengraph(): + if not location_not_found: + stdout, stderr, returncode = _wego_wrapper(location, parsed_query) - if local_url is None: - url = "" - else: - url = local_url - - if local_location is None: - location = "" - else: - location = local_location - - pic_url = url.replace('?', '_') - - return ( - '' - '' - '' - '' - ) % { - 'pic_url': pic_url, - 'url': url, - 'location': location, - } - - # '' - # '' - - def get_filename(location, lang=None, query=None, location_name=None): - location = location.replace('/', '_') - timestamp = time.strftime( "%Y%m%d%H", time.localtime() ) - - imperial_suffix = '' - if query.get('use_imperial', False): - imperial_suffix = '-imperial' - - lang_suffix = '' - if lang is not None: - lang_suffix = '-lang_%s' % lang - - if query != None: - query_line = "_" + "_".join("%s=%s" % (key, value) for (key, value) in query.items()) - else: - query_line = "" - - if location_name is None: - location_name = "" - - filename = "".join([timestamp, imperial_suffix, lang_suffix, query_line, location_name]) - digest = hashlib.sha1(filename.encode('utf-8')).hexdigest() - return "%s/%s/%s" % (CACHEDIR, location, digest) - - def save_weather_data(location, filename, lang=None, query=None, location_name=None, full_address=None): - - if _is_invalid_location( location ): - error("Invalid location: %s" % location) - - NOT_FOUND_MESSAGE_HEADER = "" - while True: - location_not_found = False - if location in [ "test-thunder" ]: - test_name = location[5:] - test_file = TEST_FILE.replace('NAME', test_name) - stdout = open(test_file, 'r').read() - stderr = "" - break - if location == NOT_FOUND_LOCATION: - location_not_found = True - location = DEFAULT_LOCATION - - cmd = [WEGO, '--city=%s' % location] - - if query.get('inverted_colors'): - cmd += ['-inverse'] - - if query.get('use_ms_for_wind'): - cmd += ['-wind_in_ms'] - - if query.get('narrow'): - cmd += ['-narrow'] - - if lang and lang in SUPPORTED_LANGS: - cmd += ['-lang=%s'%lang] - - if query.get('use_imperial', False): - cmd += ['-imperial'] - - if location_name: - cmd += ['-location_name', location_name] - - p = Popen(cmd, stdout=PIPE, stderr=PIPE) - stdout, stderr = p.communicate() - stdout = stdout.decode("utf-8") - stderr = stderr.decode("utf-8") - - if p.returncode != 0: - print("ERROR: location not found: %s" % location) - if u'Unable to find any matching weather location to the query submitted' in stderr: - if location != NOT_FOUND_LOCATION: - NOT_FOUND_MESSAGE_HEADER = u"ERROR: %s: %s\n---\n\n" % (get_message('UNKNOWN_LOCATION', lang), location) - location = NOT_FOUND_LOCATION - continue - error(stdout + stderr) - break - - dirname = os.path.dirname(filename) - if not os.path.exists(dirname): - os.makedirs(dirname) - - if location_not_found: + if location_not_found or returncode != 0: + if ('Unable to find any matching weather' + ' location to the parsed_query submitted') in stderr: + stdout, stderr, returncode = _wego_wrapper(NOT_FOUND_LOCATION, parsed_query) + location_not_found = True stdout += get_message('NOT_FOUND_MESSAGE', lang) - stdout = NOT_FOUND_MESSAGE_HEADER + stdout - if 'days' in query: - if query['days'] == '0': - stdout = "\n".join(stdout.splitlines()[:7]) + "\n" - if query['days'] == '1': - stdout = "\n".join(stdout.splitlines()[:17]) + "\n" - if query['days'] == '2': - stdout = "\n".join(stdout.splitlines()[:27]) + "\n" + first_line, stdout = _wego_postprocessing(location, parsed_query, stdout) - first = stdout.splitlines()[0] - rest = stdout.splitlines()[1:] - if query.get('no-caption', False): - separator = None - if ':' in first: - separator = ':' - if u':' in first: - separator = u':' - - if separator: - first = first.split(separator, 1)[1] - stdout = "\n".join([first.strip()] + rest) + "\n" - - if query.get('no-terminal', False): - stdout = remove_ansi(stdout) - - if query.get('no-city', False): - stdout = "\n".join(stdout.splitlines()[2:]) + "\n" - - if full_address \ - and query.get('format', 'txt') != 'png' \ - and (not query.get('no-city') - and not query.get('no-caption') - and not query.get('days') == '0'): - line = "%s: %s [%s]\n" % ( - get_message('LOCATION', lang), - full_address, - location) - stdout += line - - if query.get('padding', False): - lines = [x.rstrip() for x in stdout.splitlines()] - max_l = max(len(remove_ansi(x)) for x in lines) - last_line = " "*max_l + " .\n" - stdout = " \n" + "\n".join(" %s " %x for x in lines) + "\n" + last_line - - open(filename, 'w').write(stdout) - - cmd = ["bash", ANSI2HTML, "--palette=solarized"] - if not query.get('inverted_colors'): - cmd += ["--bg=dark"] - - p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE ) - stdout, stderr = p.communicate(stdout.encode("utf-8")) - stdout = stdout.decode("utf-8") - stderr = stderr.decode("utf-8") - if p.returncode != 0: - error(stdout + stderr) - - if query.get('inverted_colors'): - stdout = stdout.replace('', '') - - title = "%s" % first - opengraph = get_opengraph() - stdout = re.sub("", "" + title + opengraph, stdout) - open(filename+'.html', 'w').write(stdout) - - filename = get_filename(location, lang=lang, query=parsed_query, location_name=location_name) - if not os.path.exists(filename): - save_weather_data(location, filename, lang=lang, query=parsed_query, location_name=location_name, full_address=full_address) if html: - filename += '.html' + return _htmlize(stdout, first_line, parsed_query) + return stdout - return open(filename).read() +def _wego_wrapper(location, parsed_query): + + lang = parsed_query['lang'] + location_name = parsed_query['override_location_name'] + + cmd = [WEGO, '--city=%s' % location] + + if parsed_query.get('inverted_colors'): + cmd += ['-inverse'] + + if parsed_query.get('use_ms_for_wind'): + cmd += ['-wind_in_ms'] + + if parsed_query.get('narrow'): + cmd += ['-narrow'] + + if lang and lang in SUPPORTED_LANGS: + cmd += ['-lang=%s'%lang] + + if parsed_query.get('use_imperial', False): + cmd += ['-imperial'] + + if location_name: + cmd += ['-location_name', location_name] + + proc = Popen(cmd, stdout=PIPE, stderr=PIPE) + stdout, stderr = proc.communicate() + stdout = stdout.decode("utf-8") + stderr = stderr.decode("utf-8") + + return stdout, stderr, proc.returncode + +def _wego_postprocessing(location, parsed_query, stdout): + full_address = parsed_query['full_address'] + lang = parsed_query['lang'] + + if 'days' in parsed_query: + if parsed_query['days'] == '0': + stdout = "\n".join(stdout.splitlines()[:7]) + "\n" + if parsed_query['days'] == '1': + stdout = "\n".join(stdout.splitlines()[:17]) + "\n" + if parsed_query['days'] == '2': + stdout = "\n".join(stdout.splitlines()[:27]) + "\n" + + + first = stdout.splitlines()[0] + rest = stdout.splitlines()[1:] + if parsed_query.get('no-caption', False): + if ':' in first: + first = first.split(":", 1)[1] + stdout = "\n".join([first.strip()] + rest) + "\n" + + if parsed_query.get('no-terminal', False): + stdout = remove_ansi(stdout) + + if parsed_query.get('no-city', False): + stdout = "\n".join(stdout.splitlines()[2:]) + "\n" + + if full_address \ + and parsed_query.get('format', 'txt') != 'png' \ + and (not parsed_query.get('no-city') + and not parsed_query.get('no-caption') + and not parsed_query.get('days') == '0'): + line = "%s: %s [%s]\n" % ( + get_message('LOCATION', lang), + full_address, + location) + stdout += line + + if parsed_query.get('padding', False): + lines = [x.rstrip() for x in stdout.splitlines()] + max_l = max(len(remove_ansi(x)) for x in lines) + last_line = " "*max_l + " .\n" + stdout = " \n" + "\n".join(" %s " %x for x in lines) + "\n" + last_line + + return first, stdout + +def _htmlize(ansi_output, title, parsed_query): + """Return HTML representation of `ansi_output`. + Use `title` as the title of the page. + Format page according to query parameters from `parsed_query`.""" + + cmd = ["bash", ANSI2HTML, "--palette=solarized"] + if not parsed_query.get('inverted_colors'): + cmd += ["--bg=dark"] + + proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + stdout, stderr = proc.communicate(ansi_output.encode("utf-8")) + stdout = stdout.decode("utf-8") + stderr = stderr.decode("utf-8") + if proc.returncode != 0: + error(stdout + stderr) + + if parsed_query.get('inverted_colors'): + stdout = stdout.replace( + '', '') + + title = "%s" % title + opengraph = _get_opengraph(parsed_query) + stdout = re.sub("", "" + title + opengraph, stdout) + return stdout + +def _get_opengraph(parsed_query): + """Return OpenGraph data for `parsed_query`""" + + url = parsed_query['request_url'] or "" + pic_url = url.replace('?', '_') + + return ( + '' + '' + '' + '' + ) % { + 'pic_url': pic_url, + 'url': url, + } From 18f51475ce1be402815f100a022c6316a31a3f7d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 7 May 2020 09:43:42 +0200 Subject: [PATCH 58/73] bin/proxy.py: cache timing fixed --- bin/proxy.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/bin/proxy.py b/bin/proxy.py index cd12900..46fff69 100644 --- a/bin/proxy.py +++ b/bin/proxy.py @@ -83,10 +83,10 @@ def _cache_file(path, query): """ digest = hashlib.sha1(("%s %s" % (path, query)).encode("utf-8")).hexdigest() - digest_number = ord(digest[0]) - expiry_interval = 60*(30+digest_number) + digest_number = ord(digest[0].upper()) + expiry_interval = 60*(digest_number+10) - timestamp = "%010d" % (int(time.time())//1000/expiry_interval*expiry_interval) + timestamp = "%010d" % (int(time.time())//expiry_interval*expiry_interval) filename = os.path.join(PROXY_CACHEDIR, timestamp, path, query) return filename @@ -241,6 +241,8 @@ def proxy(path): content = response.content else: content = "{}" + else: + print("cache found") content = add_translations(content, lang) From 01a321c202a1a83550b2bc3b8c50c513f3308d55 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 7 May 2020 23:37:21 +0200 Subject: [PATCH 59/73] png rendering in a separate thread --- lib/wttr_srv.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py index 3b2f89a..ca42f90 100644 --- a/lib/wttr_srv.py +++ b/lib/wttr_srv.py @@ -9,6 +9,7 @@ import logging import io import os import time +from gevent.threadpool import ThreadPool from flask import render_template, send_file, make_response import fmt.png @@ -36,6 +37,8 @@ logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format='%(asctime)s % LIMITS = Limits(whitelist=[MY_EXTERNAL_IP], limits=QUERY_LIMITS) +TASKS = ThreadPool(25) + def show_text_file(name, lang): """ show static file `name` for `lang` @@ -212,8 +215,14 @@ def _response(parsed_query, query, fast_mode=False): output = get_wetter(parsed_query) if parsed_query.get('png_filename'): - output = fmt.png.render_ansi( - output, options=parsed_query) + # originally it was just a usual function call, + # but it was a blocking call, so it was moved + # to separate threads: + # + # output = fmt.png.render_ansi( + # output, options=parsed_query) + result = TASKS.spawn(fmt.png.render_ansi, output, options=parsed_query) + output = result.get() else: if query.get('days', '3') != '0' \ and not query.get('no-follow-line') \ From e1bd1bf4b495a56a4c317c579a7d1b47ea16446b Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 7 May 2020 23:38:45 +0200 Subject: [PATCH 60/73] lib/globals.py clean up --- lib/globals.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/globals.py b/lib/globals.py index 6db24cf..fbe34b7 100644 --- a/lib/globals.py +++ b/lib/globals.py @@ -29,7 +29,6 @@ PYPHOON = "/home/igor/pyphoon/bin/pyphoon-lolcat" _DATADIR = "/wttr.in" _LOGDIR = "/wttr.in/log" -CACHEDIR = os.path.join(_DATADIR, "cache/wego/") IP2LCACHE = os.path.join(_DATADIR, "cache/ip2l/") PNG_CACHE = os.path.join(_DATADIR, "cache/png") LRU_CACHE = os.path.join(_DATADIR, "cache/lru") @@ -43,7 +42,6 @@ BLACKLIST = os.path.join(MYDIR, "share/blacklist") HELP_FILE = os.path.join(MYDIR, 'share/help.txt') BASH_FUNCTION_FILE = os.path.join(MYDIR, 'share/bash-function.txt') TRANSLATION_FILE = os.path.join(MYDIR, 'share/translation.txt') -TEST_FILE = os.path.join(MYDIR, 'share/test-NAME.txt') IATA_CODES_FILE = os.path.join(MYDIR, 'share/list-of-iata-codes.txt') From 09e3d57b4063a370e8347ae0a3bb2b0b7babf88d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 7 May 2020 23:41:13 +0200 Subject: [PATCH 61/73] lib/cache.py: fixed expiration time --- lib/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cache.py b/lib/cache.py index a8bec58..2dd1151 100644 --- a/lib/cache.py +++ b/lib/cache.py @@ -77,7 +77,7 @@ def store(signature, value): value_record = { "val": value_to_store, - "expiry": time.time() + _randint(1000, 2000)*1000, + "expiry": time.time() + _randint(1000, 2000), } CACHE[signature] = value_record From 9f3a0bca97508fda3a42d621637c65f3486bb3ab Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Thu, 7 May 2020 23:42:41 +0200 Subject: [PATCH 62/73] patch_all(httplib=True) --- bin/srv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/srv.py b/bin/srv.py index 8d4f447..b664152 100644 --- a/bin/srv.py +++ b/bin/srv.py @@ -3,7 +3,7 @@ from gevent.pywsgi import WSGIServer from gevent.monkey import patch_all -patch_all() +patch_all(httplib=True) # pylint: disable=wrong-import-position,wrong-import-order import sys From 24850db80e9801dffe54d0c2c56c53fd63a83eda Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 10 May 2020 17:04:25 +0200 Subject: [PATCH 63/73] do not use u"" in python3 code --- lib/constants.py | 80 ++++++++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/lib/constants.py b/lib/constants.py index f6ff07d..2a44f9b 100644 --- a/lib/constants.py +++ b/lib/constants.py @@ -52,55 +52,55 @@ WWO_CODE = { } WEATHER_SYMBOL = { - "Unknown": u"✨", - "Cloudy": u"☁️", - "Fog": u"🌫", - "HeavyRain": u"🌧", - "HeavyShowers": u"🌧", - "HeavySnow": u"❄️", - "HeavySnowShowers": u"❄️", - "LightRain": u"🌦", - "LightShowers": u"🌦", - "LightSleet": u"🌧", - "LightSleetShowers": u"🌧", - "LightSnow": u"🌨", - "LightSnowShowers": u"🌨", - "PartlyCloudy": u"⛅️", - "Sunny": u"☀️", - "ThunderyHeavyRain": u"🌩", - "ThunderyShowers": u"⛈", - "ThunderySnowShowers": u"⛈", - "VeryCloudy": u"☁️", + "Unknown": "✨", + "Cloudy": "☁️", + "Fog": "🌫", + "HeavyRain": "🌧", + "HeavyShowers": "🌧", + "HeavySnow": "❄️", + "HeavySnowShowers": "❄️", + "LightRain": "🌦", + "LightShowers": "🌦", + "LightSleet": "🌧", + "LightSleetShowers": "🌧", + "LightSnow": "🌨", + "LightSnowShowers": "🌨", + "PartlyCloudy": "⛅️", + "Sunny": "☀️", + "ThunderyHeavyRain": "🌩", + "ThunderyShowers": "⛈", + "ThunderySnowShowers": "⛈", + "VeryCloudy": "☁️", } WEATHER_SYMBOL_WIDTH_VTE = { - u"✨": 2, - u"☁️": 1, - u"🌫": 1, - u"🌧": 2, - u"🌧": 2, - u"❄️": 1, - u"❄️": 1, - u"🌦": 1, - u"🌦": 1, - u"🌧": 1, - u"🌧": 1, - u"🌨": 2, - u"🌨": 2, - u"⛅️": 2, - u"☀️": 1, - u"🌩": 2, - u"⛈": 1, - u"⛈": 1, - u"☁️": 1, + "✨": 2, + "☁️": 1, + "🌫": 2, + "🌧": 2, + "🌧": 2, + "❄️": 1, + "❄️": 1, + "🌦": 1, + "🌦": 1, + "🌧": 1, + "🌧": 1, + "🌨": 2, + "🌨": 2, + "⛅️": 2, + "☀️": 1, + "🌩": 2, + "⛈": 1, + "⛈": 1, + "☁️": 1, } WIND_DIRECTION = [ - u"↓", u"↙", u"←", u"↖", u"↑", u"↗", u"→", u"↘", + "↓", "↙", "←", "↖", "↑", "↗", "→", "↘", ] MOON_PHASES = ( - u"🌑", u"🌒", u"🌓", u"🌔", u"🌕", u"🌖", u"🌗", u"🌘" + "🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘" ) WEATHER_SYMBOL_WEGO = { From ddc08d6fbab9117806c68a473ce243963d300aae Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 10 May 2020 17:06:12 +0200 Subject: [PATCH 64/73] %c: added spaces after weather confidition when needed (#440) --- lib/view/line.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/view/line.py b/lib/view/line.py index 2f410be..ae5cfdb 100644 --- a/lib/view/line.py +++ b/lib/view/line.py @@ -19,7 +19,7 @@ import datetime import json from astral import moon -from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION +from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION, WEATHER_SYMBOL_WIDTH_VTE from weather_data import get_weather_data from . import v2 @@ -55,12 +55,13 @@ def render_temperature(data, query): return temperature def render_condition(data, query): - """ - condition (c) + """Emoji encoded weather condition (c) """ weather_condition = WEATHER_SYMBOL[WWO_CODE[data['weatherCode']]] - return weather_condition + spaces = " "*(WEATHER_SYMBOL_WIDTH_VTE.get(weather_condition) - 1) + + return weather_condition + spaces def render_condition_fullname(data, query): """ From d307c2e08407cb226914385688d10c53d51d9b3f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 10 May 2020 17:06:39 +0200 Subject: [PATCH 65/73] gevent: patch_all() instead of patch_all(httplib=True) --- bin/proxy.py | 2 +- bin/srv.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/proxy.py b/bin/proxy.py index 46fff69..49d2157 100644 --- a/bin/proxy.py +++ b/bin/proxy.py @@ -16,7 +16,7 @@ from __future__ import print_function from gevent.pywsgi import WSGIServer from gevent.monkey import patch_all -patch_all(httplib=True) +patch_all() # pylint: disable=wrong-import-position,wrong-import-order import sys diff --git a/bin/srv.py b/bin/srv.py index b664152..8d4f447 100644 --- a/bin/srv.py +++ b/bin/srv.py @@ -3,7 +3,7 @@ from gevent.pywsgi import WSGIServer from gevent.monkey import patch_all -patch_all(httplib=True) +patch_all() # pylint: disable=wrong-import-position,wrong-import-order import sys From 768437466d2554670879c54e38fe34b000d915a3 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 10 May 2020 17:08:58 +0200 Subject: [PATCH 66/73] updated tests --- test/test-data/signatures | 48 +++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/test/test-data/signatures b/test/test-data/signatures index 3e4c52c..3777753 100644 --- a/test/test-data/signatures +++ b/test/test-data/signatures @@ -4,12 +4,12 @@ ae537911bb7b0568f478073e661abee1cb4ff941 d123e570da22dee9798d353c4281cb5a2bdbaea 3db1938bedc0ee0047bf3b043ddaf0aba1912f13 febab92af9526163bc9e502ecd7fa4225345e6f6 /?T 2cc0ba7a57a6342e72fd7142ca18dbb0eae69416 ce7fb7a88cab697f5280ddabf344f0d397888956 /Киев 928142e88da142ea8075cbfe09bfef349e72dbb1 0f86f59a45b4485fea1375ca945503d9abb9a96d /Kiev?2 -4f6f0a16ff415fad1c102c8023c5d8365ef63402 4a73927d12540efae05a0af874e63283d0d2c310 /Kiev?format=1 -c99903b86971ccccfcca4f13e6fca72776b4fbcf 390bbc1a1e11e99c5ef713484ee8f3e7c79d04a4 /Kiev?format=2 -2a0d6cd8d30a84328580611ca6dd6bed1d805a04 bd08c36eba08dcef794a95b1ac098d981de78ec0 /Kiev?format=3 -4e4e15eeb6a8b6ddb5d00591c4b5e9b74a13e6bc f64a3de698cedace25ca6834cf18094c65ac7855 /Kiev?format=4 -a27d3e4ad7f820124ef57c9299715bc61cb71387 e4fe976ce280eaad650391e93786995749a2d9ed /Kiev?format=v2 -83cc0ef08c24ad7ecc81d1a6cbd693bb06214ece 212431752d0b0d21fabf073e98efa3f1bc24e76e /:help +4f6f0a16ff415fad1c102c8023c5d8365ef63402 de3b9821d587753149eded5411ec397e7a2000e2 /Kiev?format=1 +c99903b86971ccccfcca4f13e6fca72776b4fbcf f64086e48d84ac6eb440ca080eff28de1470ec30 /Kiev?format=2 +2a0d6cd8d30a84328580611ca6dd6bed1d805a04 da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 /Kiev?format=3 +4e4e15eeb6a8b6ddb5d00591c4b5e9b74a13e6bc 6a61305ea631fbccb98090c62a96898e8ffa0d75 /Kiev?format=4 +a27d3e4ad7f820124ef57c9299715bc61cb71387 fed12e63dd5fb5e348ee30d94c7231112eca71cd /Kiev?format=v2 +83cc0ef08c24ad7ecc81d1a6cbd693bb06214ece 0ea998c1b53e452a373699ab953ab00e8a2870f0 /:help 310b64f65fc9f66a5142bf6104f4f9b9d5eef0ea b0bd07f0c87aae9464c091ccb955f41ec6973098 /Kiev?T 9bd1b460d4927df24724f45f69bd3132f3de8e04 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 /Kiev?p 3ee1a25d436799804d7ebd8371d8022fa55a71d7 80f18be012d0471dce9fcd2b500f482bcd635347 /Kiev?q @@ -23,12 +23,12 @@ ee6bf0665c2719cda3ec1fbdb80413d821c99b8e 3f9c5091269ece259cce13fc842265019001ed5 ecbcf2cb9004a754c4559ce7e92fead68f71721a 2ea6d52a2108a481cbc0f44a881eb88642d68e80 -A firefox /?T 74206d869128383dba2d840b848b90eb376fd851 7c6ce53ff25d91a5f46baa30077b69e0f09f2571 -A firefox /Киев 91b89025b5acd56ca475924e0eb559a9734f3333 dbd49d93eff2b2cf82f7d266f90de950207a0561 -A firefox /Kiev?2 -e6cb82dab95e05167ffbcb90a10d6cb03cd02ec1 4a73927d12540efae05a0af874e63283d0d2c310 -A firefox /Kiev?format=1 -e65bc57e8d1df26c442a9ecf45afee390ff331a3 390bbc1a1e11e99c5ef713484ee8f3e7c79d04a4 -A firefox /Kiev?format=2 -d743b331d5f4c81bbc8b168ce84a99ab22dc70cf bd08c36eba08dcef794a95b1ac098d981de78ec0 -A firefox /Kiev?format=3 -bf359ee92690c3a3061542dc6e78cb42ca837412 f64a3de698cedace25ca6834cf18094c65ac7855 -A firefox /Kiev?format=4 -cb875772a6610c991b95b3fbfa22fc7192e25843 9ff741ffc637f42dc38bddeb645c10cf3a45ef4f -A firefox /Kiev?format=v2 -d520af45b491689d53024c696955db8b1e4eaa87 a76e0c26bac17e43af879db6cb11a3cd9eda2344 -A firefox /:help +e6cb82dab95e05167ffbcb90a10d6cb03cd02ec1 de3b9821d587753149eded5411ec397e7a2000e2 -A firefox /Kiev?format=1 +e65bc57e8d1df26c442a9ecf45afee390ff331a3 f64086e48d84ac6eb440ca080eff28de1470ec30 -A firefox /Kiev?format=2 +d743b331d5f4c81bbc8b168ce84a99ab22dc70cf da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -A firefox /Kiev?format=3 +bf359ee92690c3a3061542dc6e78cb42ca837412 6a61305ea631fbccb98090c62a96898e8ffa0d75 -A firefox /Kiev?format=4 +cb875772a6610c991b95b3fbfa22fc7192e25843 5367fd6790d55639b1536ec71abf340e5c79ff45 -A firefox /Kiev?format=v2 +d520af45b491689d53024c696955db8b1e4eaa87 e916b140b1297cf5bea16d92a91260e9dc3e2bc9 -A firefox /:help 6b80492b79a4cc510cb4a9654cb6ff085cdc1943 801d4c6d6837c9604944168a3930dfe05b0e9f5d -A firefox /Kiev?T e13d7449ce756e55ba3e84e4b7e601b36d0044b6 c5a87804710ab70b8798f56579d276f4ce1806bf -A firefox /Kiev?p 3f6c192a6da5b79ea59ef94e99b9cbf4b0e7ede2 372aca50f441920ad623d62ee8fcde46d609f6f9 -A firefox /Kiev?q @@ -42,12 +42,12 @@ b879673f66235bbf1913ff9abc58aff2fb8962d1 00a96a5d83608c2dad7921862bb3f244775f6b1 9cbb6aa3e0b46e78229a32688db1cced9a44271d b368cc8f39e7a7ced04e3f4e6506e1eb4551e904 -H Accept-Language:ru /?T 095d8d38c667923131801595b903e007b5f902f3 4ede3397f9def696adc7ecf3ffd46a59b8fb25cb -H Accept-Language:ru /Киев 4e6cdfc38c9d9f2436438b345776c42cb8cab8a5 1b00c96a05f9daea8248a8e063d990797be933ad -H Accept-Language:ru /Kiev?2 -b7d8d0f0bb4c38aabac468c9a354bb4e2b401893 4a73927d12540efae05a0af874e63283d0d2c310 -H Accept-Language:ru /Kiev?format=1 -8f3bbfc9be6418e82edacecc54a9f3e9f26b7fbf 390bbc1a1e11e99c5ef713484ee8f3e7c79d04a4 -H Accept-Language:ru /Kiev?format=2 -f1d4178892fd3dc38e9f966112d317859acc9122 bd08c36eba08dcef794a95b1ac098d981de78ec0 -H Accept-Language:ru /Kiev?format=3 -cf44e154504d9bc2b9b6066bbb0f5d52fc12f13e f64a3de698cedace25ca6834cf18094c65ac7855 -H Accept-Language:ru /Kiev?format=4 -4955c849f67da53203b8c96b15a0bf0a4a471bc6 c6c09755bb4639c277b48c4ef8219d3a0e1d053a -H Accept-Language:ru /Kiev?format=v2 -3f69f4a605ce88643b4e0d62a588c92625d41aea 237e5cf3a0f4737df49d8382fc8a84f41603cbe5 -H Accept-Language:ru /:help +b7d8d0f0bb4c38aabac468c9a354bb4e2b401893 de3b9821d587753149eded5411ec397e7a2000e2 -H Accept-Language:ru /Kiev?format=1 +8f3bbfc9be6418e82edacecc54a9f3e9f26b7fbf f64086e48d84ac6eb440ca080eff28de1470ec30 -H Accept-Language:ru /Kiev?format=2 +f1d4178892fd3dc38e9f966112d317859acc9122 da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -H Accept-Language:ru /Kiev?format=3 +cf44e154504d9bc2b9b6066bbb0f5d52fc12f13e 6a61305ea631fbccb98090c62a96898e8ffa0d75 -H Accept-Language:ru /Kiev?format=4 +4955c849f67da53203b8c96b15a0bf0a4a471bc6 e2d3509ef7f6c3fc84151f55e1c5eb2f28dfd155 -H Accept-Language:ru /Kiev?format=v2 +3f69f4a605ce88643b4e0d62a588c92625d41aea 73b4142cd3af43472897989c61408d5765c2a6ef -H Accept-Language:ru /:help 08553ca4bf71c738c4321fe7d84b4e6ff830956f 016fc03b18a8902f838719bbc171184603c08b60 -H Accept-Language:ru /Kiev?T b70f8b3fc8aee126c04b27b0d3b4c503b4292cbf b60b68a9e77275884812f7e52b06f6012ba5682a -H Accept-Language:ru /Kiev?p 400efdba61125f8cb850d7c33caf4fc2739a960b 5ee4a043a91509ef57aec46a14a0c24f09e8ec47 -H Accept-Language:ru /Kiev?q @@ -61,12 +61,12 @@ a9977eadc628b1ede5d4f91ee103dfb740caa2b1 a186d89e95061a7887c005ffa8bd1e29362de2d eec20c6be5e528967cddf6d0b72c84dbda553d43 9a39dbafa7e1550d374e38059c0f4b8f437e1739 -H X-Forwarded-For:1.1.1.1 /?T e304153f0e1e9b41781bf4eb6fb6c4a5b7513aec ce7fb7a88cab697f5280ddabf344f0d397888956 -H X-Forwarded-For:1.1.1.1 /Киев 98f0b3a28863a861c6ac6d89ee5d49adb7f3f518 0f86f59a45b4485fea1375ca945503d9abb9a96d -H X-Forwarded-For:1.1.1.1 /Kiev?2 -cf04d7fe2cf36eba8d7fb4fa6def1c9015036456 4a73927d12540efae05a0af874e63283d0d2c310 -H X-Forwarded-For:1.1.1.1 /Kiev?format=1 -67fbe9168566709450eb35d36c60c27105335a7e 390bbc1a1e11e99c5ef713484ee8f3e7c79d04a4 -H X-Forwarded-For:1.1.1.1 /Kiev?format=2 -b2604348bf39774c85b7c18ae7b51f63a2c9f31a bd08c36eba08dcef794a95b1ac098d981de78ec0 -H X-Forwarded-For:1.1.1.1 /Kiev?format=3 -cf012400156c842e569b6a9f05b094e6b75348cd f64a3de698cedace25ca6834cf18094c65ac7855 -H X-Forwarded-For:1.1.1.1 /Kiev?format=4 -1f4981348cab19df9846cd3b3923ee7a972ff9fa e4fe976ce280eaad650391e93786995749a2d9ed -H X-Forwarded-For:1.1.1.1 /Kiev?format=v2 -767a7407c14049fd77a6a2fedd1d8b35f6e47e0d 212431752d0b0d21fabf073e98efa3f1bc24e76e -H X-Forwarded-For:1.1.1.1 /:help +cf04d7fe2cf36eba8d7fb4fa6def1c9015036456 de3b9821d587753149eded5411ec397e7a2000e2 -H X-Forwarded-For:1.1.1.1 /Kiev?format=1 +67fbe9168566709450eb35d36c60c27105335a7e f64086e48d84ac6eb440ca080eff28de1470ec30 -H X-Forwarded-For:1.1.1.1 /Kiev?format=2 +b2604348bf39774c85b7c18ae7b51f63a2c9f31a da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -H X-Forwarded-For:1.1.1.1 /Kiev?format=3 +cf012400156c842e569b6a9f05b094e6b75348cd 6a61305ea631fbccb98090c62a96898e8ffa0d75 -H X-Forwarded-For:1.1.1.1 /Kiev?format=4 +1f4981348cab19df9846cd3b3923ee7a972ff9fa fed12e63dd5fb5e348ee30d94c7231112eca71cd -H X-Forwarded-For:1.1.1.1 /Kiev?format=v2 +767a7407c14049fd77a6a2fedd1d8b35f6e47e0d 0ea998c1b53e452a373699ab953ab00e8a2870f0 -H X-Forwarded-For:1.1.1.1 /:help 10631d55b42e7bc5ec15ffc5cddae712785eb354 b0bd07f0c87aae9464c091ccb955f41ec6973098 -H X-Forwarded-For:1.1.1.1 /Kiev?T 031478f562663eb9f577b04032993e2f098146f6 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 -H X-Forwarded-For:1.1.1.1 /Kiev?p e106cd21a6b67196159c2baa023142e3a8859612 80f18be012d0471dce9fcd2b500f482bcd635347 -H X-Forwarded-For:1.1.1.1 /Kiev?q From 28c55d955903a4ee244f63211b0ddfb7638c7155 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 10 May 2020 17:20:52 +0200 Subject: [PATCH 67/73] moved json below data-rich section --- README.md | 101 +++++++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index cb89f1d..13f4c1f 100644 --- a/README.md +++ b/README.md @@ -80,12 +80,12 @@ You can override this behavior by adding `?u` or `?m` to a URL like this: $ curl wttr.in/Amsterdam?u $ curl wttr.in/Amsterdam?m -## Supported output formats +## Supported output formats and views -wttr.in currently supports four output formats: +wttr.in currently supports five output formats: * ANSI for the terminal; -* ANSI for the terminal, one-line mode; +* Plain-text for the terminal and scripts; * HTML for the browser; * PNG for the graphical viewers; * JSON for scripts and APIs. @@ -122,53 +122,6 @@ You can embed a special wttr.in widget, that displays the weather condition for ![Embedded wttr.in example at feuerwehr-eisolzried.de](https://user-images.githubusercontent.com/3875145/65265457-50eac180-db11-11e9-8f9b-2e1711dfc436.png) -## JSON output - -The JSON format is a feature providing access to wttr.in data through an easy-to-parse format, without requiring the user to create a complex script to reinterpret wttr.in's graphical output. - -To fetch information in JSON format, use the following syntax: - - $ curl wttr.in/Detroit?format=j1 - -This will fetch information on the Detroit region in JSON format. The j1 format code is used to allow for the use of other layouts for the JSON output. - -The result will look something like the following: - - { - "current_condition": [ - { - "FeelsLikeC": "25", - "FeelsLikeF": "76", - "cloudcover": "100", - "humidity": "76", - "observation_time": "04:08 PM", - "precipMM": "0.2", - "pressure": "1019", - "temp_C": "22", - "temp_F": "72", - "uvIndex": 5, - "visibility": "16", - "weatherCode": "122", - "weatherDesc": [ - { - "value": "Overcast" - } - ], - "weatherIconUrl": [ - { - "value": "" - } - ], - "winddir16Point": "NNE", - "winddirDegree": "20", - "windspeedKmph": "7", - "windspeedMiles": "4" - } - ], - ... - -Most of these values are self-explanatory, aside from `weatherCode`. The `weatherCode` is an enumeration which you can find at either [the WorldWeatherOnline website](https://www.worldweatheronline.com/developer/api/docs/weather-icons.aspx) or [in the wttr.in source code](https://github.com/chubin/wttr.in/blob/master/lib/constants.py). - ## One-line output For one-line output format, specify additional URL parameter `format`: @@ -316,6 +269,54 @@ The result, should look like: ![URXVT Emoji line](https://user-images.githubusercontent.com/24360204/63842949-1d36d480-c975-11e9-81dd-998d1329bd8a.png) +## JSON output + +The JSON format is a feature providing access to wttr.in data through an easy-to-parse format, without requiring the user to create a complex script to reinterpret wttr.in's graphical output. + +To fetch information in JSON format, use the following syntax: + + $ curl wttr.in/Detroit?format=j1 + +This will fetch information on the Detroit region in JSON format. The j1 format code is used to allow for the use of other layouts for the JSON output. + +The result will look something like the following: + + { + "current_condition": [ + { + "FeelsLikeC": "25", + "FeelsLikeF": "76", + "cloudcover": "100", + "humidity": "76", + "observation_time": "04:08 PM", + "precipMM": "0.2", + "pressure": "1019", + "temp_C": "22", + "temp_F": "72", + "uvIndex": 5, + "visibility": "16", + "weatherCode": "122", + "weatherDesc": [ + { + "value": "Overcast" + } + ], + "weatherIconUrl": [ + { + "value": "" + } + ], + "winddir16Point": "NNE", + "winddirDegree": "20", + "windspeedKmph": "7", + "windspeedMiles": "4" + } + ], + ... + +Most of these values are self-explanatory, aside from `weatherCode`. The `weatherCode` is an enumeration which you can find at either [the WorldWeatherOnline website](https://www.worldweatheronline.com/developer/api/docs/weather-icons.aspx) or [in the wttr.in source code](https://github.com/chubin/wttr.in/blob/master/lib/constants.py). + + ## Moon phases wttr.in can also be used to check the phase of the Moon. This example shows how to see the current Moon phase From 270d77dd5f3946ba8e3cefb38234197b8d9b52bd Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 10 May 2020 23:09:53 +0200 Subject: [PATCH 68/73] line mode: astro funcs fixed --- lib/view/line.py | 105 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 84 insertions(+), 21 deletions(-) diff --git a/lib/view/line.py b/lib/view/line.py index ae5cfdb..e701bdf 100644 --- a/lib/view/line.py +++ b/lib/view/line.py @@ -17,8 +17,14 @@ import sys import re import datetime import json +import requests +from astral import LocationInfo from astral import moon +from astral.sun import sun + +import pytz + from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION, WEATHER_SYMBOL_WIDTH_VTE from weather_data import get_weather_data from . import v2 @@ -177,24 +183,41 @@ def render_moonday(_, query): moon_phase = moon.phase(date=datetime.datetime.today()) return str(int(moon_phase)) -def render_sunset(data, query): - """ - sunset (s) +################################## +# this part should be rewritten +# this is just a temporary solution - NOT YET IMPLEMENTED - """ - - return "%s" - - location = data['location'] - city_name = location - astral = Astral() - location = Location(('Nuremberg', 'Germany', - 49.453872, 11.077298, 'Europe/Berlin', 0)) - sun = location.sun(date=datetime.datetime.today(), local=True) +def get_geodata(location): + text = requests.get("http://localhost:8004/%s" % location).text + return json.loads(text) - return str(sun['sunset']) +def render_dawn(data, query, local_time_of): + """dawn (D) + Local time of dawn""" + return local_time_of("dawn") + +def render_dusk(data, query, local_time_of): + """dusk (d) + Local time of dusk""" + return local_time_of("dusk") + +def render_sunrise(data, query, local_time_of): + """sunrise (S) + Local time of sunrise""" + return local_time_of("sunrise") + +def render_sunset(data, query, local_time_of): + """sunset (s) + Local time of sunset""" + return local_time_of("sunset") + +def render_zenith(data, query, local_time_of): + """zenith (z) + Local time of zenith""" + return local_time_of("noon") + +################################## FORMAT_SYMBOL = { 'c': render_condition, @@ -205,17 +228,48 @@ FORMAT_SYMBOL = { 'l': render_location, 'm': render_moonphase, 'M': render_moonday, - 's': render_sunset, 'p': render_precipitation, 'o': render_precipitation_chance, 'P': render_pressure, } +FORMAT_SYMBOL_ASTRO = { + 'D': render_dawn, + 'd': render_dusk, + 'S': render_sunrise, + 's': render_sunset, + 'z': render_zenith, +} + def render_line(line, data, query): """ Render format `line` using `data` """ + def get_local_time_of(): + + location = data["location"] + geo_data = get_geodata(location) + + city = LocationInfo() + city.latitude = geo_data["latitude"] + city.longitude = geo_data["longitude"] + city.timezone = geo_data["timezone"] + + timezone = city.timezone + + local_tz = pytz.timezone(timezone) + + datetime_day_start = datetime.datetime.now()\ + .replace(hour=0, minute=0, second=0, microsecond=0) + current_sun = sun(city.observer, date=datetime_day_start) + + local_time_of = lambda x: current_sun[x]\ + .replace(tzinfo=pytz.utc)\ + .astimezone(local_tz)\ + .strftime("%H:%M:%S") + return local_time_of + def render_symbol(match): """ Render one format symbol from re `match` @@ -225,13 +279,22 @@ def render_line(line, data, query): symbol_string = match.group(0) symbol = symbol_string[-1] - if symbol not in FORMAT_SYMBOL: - return '' + if symbol in FORMAT_SYMBOL: + render_function = FORMAT_SYMBOL[symbol] + return render_function(data, query) + if symbol in FORMAT_SYMBOL_ASTRO and local_time_of is not None: + render_function = FORMAT_SYMBOL_ASTRO[symbol] + return render_function(data, query, local_time_of) - render_function = FORMAT_SYMBOL[symbol] - return render_function(data, query) + return '' - return re.sub(r'%[^%]*[a-zA-Z]', render_symbol, line) + template_regexp = r'%[^%]*[a-zA-Z]' + for template_code in re.findall(template_regexp, line): + if template_code.lstrip("%") in FORMAT_SYMBOL_ASTRO: + local_time_of = get_local_time_of() + break + + return re.sub(template_regexp, render_symbol, line) def render_json(data): output = json.dumps(data, indent=4, sort_keys=True) From f9b8c7ccc99504310b42e3130b82b28f1ef1087b Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 10 May 2020 23:14:50 +0200 Subject: [PATCH 69/73] listed astro codes in README --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 13f4c1f..697d89a 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,14 @@ To specify your own custom output format, use the special `%`-notation: p precipitation (mm), o Probability of Precipitation, P pressure (hPa), + + D Dawn*, + S Sunrise*, + z Zenith*, + s Sunset*, + d Dusk*. + +(times are shown in the local timezone) ``` So, these two calls are the same: From 7d6dfcc59531fae7ab40baec1b222adb61b05b7d Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Sun, 10 May 2020 23:17:12 +0200 Subject: [PATCH 70/73] added tests for astrodata --- test/query.sh | 3 +++ test/test-data/signatures | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/test/query.sh b/test/query.sh index 6b7445a..d49d5f4 100644 --- a/test/query.sh +++ b/test/query.sh @@ -10,6 +10,9 @@ queries=( "/Kiev?format=3" "/Kiev?format=4" "/Kiev?format=v2" + "/Kiev?format=%s" + "/Kiev?format=%S" + "/Kiev?format=%D+%S+%z+%s+%d" "/:help" "/Kiev?T" "/Kiev?p" diff --git a/test/test-data/signatures b/test/test-data/signatures index 3777753..d14dff1 100644 --- a/test/test-data/signatures +++ b/test/test-data/signatures @@ -9,6 +9,9 @@ c99903b86971ccccfcca4f13e6fca72776b4fbcf f64086e48d84ac6eb440ca080eff28de1470ec3 2a0d6cd8d30a84328580611ca6dd6bed1d805a04 da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 /Kiev?format=3 4e4e15eeb6a8b6ddb5d00591c4b5e9b74a13e6bc 6a61305ea631fbccb98090c62a96898e8ffa0d75 /Kiev?format=4 a27d3e4ad7f820124ef57c9299715bc61cb71387 fed12e63dd5fb5e348ee30d94c7231112eca71cd /Kiev?format=v2 +cbe9fc56091b519e6aebcedd9a7541241f4c4cda 2d97c405f1557b822cc86b038aeea40c3eb79d7d /Kiev?format=%s +84b79ec29670254c3570901e4c5db017516e088d 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 /Kiev?format=%S +8a0111eb7a519adad1210661bbb49f960ba7f95f fe58cbd420cf36a910551a1037f5c4fa19b31074 /Kiev?format=%D+%S+%z+%s+%d 83cc0ef08c24ad7ecc81d1a6cbd693bb06214ece 0ea998c1b53e452a373699ab953ab00e8a2870f0 /:help 310b64f65fc9f66a5142bf6104f4f9b9d5eef0ea b0bd07f0c87aae9464c091ccb955f41ec6973098 /Kiev?T 9bd1b460d4927df24724f45f69bd3132f3de8e04 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 /Kiev?p @@ -28,6 +31,9 @@ e65bc57e8d1df26c442a9ecf45afee390ff331a3 f64086e48d84ac6eb440ca080eff28de1470ec3 d743b331d5f4c81bbc8b168ce84a99ab22dc70cf da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -A firefox /Kiev?format=3 bf359ee92690c3a3061542dc6e78cb42ca837412 6a61305ea631fbccb98090c62a96898e8ffa0d75 -A firefox /Kiev?format=4 cb875772a6610c991b95b3fbfa22fc7192e25843 5367fd6790d55639b1536ec71abf340e5c79ff45 -A firefox /Kiev?format=v2 +91c0076d8e6665c06aab7c7b2326b29718bfeb80 2d97c405f1557b822cc86b038aeea40c3eb79d7d -A firefox /Kiev?format=%s +ae9d7b1ee27eb8201a0726a3e24fe195cf2ae9e4 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 -A firefox /Kiev?format=%S +e94371697b70bf956b6f9352fad913d716e774e7 fe58cbd420cf36a910551a1037f5c4fa19b31074 -A firefox /Kiev?format=%D+%S+%z+%s+%d d520af45b491689d53024c696955db8b1e4eaa87 e916b140b1297cf5bea16d92a91260e9dc3e2bc9 -A firefox /:help 6b80492b79a4cc510cb4a9654cb6ff085cdc1943 801d4c6d6837c9604944168a3930dfe05b0e9f5d -A firefox /Kiev?T e13d7449ce756e55ba3e84e4b7e601b36d0044b6 c5a87804710ab70b8798f56579d276f4ce1806bf -A firefox /Kiev?p @@ -47,6 +53,9 @@ b7d8d0f0bb4c38aabac468c9a354bb4e2b401893 de3b9821d587753149eded5411ec397e7a2000e f1d4178892fd3dc38e9f966112d317859acc9122 da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -H Accept-Language:ru /Kiev?format=3 cf44e154504d9bc2b9b6066bbb0f5d52fc12f13e 6a61305ea631fbccb98090c62a96898e8ffa0d75 -H Accept-Language:ru /Kiev?format=4 4955c849f67da53203b8c96b15a0bf0a4a471bc6 e2d3509ef7f6c3fc84151f55e1c5eb2f28dfd155 -H Accept-Language:ru /Kiev?format=v2 +e23e33569bbe34de944dad3a647d2a7a525513b4 2d97c405f1557b822cc86b038aeea40c3eb79d7d -H Accept-Language:ru /Kiev?format=%s +d3c44cb57a1ba487b9fe7ec37368d00eee5b4601 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 -H Accept-Language:ru /Kiev?format=%S +db91cc89883050beedd2afac7c74276a4d2dcf42 fe58cbd420cf36a910551a1037f5c4fa19b31074 -H Accept-Language:ru /Kiev?format=%D+%S+%z+%s+%d 3f69f4a605ce88643b4e0d62a588c92625d41aea 73b4142cd3af43472897989c61408d5765c2a6ef -H Accept-Language:ru /:help 08553ca4bf71c738c4321fe7d84b4e6ff830956f 016fc03b18a8902f838719bbc171184603c08b60 -H Accept-Language:ru /Kiev?T b70f8b3fc8aee126c04b27b0d3b4c503b4292cbf b60b68a9e77275884812f7e52b06f6012ba5682a -H Accept-Language:ru /Kiev?p @@ -66,6 +75,9 @@ cf04d7fe2cf36eba8d7fb4fa6def1c9015036456 de3b9821d587753149eded5411ec397e7a2000e b2604348bf39774c85b7c18ae7b51f63a2c9f31a da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -H X-Forwarded-For:1.1.1.1 /Kiev?format=3 cf012400156c842e569b6a9f05b094e6b75348cd 6a61305ea631fbccb98090c62a96898e8ffa0d75 -H X-Forwarded-For:1.1.1.1 /Kiev?format=4 1f4981348cab19df9846cd3b3923ee7a972ff9fa fed12e63dd5fb5e348ee30d94c7231112eca71cd -H X-Forwarded-For:1.1.1.1 /Kiev?format=v2 +e7d772042819bb62e6e259656a75bd7b0621d1da 2d97c405f1557b822cc86b038aeea40c3eb79d7d -H X-Forwarded-For:1.1.1.1 /Kiev?format=%s +7db5a1daac653383c18aec25ad8583f2e5296845 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 -H X-Forwarded-For:1.1.1.1 /Kiev?format=%S +a9331379fa4b5d61b5a87a8e4cd4412cdae970a1 fe58cbd420cf36a910551a1037f5c4fa19b31074 -H X-Forwarded-For:1.1.1.1 /Kiev?format=%D+%S+%z+%s+%d 767a7407c14049fd77a6a2fedd1d8b35f6e47e0d 0ea998c1b53e452a373699ab953ab00e8a2870f0 -H X-Forwarded-For:1.1.1.1 /:help 10631d55b42e7bc5ec15ffc5cddae712785eb354 b0bd07f0c87aae9464c091ccb955f41ec6973098 -H X-Forwarded-For:1.1.1.1 /Kiev?T 031478f562663eb9f577b04032993e2f098146f6 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 -H X-Forwarded-For:1.1.1.1 /Kiev?p From a0798d3482c86a01ca9a62df4a02e6c68c6f5c2f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Mon, 11 May 2020 23:40:08 +0200 Subject: [PATCH 71/73] added Iker Sagasti to the list of translators (#438) --- share/translation.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/share/translation.txt b/share/translation.txt index 0f6d163..9d11a02 100644 --- a/share/translation.txt +++ b/share/translation.txt @@ -10,6 +10,7 @@ Translated/improved/corrected by: Grigor Khachatryan @grigortw * Azerbaijani: Dmytro Nikitiuk, Elsevar Abbasov, Eldar Velibekov (@welibekov on github) + * Basque: Iker Sagasti (@isagasti on github) * Belarusian: Igor Chubin, Anton Zhavoronkov @edogby (on github) * Bosnian: Ismar Kunc @ismarkunc * Bulgarian: Vladimir Vitkov @zeridon (on github) From d7afd94c312af7d2f8db0417d2018d859ddabc6f Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Wed, 13 May 2020 10:22:38 +0200 Subject: [PATCH 72/73] v2: fixed bug with location behind polar circle --- lib/view/v2.py | 86 ++++++++++++++++++++++++++++++++++-------------- lib/view/wttr.py | 9 +++-- 2 files changed, 67 insertions(+), 28 deletions(-) diff --git a/lib/view/v2.py b/lib/view/v2.py index 6a9e122..b671c7d 100644 --- a/lib/view/v2.py +++ b/lib/view/v2.py @@ -34,8 +34,7 @@ import pyjq import pytz import numpy as np from astral import LocationInfo -from astral import moon -from astral.sun import sun +from astral import moon, sun from scipy.interpolate import interp1d from babel.dates import format_datetime @@ -255,35 +254,54 @@ def draw_astronomical(city_name, geo_data): current_date = ( datetime_day_start + datetime.timedelta(hours=1*time_interval)).replace(tzinfo=pytz.timezone(geo_data["timezone"])) - current_sun = sun(city.observer, date=current_date) - dawn = current_sun['dawn'] # .replace(tzinfo=None) - dusk = current_sun['dusk'] # .replace(tzinfo=None) - sunrise = current_sun['sunrise'] # .replace(tzinfo=None) - sunset = current_sun['sunset'] # .replace(tzinfo=None) + try: + dawn = sun.dawn(city.observer, date=current_date) + except ValueError: + dawn = current_date + try: + dusk = sun.dusk(city.observer, date=current_date) + except ValueError: + dusk = current_date + datetime.timedelta(hours=24) + + try: + sunrise = sun.sunrise(city.observer, date=current_date) + except ValueError: + sunrise = current_date + + try: + sunset = sun.sunset(city.observer, date=current_date) + except ValueError: + sunset = current_date + datetime.timedelta(hours=24) + + char = "." if current_date < dawn: char = " " elif current_date > dusk: char = " " - elif dawn < current_date and current_date < sunrise: + elif dawn <= current_date and current_date <= sunrise: char = u"─" - elif sunset < current_date and current_date < dusk: + elif sunset <= current_date and current_date <= dusk: char = u"─" - elif sunrise < current_date and current_date < sunset: + elif sunrise <= current_date and current_date <= sunset: char = u"━" answer += char # moon - if time_interval % 3 == 0: + if time_interval in [0,23,47,69]: # time_interval % 3 == 0: moon_phase = 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+0.5)) % len(constants.MOON_PHASES)] - if time_interval in [0, 24, 48, 69]: - moon_line += moon_phase_emoji + " " - else: + moon_phase_emoji = constants.MOON_PHASES[ + int(math.floor(moon_phase*1.0/28.0*8+0.5)) % len(constants.MOON_PHASES)] + # if time_interval in [0, 24, 48, 69]: + moon_line += moon_phase_emoji # + " " + elif time_interval % 3 == 0: + if time_interval not in [24,28]: #se: moon_line += " " + else: + moon_line += " " answer = moon_line + "\n" + answer + "\n" @@ -465,7 +483,6 @@ def textual_information(data_parsed, geo_data, config, html_output=False): datetime_day_start = datetime.datetime.now()\ .replace(hour=0, minute=0, second=0, microsecond=0) - current_sun = sun(city.observer, date=datetime_day_start) format_line = "%c %C, %t, %h, %w, %P" current_condition = data_parsed['data']['current_condition'][0] @@ -477,18 +494,37 @@ def textual_information(data_parsed, geo_data, config, html_output=False): local_tz = pytz.timezone(timezone) - local_time_of = lambda x: current_sun[x]\ - .replace(tzinfo=pytz.utc)\ - .astimezone(local_tz)\ - .strftime("%H:%M:%S") + def _get_local_time_of(what): + _sun = { + "dawn": sun.dawn, + "sunrise": sun.sunrise, + "noon": sun.noon, + "sunset": sun.sunset, + "dusk": sun.dusk, + }[what] + + current_time_of_what = _sun(city.observer, date=datetime_day_start) + return current_time_of_what\ + .replace(tzinfo=pytz.utc)\ + .astimezone(local_tz)\ + .strftime("%H:%M:%S") + + local_time_of = {} + for what in ["dawn", "sunrise", "noon", "sunset", "dusk"]: + try: + local_time_of[what] = _get_local_time_of(what) + except ValueError: + local_time_of[what] = "-"*8 tmp_output = [] + tmp_output.append(' Now: %%{{NOW(%s)}}' % timezone) - tmp_output.append('Dawn: %s' % local_time_of("dawn")) - tmp_output.append('Sunrise: %s' % local_time_of("sunrise")) - tmp_output.append(' Zenith: %s ' % local_time_of("noon")) - tmp_output.append('Sunset: %s' % local_time_of("sunset")) - tmp_output.append('Dusk: %s' % local_time_of("dusk")) + tmp_output.append('Dawn: %s' % local_time_of["dawn"]) + tmp_output.append('Sunrise: %s' % local_time_of["sunrise"]) + tmp_output.append(' Zenith: %s ' % local_time_of["noon"]) + tmp_output.append('Sunset: %s' % local_time_of["sunset"]) + tmp_output.append('Dusk: %s' % local_time_of["dusk"]) + tmp_output = [ re.sub("^([A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"), x) for x in tmp_output] diff --git a/lib/view/wttr.py b/lib/view/wttr.py index 7d3c682..a3874f3 100644 --- a/lib/view/wttr.py +++ b/lib/view/wttr.py @@ -27,12 +27,15 @@ def get_wetter(parsed_query): if location == NOT_FOUND_LOCATION: location_not_found = True + stderr = "" + returncode = 0 if not location_not_found: stdout, stderr, returncode = _wego_wrapper(location, parsed_query) - if location_not_found or returncode != 0: - if ('Unable to find any matching weather' - ' location to the parsed_query submitted') in stderr: + if location_not_found or \ + (returncode != 0 \ + and ('Unable to find any matching weather' + ' location to the parsed_query submitted') in stderr): stdout, stderr, returncode = _wego_wrapper(NOT_FOUND_LOCATION, parsed_query) location_not_found = True stdout += get_message('NOT_FOUND_MESSAGE', lang) From ebb961797fcf3ee806fbcc4b876c8259c371e7b0 Mon Sep 17 00:00:00 2001 From: Igor Chubin Date: Wed, 13 May 2020 10:29:01 +0200 Subject: [PATCH 73/73] added basque to proxy langs (#438) --- lib/translations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/translations.py b/lib/translations.py index 031c989..6f100f8 100644 --- a/lib/translations.py +++ b/lib/translations.py @@ -23,7 +23,7 @@ PARTIAL_TRANSLATION = [ PROXY_LANGS = [ "af", "ar", "az", "be", "bs", "ca", - "cy", "de", "el", "eo", "et", "fa", "fr", + "cy", "de", "el", "eo", "et", "eu", "fa", "fr", "fy", "he", "hr", "hu", "hy", "ia", "id", "is", "it", "ja", "kk", "lv", "mk", "nb", "nn", "ro",