File: bin/srv.py

#!/usr/bin/env python
# vim: set encoding=utf-8

from gevent.pywsgi import WSGIServer
from gevent.monkey import patch_all
patch_all()

# pylint: disable=wrong-import-position,wrong-import-order
import sys
import os
import jinja2

from flask import Flask, request, send_from_directory, send_file
APP = Flask(__name__)

MYDIR = os.path.abspath(
    os.path.dirname(os.path.dirname('__file__')))
sys.path.append("%s/lib/" % MYDIR)

import wttr_srv
from globals import TEMPLATES, STATIC, LISTEN_HOST, LISTEN_PORT
# pylint: enable=wrong-import-position,wrong-import-order

from view.v3 import v3_file

MY_LOADER = jinja2.ChoiceLoader([
    APP.jinja_loader,
    jinja2.FileSystemLoader(TEMPLATES),
])

APP.jinja_loader = MY_LOADER

@APP.route('/v3/<string:location>')
def send_v3(location):
    filepath = v3_file(location)
    if filepath.startswith("ERROR"):
        return filepath.rstrip("\n") + "\n"
    return send_file(filepath)

@APP.route('/files/<path:path>')
def send_static(path):
    "Send any static file located in /files/"
    return send_from_directory(STATIC, path)

@APP.route('/favicon.ico')
def send_favicon():
    "Send static file favicon.ico"
    return send_from_directory(STATIC, 'favicon.ico')

@APP.route('/malformed-response.html')
def send_malformed():
    "Send static file malformed-response.html"
    return send_from_directory(STATIC, 'malformed-response.html')

@APP.route("/")
@APP.route("/<string:location>")
def wttr(location=None):
    "Main function wrapper"
    return wttr_srv.wttr(location, request)

SERVER = WSGIServer(
    (LISTEN_HOST, int(os.environ.get('WTTRIN_SRV_PORT', LISTEN_PORT))),
    APP)
SERVER.serve_forever()



File: bin/geo-proxy.py


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()

import sys
import os
import json

from flask import Flask, request, render_template, send_from_directory, send_file, make_response, jsonify, Response
app = Flask(__name__)

MYDIR = os.path.abspath(os.path.dirname('__file__'))
sys.path.append(os.path.join(MYDIR, 'lib'))

CACHEDIR = os.path.join(MYDIR, 'cache')

from geopy.geocoders import Nominatim #, Mapzen
#geomapzen = Mapzen("mapzen-RBNbmcZ") # Nominatim()
geoosm = Nominatim(timeout=7, user_agent="wttrin-geo/0.0.2")

import airports

# from tzwhere import tzwhere
import timezonefinder
tf = timezonefinder.TimezoneFinder()

def load_cache(location_string):
    try:
        location_string = location_string.replace('/', '_')
        cachefile = os.path.join(CACHEDIR, location_string)

        return json.loads(open(cachefile, 'r').read())
    except:
        return None

def shorten_full_address(address):
    parts = address.split(',')
    if len(parts) > 6:
        parts = parts[:2] + [x for x in parts[-4:] if len(x) < 20]
        return ','.join(parts)
    return address
    

def save_cache(location_string, answer):
    location_string = location_string.replace('/', '_')
    cachefile = os.path.join(CACHEDIR, location_string)
    open(cachefile, 'w').write(json.dumps(answer))

def query_osm(location_string):
    try:
        location = geoosm.geocode(location_string)
        return {
            'address':  location.address,
            'latitude': location.latitude,
            'longitude':location.longitude,
        }

    except Exception as e:
        print(e)
        return None

def add_timezone_information(geo_data):
    # tzwhere_ = tzwhere.tzwhere()
    # timezone_str = tzwhere_.tzNameAt(geo_data["latitude"], geo_data["longitude"])
    timezone_str = tf.certain_timezone_at(lat=geo_data["latitude"], lng=geo_data["longitude"])

    answer = geo_data.copy()
    answer["timezone"] = timezone_str

    return answer

@app.route("/<string:location>")
def find_location(location):

    airport_gps_location = airports.get_airport_gps_location(location.upper().lstrip('~'))
    is_airport = False
    if airport_gps_location is not None:
        location = airport_gps_location
        is_airport = True

    location = location.replace('+', ' ')
    answer = load_cache(location)
    loaded_answer = None

    if answer is not None:
        loaded_answer = answer.copy()
        print("cache found: %s" % location)

    if answer is None:
        answer = query_osm(location)
    
    if is_airport:
        answer['address'] = shorten_full_address(answer['address'])

    if "timezone" not in answer:
       answer = add_timezone_information(answer)

    if answer is not None and loaded_answer != answer:
        save_cache(location, answer)

    if answer is None:
        return ""
    else:
        r = Response(json.dumps(answer)) 
        r.headers["Content-Type"] = "text/json; charset=utf-8"
        return r

app.config['JSON_AS_ASCII'] = False
server = WSGIServer(("127.0.0.1", 8004), app)
server.serve_forever()



File: bin/proxy.py

#vim: fileencoding=utf-8

"""

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

from gevent.pywsgi import WSGIServer
from gevent.monkey import patch_all
patch_all()

# pylint: disable=wrong-import-position,wrong-import-order
import sys
import os
import time
import json
import hashlib

import requests
import cyrtranslit

from flask import Flask, request
APP = Flask(__name__)


MYDIR = os.path.abspath(
    os.path.dirname(os.path.dirname('__file__')))
sys.path.append("%s/lib/" % MYDIR)

import proxy_log
import globals
from globals import PROXY_CACHEDIR, PROXY_HOST, PROXY_PORT, USE_METNO, USER_AGENT, MISSING_TRANSLATION_LOG
from metno import create_standard_json_from_metno, metno_request
from translations import PROXY_LANGS
# pylint: enable=wrong-import-position

proxy_logger = proxy_log.LoggerWWO(globals.PROXY_LOG_ACCESS, globals.PROXY_LOG_ERRORS)

def is_testmode():
    """Server is running in the wttr.in test mode"""

    return "WTTRIN_TEST" in os.environ

def load_translations():
    """
    load all translations
    """
    translations = {}

    for f_name in PROXY_LANGS:
        f_name = 'share/translations/%s.txt' % f_name
        translation = {}
        lang = f_name.split('/')[-1].split('.', 1)[0]
        with open(f_name, "r") as f_file:
            for line in f_file:
                if ':' not in line:
                    continue
                if line.count(':') == 3:
                    _, trans, orig, _ = line.strip().split(':', 4)
                else:
                    _, trans, orig = line.strip().split(':', 3)
                trans = trans.strip()
                orig = orig.strip()

                translation[orig.lower()] = trans
        translations[lang] = translation
    return translations
TRANSLATIONS = load_translations()

def _is_metno():
    return USE_METNO

def _find_srv_for_query(path, query):   # pylint: disable=unused-argument
    if _is_metno():
        return 'https://api.met.no'
    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].upper())
    expiry_interval = 60*(digest_number+180)

    timestamp = "%010d" % (int(time.time())//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:
        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):
    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):
    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 + ".headers", 'w').write(json.dumps(headers))
    open(cache_file, 'wb').write(content)

def translate(text, lang):
    """
    Translate `text` into `lang`.
    If `text` is comma-separated, translate each term independently.
    If no translation found, leave it untouched.
    """

    def _log_unknown_translation(lang, text):
        with open(MISSING_TRANSLATION_LOG % lang, "a") as f_missing_translation:
            f_missing_translation.write(text+"\n")

    if "," in text:
        terms = text.split(",")
        translated_terms = [translate(term.strip(), lang) for term in terms]
        return ", ".join(translated_terms)

    if lang not in TRANSLATIONS:
        _log_unknown_translation(lang, "UNKNOWN_LANGUAGE")
        return text

    if text.lower() not in TRANSLATIONS.get(lang, {}):
        _log_unknown_translation(lang, text)
        return text

    translated = TRANSLATIONS.get(lang, {}).get(text.lower(), text)
    return translated

def cyr(to_translate):
    """
    Transliterate `to_translate` from latin into cyrillic
    """
    return cyrtranslit.to_cyrillic(to_translate)

def _patch_greek(original):
    return original.replace(u"Ηλιόλουστη/ο", u"Ηλιόλουστη")

def add_translations(content, lang):
    """
    Add `lang` translation to `content` (JSON)
    returned by the data source
    """

    if content == "{}":
        return {}

    languages_to_translate = TRANSLATIONS.keys()
    try:
        d = json.loads(content)         # pylint: disable=invalid-name
    except (ValueError, TypeError) as exception:
        print("---")
        print(exception)
        print("---")
        return {}

    try:
        weather_condition = d['data']['current_condition'
                ][0]['weatherDesc'][0]['value'].capitalize()
        d['data']['current_condition'][0]['weatherDesc'][0]['value'] = \
            weather_condition
        if lang in languages_to_translate:
            d['data']['current_condition'][0]['lang_%s' % lang] = \
                [{'value': translate(weather_condition, lang)}]
        elif lang == 'sr':
            d['data']['current_condition'][0]['lang_%s' % lang] = \
                [{'value': cyr(
                    d['data']['current_condition'][0]['lang_%s' % lang][0]['value']\
                    )}]
        elif lang == 'el':
            d['data']['current_condition'][0]['lang_%s' % lang] = \
                [{'value': _patch_greek(
                    d['data']['current_condition'][0]['lang_%s' % lang][0]['value']\
                    )}]
        elif lang == 'sr-lat':
            d['data']['current_condition'][0]['lang_%s' % lang] = \
                [{'value':d['data']['current_condition'][0]['lang_sr'][0]['value']\
                            }]

        fixed_weather = []
        for w in d['data']['weather']:  # pylint: disable=invalid-name
            fixed_hourly = []
            for h in w['hourly']:       # pylint: disable=invalid-name
                weather_condition = h['weatherDesc'][0]['value']
                if lang in languages_to_translate:
                    h['lang_%s' % lang] = \
                        [{'value': translate(weather_condition, lang)}]
                elif lang == 'sr':
                    h['lang_%s' % lang] = \
                        [{'value': cyr(h['lang_%s' % lang][0]['value'])}]
                elif lang == 'el':
                    h['lang_%s' % lang] = \
                        [{'value': _patch_greek(h['lang_%s' % lang][0]['value'])}]
                elif lang == 'sr-lat':
                    h['lang_%s' % lang] = \
                        [{'value': h['lang_sr'][0]['value']}]
                fixed_hourly.append(h)
            w['hourly'] = fixed_hourly
            fixed_weather.append(w)
        d['data']['weather'] = fixed_weather

        content = json.dumps(d)
    except (IndexError, ValueError) as exception:
        print(exception)
    return content

def _fetch_content_and_headers(path, query_string, **kwargs):
    content, headers = _load_content_and_headers(path, query_string)

    if content is None:
        srv = _find_srv_for_query(path, query_string)
        url = "%s/%s?%s" % (srv, path, query_string)

        attempts = 10
        response = None
        error = ""
        while attempts:
            try:
                response = requests.get(url, timeout=2, **kwargs)
            except requests.ReadTimeout:
                attempts -= 1
                continue
            try:
                data = json.loads(response.content)
                error = data.get("data", {}).get("error", "")
                if error:
                    try:
                        error = error[0]["msg"]
                    except (ValueError, IndexError):
                        error = "invalid error format: %s" % error
                break
            except ValueError:
                attempts -= 1
                error = "invalid response"

        proxy_logger.log(query_string, error)
        _touch_empty_file(path, query_string)
        if response:
            headers = {}
            headers['Content-Type'] = response.headers['content-type']
            _save_content_and_headers(path, query_string, response.content, headers)
            content = response.content
        else:
            content = "{}"
    else:
        print("cache found")
    return content, headers


def _make_query(path, query_string):

    if _is_metno():
        path, query, days = metno_request(path, query_string)
        if USER_AGENT == '':
            raise ValueError('User agent must be set to adhere to metno ToS: https://api.met.no/doc/TermsOfService')
        content, headers = _fetch_content_and_headers(path, query, headers={
                                   'User-Agent': USER_AGENT
                               })
        content = create_standard_json_from_metno(content, days)
    else:
        # WWO tweaks
        query_string += "&extra=localObsTime"
        query_string += "&includelocation=yes"
        content, headers = _fetch_content_and_headers(path, query_string)

    return content, headers

@APP.route("/<path:path>")
def proxy(path):
    """
    Main proxy function. Handles incoming HTTP queries.
    """

    lang = request.args.get('lang', 'en')
    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')
    content = ""
    headers = ""

    content, headers = _make_query(path, query_string)

    # _log_query(path, query_string, error)

    content = add_translations(content, lang)

    return content, 200, headers

if __name__ == "__main__":
    #app.run(host='0.0.0.0', port=5001, debug=False)
    #app.debug = True

    if len(sys.argv) == 1:
        bind_addr = "0.0.0.0"
        SERVER = WSGIServer((bind_addr, PROXY_PORT), APP)
        SERVER.serve_forever()
    else:
        print('running single request from command line arg')
        APP.testing = True
        with APP.test_client() as c:
            resp = c.get(sys.argv[1])
            print('Status: ' + resp.status)
            # print('Headers: ' + dumps(resp.headers))
            print(resp.data.decode('utf-8'))



File: lib/fields.py

"""
Human readable description of the available data fields
describing current weather, weather forecast, and astronomical data
"""

DESCRIPTION = {
    # current condition fields
    "FeelsLikeC": (
        "Feels Like Temperature in Celsius",
        "temperature_feels_like_celsius"),
    "FeelsLikeF": (
        "Feels Like Temperature in Fahrenheit",
        "temperature_feels_like_fahrenheit"),
    "cloudcover": (
        "Cloud Coverage in Percent",
        "cloudcover_percentage"),
    "humidity": (
        "Humidity in Percent",
        "humidity_percentage"),
    "precipMM": (
        "Precipitation (Rainfall) in mm",
        "precipitation_mm"),
    "pressure": (
        "Air pressure in hPa",
        "pressure_hpa"),
    "temp_C": (
        "Temperature in Celsius",
        "temperature_celsius"),
    "temp_F": (
        "Temperature in Fahrenheit",
        "temperature_fahrenheit"),
    "uvIndex": (
        "Ultraviolet Radiation Index",
        "uv_index"),
    "visibility": (
        "Visible Distance in Kilometres",
        "visibility"),
    "weatherCode": (
        "Code to describe Weather Condition",
        "weather_code"),
    "winddirDegree": (
        "Wind Direction in Degree",
        "winddir_degree"),
    "windspeedKmph": (
        "Wind Speed in Kilometres per Hour",
        "windspeed_kmph"),
    "windspeedMiles": (
        "Wind Speed in Miles per Hour",
        "windspeed_mph"),
    "observation_time": (
        "Minutes since start of the day the observation happened",
        "observation_time"),

    # fields with `description`
    "weatherDesc": (
        "Weather Description",
        "weather_desc"),
    "winddir16Point": (
        "Wind Direction on a 16-wind compass rose",
        "winddir_16_point"),

    # forecast fields
    "maxtempC": (
        "Maximum Temperature in Celsius",
        "temperature_celsius_maximum"),
    "maxtempF": (
        "Maximum Temperature in Fahrenheit",
        "temperature_fahrenheit_maximum"),
    "mintempC": (
        "Minimum Temperature in Celsius",
        "temperature_celsius_minimum"),
    "mintempF": (
        "Minimum Temperature in Fahrenheit",
        "temperature_fahrenheit_minimum"),
    "sunHour":(
        "Hours of sunlight",
        "sun_hour"),
    "totalSnow_cm":(
        "Total snowfall in cm",
        "snowfall_cm"),

    # astronomy fields
    "moon_illumination": (
        "Percentage of the moon illuminated",
        "astronomy_moon_illumination"),

    # astronomy fields with description
    "moon_phase": (
        "Phase of the moon",
        "astronomy_moon_phase"),

    # astronomy fields with time
    "moonrise": (
        "Minutes since start of the day until the moon appears above the horizon",
        "astronomy_moonrise_min"),
    "moonset": (
        "Minutes since start of the day until the moon disappears below the horizon",
        "astronomy_moonset_min"),
    "sunrise": (
        "Minutes since start of the day until the sun appears above the horizon",
        "astronomy_sunrise_min"),
    "sunset": (
        "Minutes since start of the day until the moon disappears below the horizon",
        "astronomy_sunset_min"),
    }



File: lib/limits.py

"""
Connection limitation.

Number of connections from one IP is limited.
We have nothing against scripting and automated queries.
Even the opposite, we encourage them. But there are some
connection limits that even we can't handle.
Currently the limits are quite restrictive, but they will be relaxed
in the future.

Usage:

        limits = Limits()
        not_allowed = limits.check_ip(ip_address)
        if not_allowed:
            return "ERROR: %s" % not_allowed

[Taken from github.com/chubin/cheat.sh]
"""

import time
from globals import log

def _time_caps(minutes, hours, days):
    return {
        'min':   minutes,
        'hour':  hours,
        'day':   days,
        }

class Limits(object):
    """
    Queries limitation (by IP).

    Exports:

        check_ip(ip_address)
    """

    def __init__(self, whitelist=None, limits=None):
        self.intervals = ['min', 'hour', 'day']

        self.divisor = _time_caps(60, 3600, 86400)
        self.last_update = _time_caps(0, 0, 0)

        if limits:
            self.limit = _time_caps(*limits)
        else:
            self.limit = _time_caps(30, 600, 1000)

        if whitelist:
            self.whitelist = whitelist[:]
        else:
            self.whitelist = []

        self.counter = {
            'min':      {},
            'hour':     {},
            'day':      {},
            }

        self._clear_counters_if_needed()

    def _log_visit(self, interval, ip_address):
        if ip_address not in self.counter[interval]:
            self.counter[interval][ip_address] = 0
        self.counter[interval][ip_address] += 1

    def _limit_exceeded(self, interval, ip_address):
        visits = self.counter[interval][ip_address]
        limit = self._get_limit(interval)
        return  visits > limit

    def _get_limit(self, interval):
        return self.limit[interval]

    def _report_excessive_visits(self, interval, ip_address):
        log("%s LIMITED [%s for %s]" % (ip_address, self._get_limit(interval), interval))

    def check_ip(self, ip_address):
        """
        Check if `ip_address` is allowed, and if not raise an RuntimeError exception.
        Return True otherwise
        """
        if ip_address in self.whitelist:
            return None
        self._clear_counters_if_needed()
        for interval in self.intervals:
            self._log_visit(interval, ip_address)
            if self._limit_exceeded(interval, ip_address):
                self._report_excessive_visits(interval, ip_address)
                return ("Not so fast! Number of queries per %s is limited to %s"
                        % (interval, self._get_limit(interval)))
        return None

    def reset(self):
        """
        Reset all counters for all IPs
        """
        for interval in self.intervals:
            self.counter[interval] = {}

    def _clear_counters_if_needed(self):
        current_time = int(time.time())
        for interval in self.intervals:
            if current_time // self.divisor[interval] != self.last_update[interval]:
                self.counter[interval] = {}
                self.last_update[interval] = current_time / self.divisor[interval]



File: lib/translations_v2.py

# vim: fileencoding=utf-8

"""
Translation of v2
"""

# pylint: disable=line-too-long,bad-whitespace
V2_TRANSLATION = {
    "en": ("Weather report for:",           "Weather",  "Timezone",     "Now",      "Dawn",             "Sunrise",      "Zenith",       "Sunset",           "Dusk"),
    "af": ("Weer verslag vir:",             "", "", "", "", "", "", "", ""),
    "am": ("የአየር ሁኔታ ዘገባ ለ ፥",             "የአየር ሁኔታ", "የጊዜ ሰቅ", "አሁን", "ንጋት", "የፀሐይ መውጫ", "", "የፀሐይ መጥለቅ", "ምሽት"),
    "ar": ("تقرير حالة الطقس:",              "حالة الطقس", "المنطقة الزمنية", "الآن ", "الفجر", "شروق الشمس", "الذروة", "غروب الشمس", "الغسق"),
    "az": ("Hava proqnozu:",                "Hava",     "Saat zonası",  "İndi",     "Şəfəq",            "Günəş çıxdı",  "Zenit",        "Gün batımı",       "Toran"),
    "be": ("Прагноз надвор'я для:",         "Надвор'е", "Часавая зона", "Цяпер", "Світанак", "Усход сонца", "Зеніт", "Захад сонца", "Змярканне"),
    "bg": ("Прогноза за времето в:",        "", "", "", "", "", "", "", ""),
    "bn" : ("আবহাওয়া প্রতিবেদন:", "আবহাওয়া", "টাইমজোন", "এখন", "ভোর", "সূর্যোদয়", "সুবিন্দু", "সূর্যাস্ত", "সন্ধ্যা"),
    "bs": ("Vremenske prognoze za:",        "", "", "", "", "", "", "", ""),
    "ca": ("Informe del temps per a:",      "Oratge", "Zona horària", "Ara", "Albada", "Sortida", "Zenit", "Posta", "Crepuscle"),
    "cs": ("Předpověď počasí pro:",         "", "", "", "", "", "", "", ""),
    "cy": ("Adroddiad tywydd ar gyfer:",    "", "", "", "", "", "", "", ""),
    "da": ("Vejret i:",                     "Vejret", "Tidszone", "Nu", "Daggry", "Solopgang", "Zenit", "Solnedgang", "Skumring"),
    "de": ("Wetterbericht für:",            "Wetter",   "Zeitzone",     "Jetzt",    "Morgendämmerung",  "Sonnenaufgang","Zenit",        "Sonnenuntergang",  "Abenddämmerung"),
    "el": ("Πρόγνωση καιρού για:",          "", "", "", "", "", "", "", ""),
    "eo": ("Veterprognozo por:",            "Vetero",   "Horzono",      "Nun",      "Tagiĝo",           "Sunleviĝo",    "Zenito",       "Sunsubiro",        "Krepusko"),
    "es": ("El tiempo en:",                 "Clima", "Zona horaria", "Ahora", "Alborada", "Amanecer", "Cenit", "Atardecer", "Anochecer"),
    "et": ("Ilmaprognoos:",                 "Ilm", "Ajatsoon", "Hetkel", "Koit", "Päikesetõus", "Seniit", "Päikeseloojang", "Eha"),
    "eu": ("Eguraldia:",                    "Ordu-eremua", "Orain", "Egunsentia", "Eguzkia", "Zenit", "Ilunabarra", "Ilunabarra"),
    "fa": ("اوه و بآ تیعضو شرازگ",          "", "", "", "", "", "", "", ""),
    "fi": ("Säätiedotus:",                  "", "", "", "", "", "", "", ""),
    "fr": ("Prévisions météo pour :",       "Météo", "Fuseau Horaire", "Heure", "Aube", "Lever du Soleil", "Zénith", "Coucher du Soleil", "Crépuscule"),
    "fy": ("Waarberjocht foar:",            "", "", "", "", "", "", "", ""),
    "ga": ("Réamhaisnéis na haimsire do:",  "Aimsir", "Crios ama", "Anois", "Breacadh an lae", "Éirí na gréine", "Forar", "Dul faoi na gréine", "Coineascar"),
    "he": (":ריוואה גזמ תיזחת",             "", "", "", "", "", "", "", ""),
    "hi": ("मौसम की जानकारी",               "मौसम", "समय मण्डल", "अभी", "उदय", "सूर्योदय", "चरम बिन्दु", "सूर्यास्त", "संध्याकाल"),
    "hr": ("Vremenska prognoza za:",        "", "", "", "", "", "", "", ""),
    "hu": ("Időjárás előrejelzés:",         "Időjárás", "időzóna", "aktuális", "hajnal", "napkelte", "dél", "naplemente", "szürkület"),
    "hy": ("Եղանակի տեսություն:",           "", "", "", "", "", "", "", ""),
    "ia": ("Reporto tempore pro:",          "Tempore", "Fuso Horari", "Alora", "Alba", "Aurora", "Zenit", "Poner del sol", "Crepusculo"),
    "id": ("Prakiraan cuaca:",              "", "", "", "", "", "", "", ""),
    "it": ("Previsioni meteo:",             "Tempo", "Fuso orario", "Ora", "Alba", "Sorgere del Sole", "Zenit", "Tramonto", "Crepuscolo"),
    "is": ("Veðurskýrsla fyrir:",           "", "", "", "", "", "", "", ""),
    "ja": ("天気予報：",                    "天気",     "タイムゾーン", "今",       "夜明け",           "日の出",       "天頂",         "日の入",           "日暮れ"),
    "jv": ("Weather forecast for:",         "", "", "", "", "", "", "", ""),
    "ka": ("ამინდის პროგნოზი:",             "", "", "", "", "", "", "", ""),
    "kk": ("Ауа райы:",                     "", "", "", "", "", "", "", ""),
    "ko": ("일기 예보：",                   "날씨", "시간대", "현재", "새벽", "일출", "정오", "일몰", "황혼"),
    "ky": ("Аба ырайы:",                    "", "", "", "", "", "", "", ""),
    "lt": ("Orų prognozė:",                 "Orai",     "Laiko zona",   "Dabar",    "Aušra",            "Saulėtekis",   "Zenitas",      "Saulėlydis",       "Sutemos"),
    "lv": ("Laika ziņas:",                  "", "", "", "", "", "", "", ""),
    "mk": ("Прогноза за времето во:",       "", "", "", "", "", "", "", ""),
    "ml": ("കാലാവസ്ഥ റിപ്പോർട്ട്:",       "കാലാവസ്", "സമയ മേഖല", "ഇപ്പോൾ", "പ്രഭാതത്തെ", "ഉച്ചതിരിഞ്ഞ്","സൂര്യോദയം", "പരമോന്നത", "സൂര്യാസ്തമയം", "സന്ധ്യ"),
    "mr": ("हवामान अहवालाचे ठिकाण:", "हवामान", "कालक्षेत्र", "आता", "पहाट", "सूर्योदय", "शिखरबिंदु", "सूर्यास्त", "संध्याकाळ"),
    "nb": ("Værmelding for:",               "", "", "", "", "", "", "", ""),
    "nl": ("Weerbericht voor:",             "Weer",     "Tijdzone",     "Nu",       "Dageraad",         "Zonsopkomst",  "Zenit",        "Zonsondergang",    "Schemering"),
    "nn": ("Vêrmelding for:",               "", "", "", "", "", "", "", ""),
    "oc": ("Previsions metèo per:"          "Metèo", "Zòna orària", "Ara", "Auròra", "Alba", "Zenit", "A solelh colc", "Entrelutz"),
    "pl": ("Pogoda w:",                     "", "", "", "", "", "", "", ""),
    "pt": ("Previsão do tempo para:",       "Tempo", "Fuso horário", "Agora", "Alvorada", "Nascer do sol", "Zénite", "Pôr do sol", "Crepúsculo"),
    "pt-br": ("Previsão do tempo para:",    "Tempo", "Fuso horário", "Agora", "Alvorada", "Nascer do sol", "Zénite", "Pôr do sol", "Crepúsculo"),
    "ro": ("Prognoza meteo pentru:",        "", "", "", "", "", "", "", ""),
    "ru": ("Прогноз погоды:",               "Погода",   "Часовой пояс", "Сейчас",   "Рассвет",          "Восход",       "Зенит",        "Закат",            "Сумерки"),
    "sk": ("Predpoveď počasia pre:",        "", "", "", "", "", "", "", ""),
    "sl": ("Vremenska napoved za",          "", "", "", "", "", "", "", ""),
    "sr": ("Временска прогноза за:",        "", "", "", "", "", "", "", ""),
    "sr-lat": ("Vremenska prognoza za:",    "", "", "", "", "", "", "", ""),
    "sv": ("Väderleksprognos för:",         "", "", "", "", "", "", "", ""),
    "sw": ("Ripoti ya hali ya hewa, jiji la:", "", "", "", "", "", "", "", ""),
    "te": ("వాతావరణ సమాచారము:",           "వాతావరణం", "కాల మండలం", "ప్రస్తుతం", "తెల్లవారుజాము", "సూర్యోదయం", "ఉన్నత స్థానం", "సూర్యాస్తమయం", "సందెచీకటి"),
    "th": ("รายงานสภาพอากาศ:",              "", "", "", "", "", "", "", ""),
    "tr": ("Hava beklentisi:",              "Hava Durumu",  "Zaman Dilimi",     "Şimdi",      "Şafak",             "Gün Doğumu",      "Doruk",       "Gün Batımı",           "Akşam"),
    "uk": ("Прогноз погоди для:",           "Погода",   "Часовий пояс", "Зараз",    "Світанок",         "Схід сонця",   "Зеніт",        "Захід сонця",      "Сутінки"),
    "uz": ("Ob-havo bashorati:",            "", "", "", "", "", "", "", ""),
    "vi": ("Báo cáo thời tiết:",            "", "", "", "", "", "", "", ""),
    "zh": ("天气预报：",                    "天气", "时区", "当前", "黎明", "日出", "正午", "日落", "黄昏"),
    "zh-tw": ("天氣預報：",                 "天氣", "時區", "目前", "黎明", "日出", "日正當中", "日落", "黃昏"),
    "zu": ("Isimo sezulu:",                 "", "", "", "", "", "", "", ""),
    "mg": ("Vinavina toetr'andro hoany :",  "Toetr'andro", "Faritra ora", "Ora", "Mangirandratsy", "Maneno akoho", "Mitatao vovonana", "Masoandro milentika", "Crépuscule"),
    "ta": ("வானிலை அறிக்கை:",           "வானிலை",  "நேரம் மண்டலம்",     "இப்போது",      "விடியல்",             "சூரிய உதயம்",      "ஜெனித்",       "சூரிய அஸ்தமனம்",           "அந்தி"),
    }



File: lib/globals.py

"""
global configuration of the project

External environment variables:

    WTTR_MYDIR
    WTTR_GEOLITE
    WTTR_WEGO
    WTTR_LISTEN_HOST
    WTTR_LISTEN_PORT
    WTTR_USER_AGENT

"""
from __future__ import print_function

import logging
import os
import re

MYDIR = os.path.abspath(os.path.dirname(os.path.dirname('__file__')))

if "WTTR_GEOLITE" in os.environ:
    GEOLITE = os.environ["WTTR_GEOLITE"]
else:
    GEOLITE = os.path.join(MYDIR, 'data', "GeoLite2-City.mmdb")

WEGO = os.environ.get("WTTR_WEGO", "/home/igor/go/bin/we-lang")
PYPHOON = "pyphoon-lolcat"

_DATADIR = "/wttr.in"
_LOGDIR = "/wttr.in/log"

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')

PROXY_LOG_ACCESS = os.path.join(_LOGDIR, 'proxy-access.log')
PROXY_LOG_ERRORS = os.path.join(_LOGDIR, 'proxy-errors.log')

MISSING_TRANSLATION_LOG = os.path.join(_LOGDIR, 'missing-translation/%s.log')

ALIASES = os.path.join(MYDIR, "share/aliases")
ANSI2HTML = os.path.join(MYDIR, "share/ansi2html.sh")
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')

IATA_CODES_FILE = os.path.join(MYDIR, 'share/list-of-iata-codes.txt')

TEMPLATES = os.path.join(MYDIR, 'share/templates')
STATIC = os.path.join(MYDIR, 'share/static')

NOT_FOUND_LOCATION = "not found"
DEFAULT_LOCATION = "oymyakon"

MALFORMED_RESPONSE_HTML_PAGE = open(os.path.join(STATIC, 'malformed-response.html')).read()

GEOLOCATOR_SERVICE = 'http://localhost:8004'

# number of queries from the same IP address is limited
# (minute, hour, day) limitations:
QUERY_LIMITS = (300, 3600, 24*3600)

LISTEN_HOST = os.environ.get("WTTR_LISTEN_HOST", "")
try:
    LISTEN_PORT = int(os.environ.get("WTTR_LISTEN_PORT"))
except (TypeError, ValueError):
    LISTEN_PORT = 8002

PROXY_HOST = "127.0.0.1"
PROXY_PORT = 5001
PROXY_CACHEDIR = os.path.join(_DATADIR, "cache/proxy-wwo/")

MY_EXTERNAL_IP = '5.9.243.187'

PLAIN_TEXT_AGENTS = [
    "curl",
    "httpie",
    "lwp-request",
    "wget",
    "python-requests",
    "python-httpx",
    "openbsd ftp",
    "powershell",
    "fetch",
    "aiohttp",
    "http_get",
    "xh",
    "nushell",
]

PLAIN_TEXT_PAGES = [':help', ':bash.function', ':translation', ':iterm2']

TRANSLATION_TABLE = str.maketrans({
    '\u2196': '\u256E',  # '↖' -> '╮'
    '\u2197': '\u256D',  # '↗' -> '╭'
    '\u2198': '\u2570',  # '↘' -> '╰'
    '\u2199': '\u256F',  # '↙' -> '╯'
    '\u26A1': '\u250C\u2518'
})

_IPLOCATION_ORDER = os.environ.get(
    "WTTR_IPLOCATION_ORDER",
    'geoip,ip2location,ipinfo')
IPLOCATION_ORDER = _IPLOCATION_ORDER.split(',')

_IP2LOCATION_KEY_FILE = os.environ.get(
    "WTTR_IP2LOCATION_KEY_FILE",
    os.environ['HOME'] + '/.ip2location.key')
IP2LOCATION_KEY = None
if os.path.exists(_IP2LOCATION_KEY_FILE):
    IP2LOCATION_KEY = open(_IP2LOCATION_KEY_FILE, 'r').read().strip()

_IPINFO_KEY_FILE = os.environ.get(
    "WTTR_IPINFO_KEY_FILE",
    os.environ['HOME'] + '/.ipinfo.key')
IPINFO_TOKEN = None
if os.path.exists(_IPINFO_KEY_FILE):
    IPINFO_TOKEN = open(_IPINFO_KEY_FILE, 'r').read().strip()

_WWO_KEY_FILE = os.environ.get(
    "WTTR_WWO_KEY_FILE",
    os.environ['HOME'] + '/.wwo.key')
WWO_KEY = "key-is-not-specified"
USE_METNO = True
USER_AGENT = os.environ.get("WTTR_USER_AGENT", "")
if os.path.exists(_WWO_KEY_FILE):
    WWO_KEY = open(_WWO_KEY_FILE, 'r').read().strip()
    USE_METNO = False

def error(text):
    "log error `text` and raise a RuntimeError exception"

    if not text.startswith('Too many queries'):
        print(text)
    logging.error("ERROR %s", text)
    raise RuntimeError(text)

def log(text):
    "log error `text` and do not raise any exceptions"

    if not text.startswith('Too many queries'):
        print(text)
        logging.info(text)

def debug_log(text):
    """
    Write `text` to the debug log
    """

    with open('/tmp/wttr.in-debug.log', 'a') as f_debug:
        f_debug.write(text+'\n')

def get_help_file(lang):
    "Return help file for `lang`"

    help_file = os.path.join(MYDIR, 'share/translations/%s-help.txt' % 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)



File: lib/constants.py

#vim: fileencoding=utf-8

WWO_CODE = {
    "113": "Sunny",
    "116": "PartlyCloudy",
    "119": "Cloudy",
    "122": "VeryCloudy",
    "143": "Fog",
    "176": "LightShowers",
    "179": "LightSleetShowers",
    "182": "LightSleet",
    "185": "LightSleet",
    "200": "ThunderyShowers",
    "227": "LightSnow",
    "230": "HeavySnow",
    "248": "Fog",
    "260": "Fog",
    "263": "LightShowers",
    "266": "LightRain",
    "281": "LightSleet",
    "284": "LightSleet",
    "293": "LightRain",
    "296": "LightRain",
    "299": "HeavyShowers",
    "302": "HeavyRain",
    "305": "HeavyShowers",
    "308": "HeavyRain",
    "311": "LightSleet",
    "314": "LightSleet",
    "317": "LightSleet",
    "320": "LightSnow",
    "323": "LightSnowShowers",
    "326": "LightSnowShowers",
    "329": "HeavySnow",
    "332": "HeavySnow",
    "335": "HeavySnowShowers",
    "338": "HeavySnow",
    "350": "LightSleet",
    "353": "LightShowers",
    "356": "HeavyShowers",
    "359": "HeavyRain",
    "362": "LightSleetShowers",
    "365": "LightSleetShowers",
    "368": "LightSnowShowers",
    "371": "HeavySnowShowers",
    "374": "LightSleetShowers",
    "377": "LightSleet",
    "386": "ThunderyShowers",
    "389": "ThunderyHeavyRain",
    "392": "ThunderySnowShowers",
    "395": "HeavySnowShowers",
}

WEATHER_SYMBOL = {
    "Unknown":             "✨",
    "Cloudy":              "☁️",
    "Fog":                 "🌫",
    "HeavyRain":           "🌧",
    "HeavyShowers":        "🌧",
    "HeavySnow":           "❄️",
    "HeavySnowShowers":    "❄️",
    "LightRain":           "🌦",
    "LightShowers":        "🌦",
    "LightSleet":          "🌧",
    "LightSleetShowers":   "🌧",
    "LightSnow":           "🌨",
    "LightSnowShowers":    "🌨",
    "PartlyCloudy":        "⛅️",
    "Sunny":               "☀️",
    "ThunderyHeavyRain":   "🌩",
    "ThunderyShowers":     "⛈",
    "ThunderySnowShowers": "⛈",
    "VeryCloudy": "☁️",
}

WEATHER_SYMBOL_WIDTH_VTE = {
    "✨": 2,
    "☁️": 1,
    "🌫": 2,
    "🌧": 2,
    "🌧": 2,
    "❄️": 1,
    "❄️": 1,
    "🌦": 1,
    "🌦": 1,
    "🌧": 1,
    "🌧": 1,
    "🌨": 2,
    "🌨": 2,
    "⛅️": 2,
    "☀️": 1,
    "🌩": 2,
    "⛈": 1,
    "⛈": 1,
    "☁️": 1,
}

WIND_DIRECTION = [
    "↓", "↙", "←", "↖", "↑", "↗", "→", "↘",
]

MOON_PHASES = (
    "🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"
)

WEATHER_SYMBOL_WI_DAY = {
    "Unknown":             "",
    "Cloudy":              "",
    "Fog":                 "",
    "HeavyRain":           "",
    "HeavyShowers":        "",
    "HeavySnow":           "",
    "HeavySnowShowers":    "",
    "LightRain":           "",
    "LightShowers":        "",
    "LightSleet":          "",
    "LightSleetShowers":   "",
    "LightSnow":           "",
    "LightSnowShowers":    "",
    "PartlyCloudy":        "",
    "Sunny":               "",
    "ThunderyHeavyRain":   "",
    "ThunderyShowers":     "",
    "ThunderySnowShowers": "",
    "VeryCloudy": "",
}

WEATHER_SYMBOL_WI_NIGHT = {
    "Unknown":             "",
    "Cloudy":              "",
    "Fog":                 "",
    "HeavyRain":           "",
    "HeavyShowers":        "",
    "HeavySnow":           "",
    "HeavySnowShowers":    "",
    "LightRain":           "",
    "LightShowers":        "",
    "LightSleet":          "",
    "LightSleetShowers":   "",
    "LightSnow":           "",
    "LightSnowShowers":    "",
    "PartlyCloudy":        "",
    "Sunny":               "",
    "ThunderyHeavyRain":   "",
    "ThunderyShowers":     "",
    "ThunderySnowShowers": "",
    "VeryCloudy": "",
}

WEATHER_SYMBOL_PLAIN = {
    "Unknown":             "?",
    "Cloudy":              "mm",
    "Fog":                 "=",
    "HeavyRain":           "///",
    "HeavyShowers":        "//",
    "HeavySnow":           "**",
    "HeavySnowShowers":    "*/*",
    "LightRain":           "/",
    "LightShowers":        ".",
    "LightSleet":          "x",
    "LightSleetShowers":   "x/",
    "LightSnow":           "*",
    "LightSnowShowers":    "*/",
    "PartlyCloudy":        "m",
    "Sunny":               "o",
    "ThunderyHeavyRain":   "/!/",
    "ThunderyShowers":     "!/",
    "ThunderySnowShowers": "*!*",
    "VeryCloudy": "mmm",
}

WEATHER_SYMBOL_WIDTH_VTE_WI = {
}

WIND_DIRECTION_WI = [
    "", "", "", "", "", "", "", "",
]

WIND_SCALE_WI = [
    "", "", "", "", "", "", "", "", "", "", "", "", "",
]

MOON_PHASES_WI = (
    "", "", "", "", "", "", "",
    "", "", "", "", "", "", "",
    "", "", "", "", "", "", "",
    "", "", "", "", "", "", "",
)

WEATHER_SYMBOL_WEGO = {
    "Unknown": [
        "    .-.      ",
        "     __)     ",
        "    (        ",
        "     `-’     ",
        "      •      "],
    "Sunny": [
        "\033[38;5;226m    \\   /    \033[0m",
        "\033[38;5;226m     .-.     \033[0m",
        "\033[38;5;226m  ― (   ) ―  \033[0m",
        "\033[38;5;226m     `-’     \033[0m",
        "\033[38;5;226m    /   \\    \033[0m"],
    "PartlyCloudy": [
        "\033[38;5;226m   \\  /\033[0m      ",
        "\033[38;5;226m _ /\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m   \\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "             "],
    "Cloudy": [
        "             ",
        "\033[38;5;250m     .--.    \033[0m",
        "\033[38;5;250m  .-(    ).  \033[0m",
        "\033[38;5;250m (___.__)__) \033[0m",
        "             "],
    "VeryCloudy": [
        "             ",
        "\033[38;5;240;1m     .--.    \033[0m",
        "\033[38;5;240;1m  .-(    ).  \033[0m",
        "\033[38;5;240;1m (___.__)__) \033[0m",
        "             "],
    "LightShowers": [
        "\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "\033[38;5;111m     ‘ ‘ ‘ ‘ \033[0m",
        "\033[38;5;111m    ‘ ‘ ‘ ‘  \033[0m"],
    "HeavyShowers": [
        "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;240;1m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;240;1m(___(__) \033[0m",
        "\033[38;5;21;1m   ‚‘‚‘‚‘‚‘  \033[0m",
        "\033[38;5;21;1m   ‚’‚’‚’‚’  \033[0m"],
    "LightSnowShowers": [
        "\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "\033[38;5;255m     *  *  * \033[0m",
        "\033[38;5;255m    *  *  *  \033[0m"],
    "HeavySnowShowers": [
        "\033[38;5;226m _`/\"\"\033[38;5;240;1m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;240;1m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;240;1m(___(__) \033[0m",
        "\033[38;5;255;1m    * * * *  \033[0m",
        "\033[38;5;255;1m   * * * *   \033[0m"],
    "LightSleetShowers": [
        "\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "\033[38;5;111m     ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m* \033[0m",
        "\033[38;5;255m    *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘  \033[0m"],
    "ThunderyShowers": [
        "\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "\033[38;5;228;5m    ⚡\033[38;5;111;25m‘ ‘\033[38;5;228;5m⚡\033[38;5;111;25m‘ ‘ \033[0m",
        "\033[38;5;111m    ‘ ‘ ‘ ‘  \033[0m"],
    "ThunderyHeavyRain": [
        "\033[38;5;240;1m     .-.     \033[0m",
        "\033[38;5;240;1m    (   ).   \033[0m",
        "\033[38;5;240;1m   (___(__)  \033[0m",
        "\033[38;5;21;1m  ‚‘\033[38;5;228;5m⚡\033[38;5;21;25m‘‚\033[38;5;228;5m⚡\033[38;5;21;25m‚‘ \033[0m",
        "\033[38;5;21;1m  ‚’‚’\033[38;5;228;5m⚡\033[38;5;21;25m’‚’  \033[0m"],
    "ThunderySnowShowers": [
        "\033[38;5;226m _`/\"\"\033[38;5;250m.-.    \033[0m",
        "\033[38;5;226m  ,\\_\033[38;5;250m(   ).  \033[0m",
        "\033[38;5;226m   /\033[38;5;250m(___(__) \033[0m",
        "\033[38;5;255m     *\033[38;5;228;5m⚡\033[38;5;255;25m*\033[38;5;228;5m⚡\033[38;5;255;25m* \033[0m",
        "\033[38;5;255m    *  *  *  \033[0m"],
    "LightRain": [
        "\033[38;5;250m     .-.     \033[0m",
        "\033[38;5;250m    (   ).   \033[0m",
        "\033[38;5;250m   (___(__)  \033[0m",
        "\033[38;5;111m    ‘ ‘ ‘ ‘  \033[0m",
        "\033[38;5;111m   ‘ ‘ ‘ ‘   \033[0m"],
    "HeavyRain": [
        "\033[38;5;240;1m     .-.     \033[0m",
        "\033[38;5;240;1m    (   ).   \033[0m",
        "\033[38;5;240;1m   (___(__)  \033[0m",
        "\033[38;5;21;1m  ‚‘‚‘‚‘‚‘   \033[0m",
        "\033[38;5;21;1m  ‚’‚’‚’‚’   \033[0m"],
    "LightSnow": [
        "\033[38;5;250m     .-.     \033[0m",
        "\033[38;5;250m    (   ).   \033[0m",
        "\033[38;5;250m   (___(__)  \033[0m",
        "\033[38;5;255m    *  *  *  \033[0m",
        "\033[38;5;255m   *  *  *   \033[0m"],
    "HeavySnow": [
        "\033[38;5;240;1m     .-.     \033[0m",
        "\033[38;5;240;1m    (   ).   \033[0m",
        "\033[38;5;240;1m   (___(__)  \033[0m",
        "\033[38;5;255;1m   * * * *   \033[0m",
        "\033[38;5;255;1m  * * * *    \033[0m"],
    "LightSleet": [
        "\033[38;5;250m     .-.     \033[0m",
        "\033[38;5;250m    (   ).   \033[0m",
        "\033[38;5;250m   (___(__)  \033[0m",
        "\033[38;5;111m    ‘ \033[38;5;255m*\033[38;5;111m ‘ \033[38;5;255m*  \033[0m",
        "\033[38;5;255m   *\033[38;5;111m ‘ \033[38;5;255m*\033[38;5;111m ‘   \033[0m"],
    "Fog": [
        "             ",
        "\033[38;5;251m _ - _ - _ - \033[0m",
        "\033[38;5;251m  _ - _ - _  \033[0m",
        "\033[38;5;251m _ - _ - _ - \033[0m",
        "             "],
}

LOCALE = {
    "af": "af_ZA", "ar": "ar_TN", "az": "az_AZ", "be": "be_BY", "bg": "bg_BG",
    "bs": "bs_BA", "ca": "ca_ES", "cs": "cs_CZ", "cy": "cy_GB", "da": "da_DK",
    "de": "de_DE", "el": "el_GR", "eo": "eo", "es": "es_ES", "et": "et_EE",
    "fa": "fa_IR", "fi": "fi_FI", "fr": "fr_FR", "fy": "fy_NL", "ga": "ga_IE",
    "he": "he_IL", "hr": "hr_HR", "hu": "hu_HU", "hy": "hy_AM", "ia": "ia",
    "id": "id_ID", "is": "is_IS", "it": "it_IT", "ja": "ja_JP", "jv": "en_US",
    "ka": "ka_GE", "ko": "ko_KR", "kk": "kk_KZ", "ky": "ky_KG", "lt": "lt_LT",
    "lv": "lv_LV", "mk": "mk_MK", "ml": "ml_IN", "nb": "nb_NO", "nl": "nl_NL",
    "nn": "nn_NO", "pt": "pt_PT", "pt-br":"pt_BR", "pl": "pl_PL", "ro": "ro_RO",
    "ru": "ru_RU", "sv": "sv_SE", "sk": "sk_SK", "sl": "sl_SI", "sr": "sr_RS",
    "sr-lat": "sr_RS@latin", "sw": "sw_KE", "th": "th_TH", "tr": "tr_TR", "uk": "uk_UA",
    "uz": "uz_UZ", "vi": "vi_VN", "zh": "zh_TW", "zu": "zu_ZA", "mg": "mg_MG",
}



File: lib/translations.py

# vim: fileencoding=utf-8

"""
Translation of almost everything.
"""

FULL_TRANSLATION = [
    "am", "ar", "af", "be", "bn",  "ca", "da", "de", "el", "et",
    "fr", "fa", "gl", "hi", "hu", "ia", "id", "it", "lt", "mg",
    "nb", "nl", "oc", "pl", "pt-br", "ro",
    "ru", "ta", "tr", "th", "uk", "vi", "zh-cn", "zh-tw",
]

PARTIAL_TRANSLATION = [
    "az", "bg", "bs", "cy", "cs",
    "eo", "es", "eu", "fi", "ga", "hi", "hr",
    "hy", "is", "ja", "jv", "ka", "kk",
    "ko", "ky", "lv", "mk", "ml", "mr", "nl", "fy",
    "nn", "pt", "pt-br", "sk", "sl", "sr",
    "sr-lat", "sv", "sw", "te", "uz", "zh",
    "zu", "he",
]

PROXY_LANGS = [
    "af", "am", "ar", "az", "be", "bn", "bs", "ca",
    "cy", "de", "el", "eo", "et", "eu", "fa", "fr",
    "fy", "ga", "gl", "he", "hr", "hu", "hy",
    "ia", "id", "is", "it", "ja", "kk",
    "lt", "lv", "mg", "mk", "mr", "nb", "nn", "oc",
    "ro", "ru", "sl", "th", "pt-br", "uk", 
    "uz", "vi", "zh-cn", "zh-tw",
]

SUPPORTED_LANGS = FULL_TRANSLATION + PARTIAL_TRANSLATION


MESSAGE = {
    'NOT_FOUND_MESSAGE': {
        'en': u"""
We were unable to find your location
so we have brought you to Oymyakon,
one of the coldest permanently inhabited locales on the planet.
""",
        'af': u"""
Ons kon nie u ligging opspoor nie
gevolglik het ons vir u na Oymyakon geneem,
een van die koudste permanent bewoonde plekke op aarde.
""",
        'am': u"""
አካባቢዎን ማግኘት ስላልቻልን ወደ ኦይማያኮን አምጥተንዎታል፤ 
በአለም ላይ በጣም ቀዝቃዛ እና ሰው አልባ ከሆኑት ቦታዎች አንዱ።
""",
        'ar': u"""
تعذر علينا العثور على موقعك
لذلك قمنا بجلبك إلي أويمياكون,
 إحدى الأماكن المَأْهُولة الأكثر برودة علي الإطلاق في هذا الكوكب.
""",
        'be': u"""
Ваша месцазнаходжанне вызначыць не атрымалася,
таму мы пакажам вам надвор'е ў Аймяконе,
самым халодным населеным пункце на планеце.
Будзем спадзявацца, што ў вас сёння надвор'е лепей!
""",
        'bg':u"""
Не успяхме да открием вашето местоположение
така че ви доведохме в Оймякон,
едно от най-студените постоянно обитавани места на планетата.
""",
        'bn' : u"""
দুঃখিত, আপনার অবস্থান আমরা খুঁজে পাইনি।
তাই, আমরা আপনাকে নিয়ে এসেছি ওয়মিয়াকনে, 
যা পৃথিবীর শীতলতম স্থায়ী জন-বসতিগুলোর একটি। 
""",
        'bs': u"""
Nismo mogli pronaći vašu lokaciju,
tako da smo te doveli do Oymyakon,
jedan od najhladnijih stalno naseljena mjesta na planeti.
Nadamo se da ćete imati bolje vreme!
""",
        'ca': u"""
Hem estat incapaços de trobar la seva ubicació,
per això l'hem portat fins Oymyakon,
un dels llocs més freds i permanentment deshabitats del planeta.
""",
        'cs': u"""
Nepodařilo se nám najít vaši polohu,
takže jsme vás přivedl do Ojmjakonu.
Je to jedno z nejchladnějších trvale obydlených míst na planetě.
Doufáme, že budete mít lepší počasí!
""",
        'cy': u"""
Ni darganfyddwyd eich lleoliad,
felly rydym wedi dod â chi i Oymyakon,
un o'r llefydd oeraf ar y blaned ble mae pobl yn dal i fyw!
""",
        'de': u"""
Wir konnten Ihren Standort nicht finden,
also haben wir Sie nach Oimjakon gebracht,
einer der kältesten dauerhaft bewohnten Orte auf dem Planeten.
Wir hoffen, dass Sie besseres Wetter haben!
""",
        'el': u"""
Δεν μπορέσαμε να βρούμε την τοποθεσία σου,
για αυτό διαλέξαμε το Οϊμιάκον για εσένα,
μία από τις πιο κρύες μόνιμα κατοικημένες περιοχές στον πλανήτη.
Ελπίζουμε να έχεις καλύτερο καιρό!
""",
        'es': u"""
No hemos logrado encontrar tu ubicación,
así que hemos decidido enseñarte el tiempo en Oimiakón,
uno de los sitios más fríos y permanentemente deshabitados del planeta.
""",
        'eu': u"""
Ezin izan dugu zure kokapena aurkitu,
beraz eguraldia Oymyakonen erakustea erabaki dugu,
munduko lekurik hotz eta hutseneraiko bat
""",
        'fa': u"""
ما نتونستیم مکان شما رو پیدا کنیم. به همین خاطر شما رو به اویمیاکن بردیم
، یکی از سردترین مکان های روی زمین که اصلا قابل سکونت نیست!
""",
        'fi': u"""
Emme löytänyt sijaintiasi, joten toimme sinut Oimjakoniin,
yhteen maailman kylmimmistä pysyvästi asutetuista paikoista.
Toivottavasti sinulla on parempi sää!
""",
        'fr': u"""
Nous n'avons pas pu déterminer votre position,
Nous vous avons donc amenés à Oïmiakon,
l'un des endroits les plus froids habités en permanence sur la planète.
Nous espérons qu'il fait meilleur chez vous !
""",
        'ga': u"""
Ní rabhamar ábalta do cheantar a aimsiú
mar sin thugamar go dtí Oymyakon,
tú ceann do na ceantair bhuanáitrithe is fuaire ar domhan.
""",
        'gl': u"""
Non logramos atopar a túa localización
polo que te trouxemos até Oimiakón,
un dos lugares máis fríos e permamentemente deshabitados do planeta.
""",
        'hi': u"""
हम आपका स्थान खोजने में असमर्थ है,
इसलिए हम आपको ओयमयाकोन पर ले आए है,
जो ग्रह के सबसे ठंडे स्थानों में से एक है|
""",
        'hu': u"""
Nem sikerült megtalálni a pozíciódat,
így elhoztunk Ojmjakonba;
az egyik leghidegebb állandóan lakott településre a bolygón.
""",
        'hy': u"""
Ձեր գտնվելու վայրը չհաջողվեց որոշել,
այդ պատճառով մենք ձեզ կցուցադրենք եղանակը Օյմյակոնում.
երկրագնդի ամենասառը բնակավայրում։
Հույս ունենք որ ձեր եղանակը այսօր ավելի լավն է։
""",
        'ia': u"""
Nos non trovate su location,
assi nos su apporte a Oymyakon,
un del plus frigide locos habita super le planeta!
""",
        'id': u"""
Kami tidak dapat menemukan lokasi anda,
jadi kami membawa anda ke Oymyakon,
salah satu tempat terdingin yang selalu dihuni di planet ini!
""",
        'is': u"""
Við finnum ekki staðsetninguna þína og vísum þér þar með á Ojmjakon,
ein af köldustu byggðum jarðar.
Vonandi er betra veður hjá þér.
""",
        'it': u"""
Non siamo riusciti a trovare la sua posizione
quindi la abbiamo portato a Oymyakon,
uno dei luoghi abitualmente abitati più freddi del pianeta.
Ci auguriamo che le condizioni dove lei si trova siano migliori!
""",
        'ja': u"""
指定された場所が見つかりませんでした。
代わりにオイミャコンの天気予報を表示しています。
オイミャコンは地球上で最も寒い居住地の一つです。
""",
        'ko': u"""
지정된 장소를 찾을 수 없습니다,
대신 오이먀콘의 일기 예보를 표시합니다,
오이먀콘은 지구상에서 가장 추운 곳에 위치한 마을입니다!
""",
        'lt': u"""
Mums nepavyko rasti jūsų vietovės,
todėl mes nukreipėme jus į Omjakoną,
vieną iš šalčiausių nuolatinių gyvenviečių planetoje.
""",
        'lv': u"""
Mēs nevarējām atrast jūsu atrašanās vietu tādēļ nogādājām jūs Oimjakonā,
vienā no aukstākajām apdzīvotajām vietām uz planētas.
""",
        'mk': u"""
Неможевме да ја пронајдеме вашата локација,
затоа ве однесовме во Ојмајкон,
еден од најладните трајно населени места на планетата.
""",
        'mr': u"""
आमहाला तुमचे स्थळ सापडले नाही.
म्हणून आम्ही तुम्हाला ओयम्याकोन येथे आणले आहे,
जे कि आपल्या ग्रहावरील सर्वात थंड वस्तिस्थानांपैकी एक आहे.
""",
        'nb': u"""
Vi kunne ikke finne din lokasjon,
så her får du Ojmjakon, et av de kaldeste bebodde stedene på planeten.
Vi håper været er bedre hos deg!
""",
        'nl': u"""
Wij konden uw locatie niet vaststellen
dus hebben we u naar Ojmjakon gebracht,
één van de koudste permanent bewoonde gebieden op deze planeet.
""",
        'fy': u"""
Wy koenen jo lokaasje net fêststelle
dus wy ha jo nei Ojmjakon brocht,
ien fan de kâldste permanent bewenbere plakken op ierde.
""",
        'oc': u"""
Avèm pas pogut determinar vòstra posicion,
Vos avèm doncas menat a Oïmiakon,
un dels endreches mai freds abitat permanéncia del monde.
Esperam que fa melhor en çò vòstre !
""",
        'pt': u"""
Não conseguimos encontrar a sua localização,
então decidimos te mostrar o tempo em Oymyakon,
um dos lugares mais frios e permanentemente desabitados do planeta.
""",
        'pt-br': u"""
Não conseguimos encontrar a sua localização,
então decidimos te mostrar o tempo em Oymyakon,
um dos lugares mais frios e permanentemente desabitados do planeta.
""",
        'pl': u"""
Nie udało nam się znaleźć podanej przez Ciebie lokalizacji,
więc zabraliśmy Cię do Ojmiakonu,
jednego z najzimniejszych, stale zamieszkanych miejsc na Ziemi.
Mamy nadzieję, że u Ciebie jest cieplej!
""",
        'ro': u"""
Nu v-am putut identifica localitatea, prin urmare vă arătăm vremea din Oimiakon,
una dintre cele mai reci localități permanent locuite de pe planetă.
Sperăm că aveți vreme mai bună!
""",
        'ru': u"""
Ваше местоположение определить не удалось,
поэтому мы покажем вам погоду в Оймяконе,
самом холодном населённом пункте на планете.
Будем надеяться, что у вас сегодня погода лучше!
""",
        'sk': u"""
Nepodarilo sa nám nájsť vašu polohu,
takže sme vás priviedli do Ojmiakonu.
Je to jedno z najchladnejších trvale obývaných miest na planéte.
Dúfame, že budete mať lepšie počasie!
""",
        'sr': u"""
Нисмо успели да пронађемо Вашу локацију,
па смо Вас довели у Ојмјакон,
једно од најхладнијих стално насељених места на планети.
Надамо се да је време код Вас боље него што је то случај овде!
""",
        'sv': u"""
Vi lyckades inte hitta er plats så vi har istället tagit er till Ojmjakon,
en av planetens kallaste platser med permanent bosättning.
Vi hoppas att vädret är bättre hos dig!
""",
        'tr': u"""
Aradığınız konum bulunamadı. O yüzden sizi dünyadaki en soğuk sürekli
yerleşim yerlerinden biri olan Oymyakon'e getirdik.
Umarız sizin olduğunuz yerde havalar daha iyidir!
""",
        'te': u"""
మేము మీ స్థానాన్ని కనుగొనలేకపోయాము
కనుక మనం "ఓమాయకాన్కు" తీసుకొని వచ్చాము,
భూమిపై అత్యల్ప శాశ్వతంగా నివసించే స్థానిక ప్రదేశాలలో ఒకటి.
""",
        'th': u"""
เราไม่สามารถหาตำแหน่งของคุณได้เราจึงนำคุณไปสู่ Oymyakon หมู่บ้านที่หนาวที่สุดในโลก!
""",
        'uk': u"""
Ми не змогли визначити Ваше місцезнаходження,
тому покажемо Вам погоду в Оймяконі —
найхолоднішому населеному пункті на планеті.
Будемо сподіватися, що у Вас сьогодні погода краще!
""",
        'uz': u"""
Sizning joylashuvingizni aniqlay olmadik,
shuning uchun sizga sayyoramizning eng sovuq aholi punkti - Oymyakondagi ob-havo haqida ma'lumot beramiz.
Umid qilamizki, sizda bugungi ob-havo bundan yaxshiroq!
""",
        'zh': u"""
我们无法找到您的位置,
当前显示奥伊米亚康(Oymyakon)，这个星球上最冷的人类定居点。
""",
        'da': u"""
Vi kunne desværre ikke finde din lokation
så vi har bragt dig til Oymyakon,
En af koldeste og helt ubolige lokationer på planeten.
""",
        'et': u"""
Me ei suutnud tuvastada teie asukohta
ning seetõttu paigutasime teid Oymyakoni,
mis on üks kõige külmemaid püsivalt asustatud paiku planeedil.
""",
        'vi': u"""
Chúng tôi không tìm thấy địa điểm của bạn
vì vậy chúng tôi đưa bạn đến Oymyakon,
một trong những nơi lạnh nhất có người sinh sống trên trái đất.
""",
        'zh-tw': u"""
我們找不到您的位置
所以我們帶您到奧伊米亞康，
這個星球上有人類定居最冷之處。
""",
        'mg': u"""
Tsy hita ny toerana misy anao koa nentinay tany Oymyakon ianao,
iray amin'ireo toerana mangatsiaka indrindra tsisy mponina eto an-tany.
""",
        'ta': u"""
உங்கள் இருப்பிடத்தை எங்களால் கண்டுபிடிக்க முடியவில்லை
எனவே நாங்கள் உங்களை ஓமியாகோனுக்கு அழைத்து வந்தோம்.
கிரகத்தின் குளிர்ந்த நிரந்தரமாக வசிக்கும் இடங்களில் ஒன்று.
""",
    },

    'UNKNOWN_LOCATION': {
        'en': u'Unknown location',
        'af': u'Onbekende ligging',
        'am': u'ያልታወቀ ቦታ',
        'ar': u'موقع غير معروف',
        'be': u'Невядомае месцазнаходжанне',
        'bg': u'Неизвестно местоположение',
        'bn': u'অজানা অবস্থান',
        'bs': u'Nepoznatoja lokacija',
        'ca': u'Ubicació desconeguda',
        'cs': u'Neznámá poloha',
        'cy': u'Lleoliad anhysbys',
        'de': u'Unbekannter Ort',
        'da': u'Ukendt lokation',
        'el': u'Άνγωστη τοποθεσία',
        'es': u'Ubicación desconocida',
        'et': u'Tundmatu asukoht',
        'eu': u'Kokapen ezezaguna',
        'fa': u'مکان نامعلوم',
        'fi': u'Tuntematon sijainti',
        'fr': u'Emplacement inconnu',
        'ga': u'Ceantar anaithnid',
        'gl': u'Localización descoñecida',
        'hi': u'अज्ञात स्थान',
        'hu': u'Ismeretlen lokáció',
        'hy': u'Անհայտ գտնվելու վայր',
        'id': u'Lokasi tidak diketahui',
        'ia': u'Location incognite',
        'is': u'Óþekkt staðsetning',
        'it': u'Località sconosciuta',
        'ja': u'未知の場所です',
        'ko': u'알 수 없는 장소',
        'kk': u'',
        'lt': u'Nežinoma vietovė',
        'lv': u'Nezināma atrašanās vieta',
        'mk': u'Непозната локација',
        'mr': u'अज्ञात स्थळ',
        'nb': u'Ukjent sted',
        'nl': u'Onbekende locatie',
        'oc': u'Emplaçament desconegut',        
        'fy': u'Ûnbekende lokaasje',
        'pl': u'Nieznana lokalizacja',
        'pt': u'Localização desconhecida',
        'pt-br': u'Localização desconhecida',
        'ro': u'Localitate necunoscută',
        'ru': u'Неизвестное местоположение',
        'sk': u'Neznáma poloha',
        'sl': u'Neznano lokacijo',
        'sr': u'Непозната локација',
        'sv': u'Okänd plats',
        'te': u'తెలియని ప్రదేశం',
        'tr': u'Bilinmeyen konum',
        'th': u'ไม่สามารถระบุตำแหน่งได้',
        'uk': u'Невідоме місце',
        'uz': u'Аникланмаган худуд',
        'zh': u'未知地点',
        'vi': u'Địa điểm không xác định',
        'zh-tw': u'未知位置',
        'mg': u'Toerana tsy fantatra',
        'ta': u'தெரியாத இடம்',
    },

    'LOCATION': {
        'en': u'Location',
        'af': u'Ligging',
        'ar': u'الموقع',
        'be': u'Месцазнаходжанне',
        'bg': u'Местоположение',
        'bn': u'অবস্থান',
        'bs': u'Lokacija',
        'ca': u'Ubicació',
        'cs': u'Poloha',
        'cy': u'Lleoliad',
        'de': u'Ort',
        'da': u'Lokation',
        'el': u'Τοποθεσία',
        'es': u'Ubicación',
        'et': u'Asukoht',
        'eu': u'Kokaena',
        'fa': u'مکان',
        'fi': u'Tuntematon sijainti',
        'fr': u'Emplacement',
        'ga': u'Ceantar',
        'gl': u'Localización',
        'hi': u'स्थान',
        'hu': u'Lokáció',
        'hy': u'Դիրք',
        'ia': u'Location',
        'id': u'Lokasi',
        'is': u'Staðsetning',
        'it': u'Località',
        'ja': u'位置情報',
        'ko': u'위치',
        'kk': u'',
        'lt': u'Vietovė',
        'lv': u'Atrašanās vieta',
        'mk': u'Локација',
        'mr': u'स्थळ',
        'nb': u'Sted',
        'nl': u'Locatie',
        'oc': u'Emplaçament',        
        'fy': u'Lokaasje',
        'pl': u'Lokalizacja',
        'pt': u'Localização',
        'pt-br': u'Localização',
        'ro': u'Localitate',
        'ru': u'Местоположение',
        'sk': u'Poloha',
        'sl': u'Lokacijo',
        'sr': u'Локација',
        'sv': u'Plats',
        'zh': u'地点',
        'te': u'స్థానము',
        'tr': u'Konum',
        'th': u'ตำแหน่ง',
        'uk': u'Місцезнаходження',
        'vi': u'Địa điểm',
        'zh-tw': u'位置',
        'mg': u'Toerana',
        'ta': u'இடம்',
    },

    'CAPACITY_LIMIT_REACHED': {
        'en': u"""
Sorry, we are running out of queries to the weather service at the moment.
Here is the weather report for the default city (just to show you what it looks like).
We will get new queries as soon as possible.
You can follow https://twitter.com/igor_chubin for the updates.
======================================================================================
""",
        'af': u"""
Verskoning, ons oorskry tans die vermoë om navrae aan die weerdiens te rig.
Hier is die weerberig van 'n voorbeeld ligging (bloot om aan u te wys hoe dit lyk).
Ons sal weereens nuwe navrae kan hanteer so gou as moontlik.
U kan vir https://twitter.com/igor_chubin volg vir opdaterings.
======================================================================================
""",
        'am': u"""
ይቅርታ ፤ ለጊዜው መጠይቆችን ማስተናገድ አልቻልንም፡፡
የነባሪ ከተማው የአየር ሁኔታ ሪፖርት ይኸውልዎ (ምን እንደሚመስል ለማሳየት)።
አዳዲስ መጠይቆችን በተቻለ ፍጥነት ለማስተናገድ እንሞክራለን።
ለበለጠ መረጃ እባክዎ https://twitter.com/igor_chubin ን ይከተሉ።
======================================================================================
""",
        'ar': u"""
عذرًا ، استعلامات خدمة الطقس نفذت في الوقت الحالي.
هذا هو تقرير الطقس للمدينة الافتراضية (فقط لتظهر لك كيف تبدو).
سوف نتلقى استعلامات جديدة في أقرب وقت ممكن.
يمكنك متابعة https://twitter.com/igor_chubin لآخر المستجدات.
======================================================================================
""",
        'be': u"""
Прабачце, мы выйшлі за ліміты колькасці запытаў да службы надвор'я ў дадзены момант.
Вось прагноз надвор'я для горада па змаўчанні (толькі, каб паказаць вам, як гэта выглядае).
Мы вернемся як мага хутчэй.
Вы можаце сачыць на https://twitter.com/igor_chubin за абнаўленнямі.
======================================================================================
""",
        'bg': u"""
Съжаляваме, количеството заявки към услугата за предсказване на време е почти изчерпано.
Ето доклад за града по подразбиране (просто да видите как изглежда).
Ще осогурим допълнителни заявки максимално бързо.
Може да последвате https://twitter.com/igor_chubin за обновления.
""",
        'bn': u"""
দুঃখিত, এই মুহুর্তে আবহাওয়া পরিসেবাতে  আমাদের কুইরী শেষ হয়ে আসছে। 
এখানে ডিফল্ট শহরের আবহাওয়ার প্রতিবেদন রয়েছে (এটি দেখতে কেমন তা আপনাকে দেখানোর জন্য)। 
আমরা খুব দ্রুত নতুন কুইরী পাওয়ার ব্যবস্থা করছি। 
আপডেটের জন্য আপনি https://twitter.com/igor_chubin অনুসরণ করতে পারেন। 
""",
        'bs': u"""
Žao mi je, mi ponestaje upita i vremenska prognoza u ovom trenutku.
Ovdje je izvještaj o vremenu za default grada (samo da vam pokažem kako to izgleda).
Mi ćemo dobiti nove upite u najkraćem mogućem roku.
Možete pratiti https://twitter.com/igor_chubin za ažuriranja.
======================================================================================
""",
        'ca': u"""
Disculpa'ns, ens hem quedat momentàniament sense consultes al servei meteorològic.
Aquí t'oferim l'informe del temps a la ciutat per defecte (només per mostrar quin aspecte té).
Obtindrem noves consultes tan aviat com ens sigui possible.
Pots seguir https://twitter.com/igor_chubin per noves actualitzacions.
======================================================================================
""",
        'de': u"""
Entschuldigung, wir können momentan den Wetterdienst nicht erreichen.
Dafür zeigen wir Ihnen das Wetter an einem Beispielort, damit Sie sehen, wie die Seite das Wetter anzeigt.
Wir werden versuchen das Problem so schnell wie möglich zu beheben.
Folgen Sie https://twitter.com/igor_chubin für Updates.
======================================================================================
""",
        'cy': u"""
Rydym yn brin o ymholiadau i'r gwasanaeth tywydd ar hyn o bryd.
Felly dyma'r adroddiad tywydd ar gyfer y ddinas ragosod (er mwyn arddangos sut mae'n edrych).
Byddwn gyda ymholiadau newydd yn fuan.
Gellir dilyn https://twitter.com/igor_chubin i gael newyddion pellach.
======================================================================================
""",
        'es': u"""
Lo siento, hemos alcanzado el límite de peticiones al servicio de previsión del tiempo en este momento.
A continuación, la previsión del tiempo para una ciudad estándar (solo para que puedas ver el formato del reporte).
Muy pronto volveremos a tener acceso a las peticiones.
Puedes seguir https://twitter.com/igor_chubin para estar al tanto de la situación.
======================================================================================
""",
        'fa': u"""
متأسفانه در حال حاضر ظرفیت ما برای درخواست به سرویس هواشناسی به اتمام رسیده.
اینجا می تونید گزارش هواشناسی برای شهر پیش فرض رو ببینید (فقط برای اینه که بهتون نشون بدیم چه شکلی هست)
ما تلاش میکنیم در اسرع وقت ظرفیت جدید به دست بیاریم.
برای دنبال کردن اخبار جدید میتونید https://twitter.com/igor_chubin رو فالو کنید.
======================================================================================
""",
        'fr': u"""
Désolé, nous avons épuisé les requêtes vers le service météo.
Voici un bulletin météo de l'emplacement par défaut (pour vous donner un aperçu).
Nous serons très bientôt en mesure de faire de nouvelles requêtes.
Vous pouvez suivre https://twitter.com/igor_chubin pour rester informé.
======================================================================================
""",
        'ga': u"""
Tá brón orainn, níl mórán iarratas le fail chuig seirbhís na haimsire faoi láthair.
Seo duit réamhaisnéis na haimsire don chathair réamhshocraithe (chun é a thaispeaint duit).
Gheobhaimid iarratais nua chomh luath agus is feidir.
Lean orainn ar https://twitter.com/igor_chubin don eolas is déanaí.
======================================================================================
""",
        'gl': u"""
Desculpa, estamos a chegar ao límite de peticións ao servizo meteorolóxico neste momento.
Aquí está a previsión do tempo para a cidade por defecto (tan só para amosarche un exemplo).
Imos obter máis peticións tan pronto como poidamos.
Podes seguir https://twitter.com/igor_chubin para estares actualizada.
======================================================================================
""",
        'hi': u"""
क्षमा करें, इस समय हम मौसम सेवा से संपर्क नहीं कर पा रहे है।
यहा पूर्व निर्धारित शहर के लिए मौसम की जानकारी (आपको यह दिखाने के लिए कि यह कैसा दिखता है)।
हम जल्द से जल्द मौसम सेवा से संपर्क करने नई कोशिश करेंगे।
आप अपडेट के लिए https://twitter.com/igor_chubin का अनुसरण कर सकते हैं।
======================================================================================
""",
        'hu': u"""
Sajnáljuk, kifogytunk az időjárási szolgáltatásra fordított erőforrásokból.
Itt van az alapértelmezett város időjárási jelentése (hogy lásd, hogyan néz ki).
A lehető leghamarabb új erőforrásokat fogunk kapni.
A frissítésekért tekintsd meg a https://twitter.com/igor_chubin oldalt.
======================================================================================
""",
        'hy': u"""
Կներեք, այս պահին մենք գերազանցել ենք եղանակային տեսության կայանին հարցումների քանակը.
Կարող եք տեսնել տիպային եղանակը զեկուցում հիմնական քաղաքի համար (Ուղղակի որպես նմուշ):
Մենք մշտապես աշխատում ենք հարցումների քանակը բարելավելու ուղղությամբ:
Կարող եք հետևել մեզ https://twitter.com/igor_chubin թարմացումների համար.
======================================================================================
""",
        'ia': u"""
Pardono, nos ha exhaurite inquisitione del servicio tempore alora.
Ecce es le reporto tempore por qualque citate (demonstrar le reporte).
Nos recipera inquisitione nove tosto.
Tu pote abonar a https://twitter.com/igor_chubin por nove information.
======================================================================================
""",
        'id': u"""
Maaf, kami kehabian permintaan ke layanan cuaca saat ini.
Ini adalah laporan cuaca dari kota standar (hanya untuk menunjukkan kepada anda bagaimana tampilannya).
Kami akan mencoba permintaan baru lagi sesegera mungkin.
Anda dapat mengikuti https://twitter.com/igor_chubin untuk informasi terbaru.
======================================================================================
""",
        'it': u"""
Scusate, attualmente stiamo esaurendo le risorse a disposizione del servizio meteo.
Qui trovate il bollettino del tempo per la città di default (solo per mostrarvi come si presenta).
Potremo elaborare nuove richieste appena possibile.
Potete seguire https://twitter.com/igor_chubin per gli aggiornamenti.
======================================================================================
""",
        'ko': u"""
죄송합니다. 현재 날씨 정보를 가져오는 쿼리 요청이 한도에 도달했습니다.
대신 기본으로 설정된 도시에 대한 일기 예보를 보여드리겠습니다. (이는 단지 어떻게 보이는지 알려주기 위함입니다).
쿼리 요청이 가능한 한 빨리 이루어질 수 있도록 하겠습니다.
업데이트 소식을 원하신다면 https://twitter.com/igor_chubin 을 팔로우 해주세요.
======================================================================================
""",
        'lt': u"""
Atsiprašome, šiuo metu pasiekėme orų prognozės paslaugos užklausų ribą.
Štai orų prognozė numatomam miestui (tam, kad parodytume, kaip ji atrodo).
Naujas užklausas priimsime, kai tik galėsime.
Atnaujinimus galite sekti https://twitter.com/igor_chubin
======================================================================================
""",
        'lv': u"""
Atvainojiet, uz doto brīdi mēs esam mazliet noslogoti.
Šeit ir laika ziņas noklusējuma pilsētai (lai parādītu jums, kā izskatās izveidotais ziņojums).
Mēs atsāksim darbu cik ātri vien varēsim.
Jūs varat sekot https://twitter.com/igor_chubin lai redzētu visus jaunumus.
======================================================================================
""",
        'mk': u"""
Извинете, ни снемуваат барања за до сервисот кој ни нуди временска прогноза во моментот.
Еве една временска прогноза за град (за да видите како изгледа).
Ќе добиеме нови барања најбрзо што можеме.
Следете го https://twitter.com/igor_chubin за известувања
======================================================================================
""",
        'mr': u"""
क्षमस्व, याक्षणी आम्ही हवामान सेवेशी संपर्क करू शकत नाही.
हा एका पूर्वनिर्धारित शहराचा हवामान अहवाल आहे (केवळ तो कसा दिसतो हे दाखवण्याकरिता).
आम्ही लवकरात लवकर सेवा पुनः चालू करण्याचा प्रयत्न करू.
अद्यावत माहितीसाठी तुम्ही https://twitter.com/igor_chubin चे अनुसरण करू शकता.
======================================================================================
""",
        'nb': u"""
Beklager, vi kan ikke nå værtjenesten for øyeblikket.
Her er værmeldingen for standardbyen så du får se hvordan tjenesten ser ut.
Vi vil forsøke å fikse problemet så snart som mulig.
Du kan følge https://twitter.com/igor_chubin for oppdateringer.
======================================================================================
""",
        'zh': u"""
抱歉，当前天气服务不可用。
以下显示默认城市（只对您可见）。
我们将会尽快获取新数据。
您可以通过 https://twitter.com/igor_chubin 获取最新动态。
======================================================================================
""",
        'nl': u"""
Excuse, wij kunnen u op dit moment dit weerbericht niet laten zien.
Hier is het weerbericht voor de standaard stad(zodat u weet hoe het er uitziet)
Wij lossen dit probleem zo snel mogelijk op.
voor updates kunt u ons op https://twitter.com/igor_chubin volgen.
======================================================================================
""",
        'oc': u"""
O planhèm, avèm pas mai de requèstas cap al servici metèo.
Vaquí las prevision metèo de l'emplaçament per defaut (per vos donar un apercebut).
Poirem lèu ne far de novèlas.
Podètz seguir https://twitter.com/igor_chubin per demorar informat.
======================================================================================
""",
        'fy': u"""
Excuses, wy kinne op dit moment 't waarberjocht net sjin litte.
Hjir is 't waarberjocht foar de standaard stêd.
Wy losse dit probleem sa gau mooglik op.
Foar updates kinne jo ús op https://twitter.com/igor_chubin folgje.
======================================================================================
""",
        'pl': u"""
Bardzo nam przykro, ale chwilowo wykorzystaliśmy limit zapytań do serwisu pogodowego.
To, co widzisz jest przykładowym raportem pogodowym dla domyślnego miasta.
Postaramy się przywrócić funkcjonalność tak szybko, jak to tylko możliwe.
Możesz śledzić https://twitter.com/igor_chubin na Twitterze, aby być na bieżąco.
======================================================================================
""",
        'pt': u"""
Desculpe-nos, estamos atingindo o limite de consultas ao serviço de previsão do tempo neste momento.
Veja a seguir a previsão do tempo para uma cidade padrão (apenas para você ver que aspecto o relatório tem).
Em breve voltaremos a ter acesso às consultas.
Você pode seguir https://twitter.com/igor_chubin para acompanhar a situação.
======================================================================================
""",
        'pt-br': u"""
Desculpe-nos, atingimos o limite de consultas ao serviço de previsão do tempo neste momento.
Veja a seguir a previsão do tempo para uma cidade padrão (apenas para você ver que aspecto o relatório tem).
Em breve voltaremos a ter acesso às consultas.
Você pode seguir https://twitter.com/igor_chubin para acompanhar a situação.
======================================================================================
""",
        'ro': u"""
Ne pare rău, momentan am epuizat cererile alocate de către serviciul de prognoză meteo.
Vă arătăm prognoza meteo pentru localitatea implicită (ca exemplu, să vedeți cum arată).
Vom obține alocarea de cereri noi cât de curând posibil.
Puteți urmări https://twitter.com/igor_chubin pentru actualizări.
======================================================================================
""",
        'te': u"""
క్షమించండి, ప్రస్తుతానికి మేము వాతావరణ సేవకు ప్రశ్నలను గడుపుతున్నాం.
ఇక్కడ డిఫాల్ట్ నగరం కోసం వాతావరణ నివేదిక (కేవలం మీకు చూపించడానికి, ఇది ఎలా కనిపిస్తుంది).
సాధ్యమైనంత త్వరలో కొత్త ప్రశ్నలను పొందుతారు.
నవీకరణల కోసం https://twitter.com/igor_chubin ను మీరు అనుసరించవచ్చు.
======================================================================================
""",
        'tr': u"""
Üzgünüz, an itibariyle hava durumu servisine yapabileceğimiz sorgu limitine ulaştık.
Varsayılan şehir için hava durumu bilgisini görüyorsunuz (neye benzediğini gösterebilmek için).
Mümkün olan en kısa sürede servise yeniden sorgu yapmaya başlayacağız.
Gelişmeler için https://twitter.com/igor_chubin adresini takip edebilirsiniz.
======================================================================================
""",
        'th': u"""
ขออภัย การเรียกค้นหาระบบสภาพอากาศของเราหมดชั่วคราว
เราจึงแสดงข้อมูลของเมืองตัวอย่าง (เพื่อที่จะแสดงให้คุณเห็นว่าหน้าตาเป็นยังไง)
เราจะเรียกค้นหาใหม่โดยเร็วที่สุด
คุณสามารถติดตามการอัพเดทได้ที่ https://twitter.com/igor_chubin
======================================================================================
""",
        'da': u"""
Beklager, men vi er ved at løbe tør for forespørgsler til vejr-servicen lige nu.
Her er vejr rapporten for standard byen (bare så du ved hvordan det kan se ud).
Vi får nye forespørsler hurtigst muligt.
Du kan følge https://twitter.com/igor_chubin for at få opdateringer.
======================================================================================
""",
        'et': u"""
Vabandage, kuid hetkel on päringud ilmateenusele piiratud.
Selle asemel kuvame hetkel näidislinna ilmaprognoosi (näitamaks, kuidas see välja näeb).
Üritame probleemi lahendada niipea kui võimalik.
Jälgige https://twitter.com/igor_chubin värskenduste jaoks.
======================================================================================
""",
        'uk': u"""
Вибачте, ми перевищили максимальну кількість запитів до сервісу погоди.
Ось прогноз погоди у нашому місті (просто показати Вам як це виглядає).
Ми відновимо роботу як тільки зможемо.
Ви можете підписатися на https://twitter.com/igor_chubin для отримання новин.
======================================================================================
""",
        'vi': u"""
Xin lỗi, hiện tại chúng tôi đã hết lượt yêu cầu thông tin thời tiết.
Đây là dự báo thời tiết cho thành phố mặc định (chỉ để cho bạn thấy nó trông như thế nào).
Chung tôi sẽ có thêm lượt truy vấn sớm nhất có thể
Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật thông tin mới nhất.
======================================================================================
""",
        'zh-tw': u"""
抱歉，目前天氣服務的查詢請求太多了。
這裡是預設城市的天氣報告（只是為您展示它的外觀）。
我們將盡快取得新的資料。
您可以追蹤 https://twitter.com/igor_chubin 以取得更新。
======================================================================================
""",
        'mg': u"""
Miala tsiny fa misedra olana ny sampan-draharaha momba ny toetrandro amin'izao fotoana izao.
Ity ny tatitra momba ny toetr'andro ho an'ny tanàna mahazatra (mba hampisehoana anao ny endriny).
Haivaly aminao haingana ny fangatahanao.
Azonao atao ny manaraka ny pejy https://twitter.com/igor_chubin.
""",
        'ta': u"""
மன்னிக்கவும், தற்போது வானிலை சேவைக்கான வினவல்கள் எங்களிடம் இல்லை.
இயல்புநிலை நகரத்திற்கான வானிலை அறிக்கை இதோ (அது எப்படி இருக்கும் என்பதை உங்களுக்குக் காண்பிப்பதற்காக).
கூடிய விரைவில் புதிய வினவல்களைப் பெறுவோம்.
புதுப்பிப்புகளுக்கு நீங்கள் https://twitter.com/igor_chubin ஐப் பின்தொடரலாம்.
""",
    },

    # Historical messages:
    #     'Check new Feature: \033[92mwttr.in/Moon\033[0m or \033[92mwttr.in/Moon@2016-Mar-23\033[0m to see the phase of the Moon'
    #     'New feature: \033[92mwttr.in/Rome?lang=it\033[0m or \033[92mcurl -H "Accept-Language: it" wttr.in/Rome\033[0m for the localized version. Your lang instead of "it"'

    'NEW_FEATURE': {
        'en': u'New feature: multilingual location names \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) and location search \033[92mwttr.in/~Kilimanjaro\033[0m (just add ~ before)',
        'am': u'አዲስ ባህሪ: የአካባቢ ስሞች በተለያዩ ቋንቋዎች \033[92mwttr.in/ኢትዮጵያ\033[0m (በ UTF-8) እና የአካባቢ ፍለጋ \033[92mwttr.in/~Kilimanjaro\033[0m (ከአካባቢ ስም በፊት ~ ያክሉ)',
        'ar': u'ميزة جديدة : أسماء الأماكن بلغات متعددة \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) والبحث عن الأماكن \033[92mwttr.in/~Kilimanjaro\033[0m (فقط أضف ~ قبل)',
        'af': u'Nuwe eienskap: veeltalige name vir liggings \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) en ligging soek \033[92mwttr.in/~Kilimanjaro\033[0m (plaas net ~ vooraan)',
        'be': u'Новыя магчымасці: назвы месц на любой мове \033[92mwttr.in/станция+Восток\033[0m (в UTF-8) i пошук месц \033[92mwttr.in/~Kilimanjaro\033[0m (трэба дадаць ~ ў пачатак)',
        'bg': u'Нова функционалност: многоезични имена на места\033[92mwttr.in/станция+Восток\033[0m (в UTF-8) и в търсенето \033[92mwttr.in/~Kilimanjaro\033[0m (добавете ~ преди)',
        'bn': u'নতুন ফিচার : বহুভাষিক অবস্থানের নাম \ 033 [92mwttr.in/станция+Восток\033 [0m (UTF-8)] এবং অবস্থান অনুসন্ধান \ 033 [92mwttr.in/~Kilimanjaro\033 [0m (শুধু আগে ~ যোগ করুন)',
        'bs': u'XXXXXXXXXXXXXXXXXXXX: XXXXXXXXXXXXXXXXXXXXXXXXXXXXX\033[92mwttr.in/станция+Восток\033[0m (XX UTF-8) XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
        'ca': u'Noves funcionalitats: noms d\'ubicació multilingües \033[92mwttr.in/станция+Восток\033[0m (en UTF-8) i la ubicació de recerca \033[92mwttr.in/~Kilimanjaro\033[0m (només cal afegir ~ abans)',
        'es': u'Nuevas funcionalidades: los nombres de las ubicaciones en varios idiomas \033[92mwttr.in/станция+Восток\033[0m (em UTF-8) y la búsqueda por ubicaciones \033[92mwttr.in/~Kilimanjaro\033[0m (tan solo inserte ~ al principio)',
        'fa': u'قابلیت جدید: پشتیبانی از نام چند زبانه مکانها \033[92mwttr.in/станция+Восток\033[0m (در فرمت UTF-8) و جسجتوی مکان ها \033[92mwttr.in/~Kilimanjaro\033[0m (فقط قبل از اون ~ اضافه کنید)',
        'fr': u'Nouvelles fonctionnalités: noms d\'emplacements multilingues \033[92mwttr.in/станция+Восток\033[0m (en UTF-8) et recherche d\'emplacement \033[92mwttr.in/~Kilimanjaro\033[0m (ajouter ~ devant)',
        'gl': u'Nova funcionalidade: nomes de localizacións en varios idiomas\033[92mwttr.in/станция+Восток\033[0m (en UTF-8) e procuras de localizacións \033[92mwttr.in/~Kilimanjaro\033[0m (engade ~ antes)',
        'mk': u'Нова функција: повеќе јазично локациски имиња \033[92mwttr.in/станция+Восток\033[0m (во UTF-8) и локациско пребарување \033[92mwttr.in/~Kilimanjaro\033[0m (just add ~ before)',
        'nb': u'Ny funksjon: flerspråklige stedsnavn \033[92mwttr.in/станция+Восток\033[0m (i UTF-8) og lokasjonssøk \033[92mwttr.in/~Kilimanjaro\033[0m (bare legg til ~ foran)',
        'nl': u'Nieuwe functie: tweetalige locatie namen \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) en locatie zoeken \033[92mwttr.in/~Kilimanjaro\033[0m (zet er gewoon een ~ voor)',
        'fy': u'Nije funksje: twatalige lokaasje nammen \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) en lokaasje sykjen \033[92mwttr.in/~Kilimanjaro\033[0m (set er gewoan in ~ foar)',
        'cy': u'Nodwedd newydd: enwau lleoliadau amlieithog \033[92mwttr.in/станция+Восток\033[0m (yn UTF-8) a chwilio am leoliad \033[92mwttr.in/~Kilimanjaro\033[0m (ychwanegwch ~ yn gyntaf)',
        'de': u'Neue Funktion: mehrsprachige Ortsnamen \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) und Ortssuche \033[92mwttr.in/~Kilimanjaro\033[0m (fügen Sie ein ~ vor dem Ort ein)',
        'hi': u'नई सुविधा: बहुभाषी स्थान के नाम \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) और स्थान खोज \033[92mwttr.in/~Kilimanjaro\033[0m (बस ~ आगे लगाये)',
        'hu': u'Új funkcinalitás: többnyelvű helynevek \033[92mwttr.in/станция+Восток\033[0m (UTF-8-ban) és pozíció keresés \033[92mwttr.in/~Kilimanjaro\033[0m (csak adj egy ~ jelet elé)',
        'hy': u'Փորձարկեք: տեղամասերի անունները կամայական լեզվով \033[92mwttr.in/Դիլիջան\033[0m (в UTF-8) և տեղանքի որոնում \033[92mwttr.in/~Kilimanjaro\033[0m (հարկավոր է ~ ավելացնել դիմացից)',
        'ia': u'Nove functione: location nomine multilingue \033[92mwttr.in/станция+Восток\033[0m (a UTF-8) e recerca de location\033[92mwttr.in/~Kilimanjaro\033[0m (solo adde ~ ante)',
        'id': u'Fitur baru: nama lokasi dalam multibahasa \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) dan pencarian lokasi \033[92mwttr.in/~Kilimanjaro\033[0m (hanya tambah tanda ~ sebelumnya)',
        'it': u'Nuove funzionalità: nomi delle località multilingue \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) e ricerca della località \033[92mwttr.in/~Kilimanjaro\033[0m (basta premettere ~)',
        'ko': u'새로운 기능: 다국어로 대응된 위치 \033[92mwttr.in/서울\033[0m (UTF-8에서) 장소 검색 \033[92mwttr.in/~Kilimanjaro\033[0m (앞에 ~를 붙이세요)',
        'kk': u'',
        'lt': u'Naujiena: daugiakalbiai vietovių pavadinimai \033[92mwttr.in/станция+Восток\033[0m (UTF-8) ir vietovių paieška \033[92mwttr.in/~Kilimanjaro\033[0m (tiesiog priekyje pridėkite ~)',
        'lv': u'Jaunums: Daudzvalodu atrašanās vietu nosaukumi \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) un dabas objektu meklēšana \033[92mwttr.in/~Kilimanjaro\033[0m (tikai priekšā pievieno ~)',
        'mk': u'Нова функција: повеќе јазично локациски имиња \033[92mwttr.in/станция+Восток\033[0m (во UTF-8) и локациско пребарување \033[92mwttr.in/~Kilimanjaro\033[0m (just add ~ before)',
        'mr': u'नवीन वैशिष्ट्य: स्थळांची बहुभाषिक नावे \033[92mwttr.in/станция+Восток\033[0m (UTF-8 मध्ये) आणि स्थळ शोध \033[92mwttr.in/~Kilimanjaro\033[0m (फक्त आधी ~ जोडा)',
        'oc': u'Novèla foncionalitat : nom de lòc multilenga \033[92mwttr.in/станция+Восток\033[0m (en UTF-8) e recèrca de lòc \033[92mwttr.in/~Kilimanjaro\033[0m (solament ajustatz ~ abans)',        
        'pl': u'Nowa funkcjonalność: wielojęzyczne nazwy lokalizacji \033[92mwttr.in/станция+Восток\033[0m (w UTF-8) i szukanie lokalizacji \033[92mwttr.in/~Kilimanjaro\033[0m (poprzedź zapytanie ~ - znakiem tyldy)',
        'pt': u'Nova funcionalidade: nomes de localidades em várias línguas \033[92mwttr.in/станция+Восток\033[0m (em UTF-8) e procura por localidades \033[92mwttr.in/~Kilimanjaro\033[0m (é só colocar ~ antes)',
        'pt-br': u'Nova funcionalidade: nomes de localidades em várias línguas \033[92mwttr.in/станция+Восток\033[0m (em UTF-8) e procura por localidades \033[92mwttr.in/~Kilimanjaro\033[0m (é só colocar ~ antes)',
        'ro': u'Funcționalitate nouă: nume de localități multilingve \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) și căutare de localități \033[92mwttr.in/~Kilimanjaro\033[0m (adăuați ~ în față)',
        'ru': u'Попробуйте: названия мест на любом языке \033[92mwttr.in/станция+Восток\033[0m (в UTF-8) и поиск мест \033[92mwttr.in/~Kilimanjaro\033[0m (нужно добавить ~ спереди)',
        'zh': u'新功能：多语言地点名称 \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) 及地点搜索\033[92mwttr.in/~Kilimanjaro\033[0m （只需在名称前加~）',
        'tr': u'Yeni özellik: çok dilli konum isimleri \033[92mwttr.in/станция+Восток\033[0m (UTF-8 ile) ve konum arama \033[92mwttr.in/~Kilimanjaro\033[0m (sadece önüne ~ ekleyin)',
        'te': u'క్రొత్త లక్షణం: బహుభాషా స్థాన పేర్లు \ 033 [92mwttr.in/stancelя+Vostок\033 [0 U (UTF-8 లో) మరియు స్థానం శోధన \ 033 [92mwttr.in/~kilimanjaro\033 [0m (కేవలం ~ ముందుకి జోడించండి)',
        'th': u'ฟีเจอร์ใหม่: แสดงที่ตั้งได้หลายภาษา \033[92mwttr.in/станция+Восток\033[0m (ใน UTF-8) และการค้นหาที่ตั้ง \033[92mwttr.in/~Kilimanjaro\033[0m (เพียงเพิ่ม ~ ข้างหน้า)',
        'da': u'Ny funktion: flersprogede lokationsnavne \033[92mwttr.in/станция+Восток\033[0m (som UTF-8) og lokations søgning \033[92mwttr.in/~Kilimanjaro\033[0m (bare tilføj ~ inden)',
        'et': u'Uus funktsioon: mitmekeelsed asukohanimed \033[92mwttr.in/станция+Восток\033[0m (UTF-8 vormingus) ja asukoha otsing \033[92mwttr.in/~Kilimanjaro\033[0m (lisa ~ enne)',
        'uk': u'Спробуйте: назви місць будь-якою мовою \033[92mwttr.in/станція+Восток\033[0m (в UTF-8) та пошук місць \033[92mwttr.in/~Kilimanjaro\033[0m (потрібно додати ~ спочатку)',
        'vi': u'Chức năng mới: tên địa điểm đa ngôn ngữ \033[92mwttr.in/станция+Восток\033[0m (dùng UTF-8) và tìm kiếm địa điểm \033[92mwttr.in/~Kilimanjaro\033[0m (chỉ cần thêm ~ phía trước)',
        'zh-tw': u'新功能：多語言地點名稱 \033[92mwttr.in/станция+Восток\033[0m （使用 UTF-8 編碼）與位置搜尋 \033[92mwttr.in/~Kilimanjaro\033[0m （只要在地點前加 ~ 就可以了）',
        'mg': u'Fanatsrana vaovao: anarana toerana amin\'ny fiteny maro\033[92mwttr.in/станция+Восток\033[0m (en UTF-8) sy fitadiavana toerana \033[92mwttr.in/~Kilimanjaro\033[0m (ampio ~ fotsiny eo aloha)',
        'ta': u'புதிய அம்சம்: பன்மொழி இருப்பிடப் பெயர்கள் \033[92mwttr.in/станция+Восток\033[0m (UTF-8 இல்) மற்றும் இருப்பிடத் தேடல் \033[92mwttr.in/~Kilimanjaro\033[0m (முன் ~ஐச் சேர்க்கவும்)',
    },

    'FOLLOW_ME': {
        'en': u'Follow \033[46m\033[30m@igor_chubin\033[0m for wttr.in updates',
        'ar': u'لآخر المستجدات تابع \033[46m\033[30m@igor_chubin\033[0m',
        'af': u'Volg \033[46m\033[30m@igor_chubin\033[0m vir wttr.in opdaterings',
        'am': u'ለተጨማሪ wttr.in ዜና እና መረጃ \033[46m\033[30m@igor_chubin\033[0m ን ይከተሉ',
        'be': u'Сачыце за \033[46m\033[30m@igor_chubin\033[0m за навінамі wttr.in',
        'bg': u'Последвай \033[46m\033[30m@igor_chubin\033[0m за обновления свързани с wttr.in',
        'bn': u'wttr.in আপডেটের জন্য \033[46m\033[30m@igor_chubin\033[0m কে অনুসরণ করুন',
        'bs': u'XXXXXX \033[46m\033[30m@igor_chubin\033[0m XXXXXXXXXXXXXXXXXXX',
        'ca': u'Segueix \033[46m\033[30m@igor_chubin\033[0m per actualitzacions de wttr.in',
        'es': u'Sigue a \033[46m\033[30m@igor_chubin\033[0m para enterarte de las novedades de wttr.in',
        'eu': u'\033[46m\033[30m@igor_chubin\033[0m jarraitu wttr.in berriak jasotzeko',
        'cy': u'Dilyner \033[46m\033[30m@igor_Chubin\033[0m am diweddariadau wttr.in',
        'fa': u'برای دنبال کردن خبرهای wttr.in شناسه \033[46m\033[30m@igor_chubin\033[0m رو فالو کنید.',
        'fr': u'Suivez \033[46m\033[30m@igor_Chubin\033[0m pour rester informé sur wttr.in',
        'de': u'Folgen Sie \033[46m\033[30mhttps://twitter.com/igor_chubin\033[0m für wttr.in Updates',
        'ga': u'Lean \033[46m\033[30m@igor_chubin\033[0m don wttr.in eolas is deanaí',
        'gl': u'Segue a \033[46m\033[30m@igor_chubin\033[0m para actualizacións sobre wttr.in',
        'hi': u'अपडेट के लिए फॉलो करें \033[46m\033[30m@igor_chubin\033[0m',
        'hu': u'Kövesd \033[46m\033[30m@igor_chubin\033[0m-t további wttr.in információkért',
        'hy': u'Նոր ֆիչռների համար հետևեք՝ \033[46m\033[30m@igor_chubin\033[0m',
        'ia': u'Seque \033[46m\033[30m@igor_chubin\033[0m por nove information de wttr.in',
        'id': u'Ikuti \033[46m\033[30m@igor_chubin\033[0m untuk informasi wttr.in terbaru',
        'it': u'Seguite \033[46m\033[30m@igor_chubin\033[0m per aggiornamenti a wttr.in',
        'ko': u'wttr.in의 업데이트 소식을 원하신다면 \033[46m\033[30m@igor_chubin\033[0m 을 팔로우 해주세요',
        'kk': u'',
        'lt': u'wttr.in atnaujinimus sekite \033[46m\033[30m@igor_chubin\033[0m',
        'lv': u'Seko \033[46m\033[30m@igor_chubin\033[0m , lai uzzinātu wttr.in jaunumus',
        'mk': u'Следете \033[46m\033[30m@igor_chubin\033[0m за wttr.in новости',
        'mr': u'wttr.in च्या अद्यावत माहितीसाठी  \033[46m\033[30m@igor_chubin\033[0m चे अनुसरण करा',
        'nb': u'Følg \033[46m\033[30m@igor_chubin\033[0m for wttr.in oppdateringer',
        'nl': u'Volg \033[46m\033[30m@igor_chubin\033[0m voor wttr.in updates',
        'oc': u'Seguissètz \033[46m\033[30m@igor_Chubin\033[0m per demorar informat sus wttr.in',        
        'fy': u'Folgje \033[46m\033[30m@igor_chubin\033[0m foar wttr.in updates',
        'pl': u'Śledź \033[46m\033[30m@igor_chubin\033[0m aby być na bieżąco z nowościami dotyczącymi wttr.in',
        'pt': u'Seguir \033[46m\033[30m@igor_chubin\033[0m para as novidades de wttr.in',
        'pt-br': u'Seguir \033[46m\033[30m@igor_chubin\033[0m para as novidades de wttr.in',
        'ro': u'Urmăriți \033[46m\033[30m@igor_chubin\033[0m pentru actualizări despre wttr.in',
        'ru': u'Все новые фичи публикуются здесь: \033[46m\033[30m@igor_chubin\033[0m',
        'zh': u'关注 \033[46m\033[30m@igor_chubin\033[0m 获取 wttr.in 动态',
        'te': u'అనుసరించండి \ 033 [46m \ 033 [30m @ igor_chubin \ 033 [wttr.in నవీకరణలను కోసం',
        'tr': u'wttr.in ile ilgili gelişmeler için \033[46m\033[30m@igor_chubin\033[0m adresini takip edin',
        'th': u'ติดตาม \033[46m\033[30m@igor_chubin\033[0m สำหรับการอัพเดท wttr.in',
        'da': u'Følg \033[46m\033[30m@igor_chubin\033[0m for at få wttr.in opdateringer',
        'et': u'Jälgi \033[46m\033[30m@igor_chubin\033[0m wttr.in uudiste tarbeks',
        'uk': u'Нові можливості wttr.in публікуються тут: \033[46m\033[30m@igor_chubin\033[0m',
        'vi': u'Theo dõi \033[46m\033[30m@igor_chubin\033[0m để cập nhật thông tin về wttr.in',
        'zh-tw': u'追蹤 \033[46m\033[30m@igor_chubin\033[0m 以取得更多 wttr.in 的動態',
        'mg': u'Araho ao ny pejy \033[46m\033[30m@igor_Chubin\033[0m raha toa ka te hahazo vaovao momban\'ny wttr.in',
        'ta': u'wttr.in புதுப்பிப்புகளுக்கு \033[46m\033[30m@igor_chubin\033[0m ஐப் பின்தொடரவும்',
    },
}
CAPTION = {
    "af": u"Weer verslag vir:",
    "am": u"የአየር ሁኔታ ሪፖርት ለ",
    "ar": u"تقرير حالة الطقس",
    "az": u"Hava proqnozu:",
    "be": u"Прагноз надвор'я для:",
    "bg": u"Прогноза за времето в:",
    "bn": u"আবহাওয়ার প্রতিবেদন:",
    "bs": u"Vremenske prognoze za:",
    "ca": u"Informe del temps per a:",
    "cs": u"Předpověď počasí pro:",
    "cy": u"Adroddiad tywydd ar gyfer:",
    "da": u"Vejret i:",
    "de": u"Wetterbericht für:",
    "el": u"Πρόγνωση καιρού για:",
    "en": u"Weather report for:",
    "eo": u"Veterprognozo por:",
    "es": u"Pronóstico del tiempo en:",
    "et": u"Ilmaprognoos:",
    "eu": u"Eguraldiaren iragarpena:",
    "fa": u"گزارش آب و هئا برای شما:",
    "fi": u"Säätiedotus:",
    "fr": u"Prévisions météo pour:",
    "fy": u"Waarberjocht foar:",
    "ga": u"Réamhaisnéis na haimsire do:",
    "he": u":ריוואה גזמ תיזחת",
    "hi": u"मौसम की जानकारी",
    "hr": u"Vremenska prognoza za:",
    "hu": u"Időjárás előrejelzés:",
    "hy": u"Եղանակի տեսություն:",
    "ia": u"Reporto tempore por:",
    "id": u"Prakiraan cuaca:",
    "it": u"Previsioni meteo:",
    "is": u"Veðurskýrsla fyrir:",
    "ja": u"天気予報：",
    "jv": u"Weather forecast for:",
    "ka": u"ამინდის პროგნოზი:",
    "kk": u"Ауа райы:",
    "ko": u"일기 예보：",
    "ky": u"Аба ырайы:",
    "lt": u"Orų prognozė:",
    "lv": u"Laika ziņas:",
    "mk": u"Прогноза за времето во:",
    "ml": u"കാലാവസ്ഥ റിപ്പോർട്ട്:",
    "mr": u"हवामान अहवाल:",
    "nb": u"Værmelding for:",
    "nl": u"Weerbericht voor:",
    "nn": u"Vêrmelding for:",
    "oc": u"Previsions metèo per :",    
    "pl": u"Pogoda w:",
    "pt": u"Previsão do tempo para:",
    "pt-br": u"Previsão do tempo para:",
    "ro": u"Prognoza meteo pentru:",
    "ru": u"Прогноз погоды:",
    "sk": u"Predpoveď počasia pre:",
    "sl": u"Vremenska napoved za",
    "sr": u"Временска прогноза за:",
    "sr-lat": u"Vremenska prognoza za:",
    "sv": u"Väderleksprognos för:",
    "sw": u"Ripoti ya hali ya hewa, jiji la:",
    "te": u"వాతావరణ సమాచారము:",
    "th": u"รายงานสภาพอากาศ:",
    "tr": u"Hava beklentisi:",
    "uk": u"Прогноз погоди для:",
    "uz": u"Ob-havo bashorati:",
    "vi": u"Báo cáo thời tiết:",
    "zh": u"天气预报：",
    "zu": u"Isimo sezulu:",
    "zh-tw": u"天氣報告：",
    "mg": u"Toetr\'andro any :",
    "ta": u"வானிலை அறிக்கை:",
}

def get_message(message_name, lang):
    if lang == 'zh-cn':
        lang = 'zh'
    if message_name not in MESSAGE:
        return ''
    message_dict = MESSAGE[message_name]
    return message_dict.get(lang, message_dict.get('en', ''))



File: lib/cache.py

"""
LRU-Cache implementation for formatted (`format=`) answers
"""

import datetime
import re
import time
import os
import hashlib
import random

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")

    if isinstance(answer, str) and "%{{NOW(" in answer:
        answer = re.sub(r"%{{NOW\(([^}]*)\)}}", lambda x: _now_in_tz(x.group(1)), answer)

    return answer

def get_signature(user_agent, query_string, client_ip_address, lang):
    """
    Get cache signature based on `user_agent`, `url_string`,
    `lang`, and `client_ip_address`
    Return `None` if query should not be cached.
    """

    if "?" in query_string:
        location = query_string.split("?", 1)[0]
    else:
        location = query_string
    if location.startswith("http://"):
        location = location[7:]
    elif location.startswith("https://"):
        location = location[8:]
    if ":" in location:
        return None

    signature = "%s:%s:%s:%s" % \
        (user_agent, query_string, client_ip_address, lang)
    print(signature)
    return signature

def get(signature):
    """
    If `update_answer` is not True, return answer as it is
    stored in the cache. Otherwise update it, using
    the `_update_answer` function.
    """

    if not signature:
        return None

    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:
                return None
        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 not signature:
        return _update_answer(value)

    if len(value) >= MIN_SIZE_FOR_FILECACHE:
        value_to_store = _store_in_file(signature, value)
    else:
        value_to_store = value

    value_record = {
        "val": value_to_store,
        "expiry": time.time() + _randint(1000, 2000),
        }

    CACHE[signature] = value_record

    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.
    `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)

    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

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.

    `sigash` can be prefixed with "file:" (for text files)
    or "bfile:" (for binary files).
    """

    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, mode) as f_cache:
        return f_cache.read()



File: lib/extract_emoji.py

#!/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:<span font=\"%s\" size=\"20000\">%s</span>" % (emoji_font, emoji),
            filename
        ]
        subprocess.Popen(convert_string)

if __name__ == '__main__':
    extract_emojis_to_directory("share/emoji")



File: lib/location.py

"""
All location related functions and converters.

The main entry point is `location_processing` which gets `location` and
`source_ip_address` and basing on this information generates precise location
description.

     [query]      -->   [location]      -->  [(lat,long)] -->
                            ^
                            |
     [ip-address] --> _get_location()

To resolve IP address into location, the module uses function `_get_location()`,
which subsequenlty utilizes one of the three methods:

* `_geoip2()` (local sqlite database);
* `_ip2location()` (an external paid service);
* `_ipinfo()` (an external free service).

IP-address resolution data is saved in cache (_ipcachewrite, _ipcache).
Cache entry format:

    COUNTRY_CODE;COUNTRY;REGION;CITY[;REST]

To resolve location name into a (lat,long) pair,
an external service is used, which is wrapped
with a function `_geolocator()`.

Exports:

    location_processing
    is_location_blocked
"""


from __future__ import print_function

import datetime
import json
import os
import socket
import sys
import random

import geoip2.database
import pycountry
import requests

from globals import GEOLITE, GEOLOCATOR_SERVICE, IP2LCACHE, IP2LOCATION_KEY, NOT_FOUND_LOCATION, \
                    ALIASES, BLACKLIST, IATA_CODES_FILE, IPLOCATION_ORDER, IPINFO_TOKEN

GEOIP_READER = geoip2.database.Reader(GEOLITE)
COUNTRY_MAP = {"Russian Federation": "Russia"}


def _debug_log(s):
    if os.environ.get("WTTR_DEBUG_LOCATION"):
        with open("/tmp/location-debug.log", "a") as f:
            f.write("%s %s\n" % (datetime.datetime.now(),s))


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)
        return True
    except socket.error:
        try:
            socket.inet_pton(socket.AF_INET6, ip_addr)
            return True
        except socket.error:
            return False


def _location_normalize(location):
    """
    Normalize location name `location`
    """
    #translation_table = dict.fromkeys(map(ord, '!@#$*;'), None)
    def _remove_chars(chars, string):
        return ''.join(x for x in string if x not in chars)

    location = location.lower().replace('_', ' ').replace('+', ' ').strip()
    if not location.startswith('moon@'):
        location = _remove_chars(r'!@#$*;:\\', location)
    return location


def _geolocator(location):
    """
    Return a GPS pair for specified `location` or None
    if nothing can be found
    """

    try:
        if random.random() < 0:
          geo = requests.get('%s/%s' % (GEOLOCATOR_SERVICE, location)).text
        else:
          geo = requests.get("http://127.0.0.1:8085/:geo-location?location=%s" % location).text
    except requests.exceptions.ConnectionError as exception:
        print("ERROR: %s" % exception)
        return None

    if geo == "":
        return None

    try:
        answer = json.loads(geo.encode('utf-8'))
        if "error" in answer:
            return None
        return answer
    except ValueError as exception:
        print("ERROR: %s" % exception)
        return None

    return None


def _ipcachewrite(ip_addr, location):
    """ Write a retrieved ip+location into cache
        Can stress some filesystems after long term use, see
        https://stackoverflow.com/questions/466521/how-many-files-can-i-put-in-a-directory

        Expects a location of the form:
            `(city, region, country, country_code, <lat>, <long>)`
        Writes a cache entry of the form:
            `country_code;country;region;city;<lat>;<long>`

        The latitude and longitude are optional elements.
    """
    return
    cachefile = os.path.join(IP2LCACHE, ip_addr)
    if not os.path.exists(IP2LCACHE):
        os.makedirs(IP2LCACHE)
    with open(cachefile, 'w') as file:
        # like ip2location format
        file.write(location[3] + ';' + location[2] + ';' + location[1] + ';' + location[0])
        if len(location) > 4:
            file.write(';' + ';'.join(map(str, location[4:])))


def _ipcache(ip_addr):
    """ Retrieve a location from cache by ip addr
        Returns a triple of (CITY, REGION, COUNTRY) or None
        TODO: When cache becomes more robust, transition to using latlong
    """

    ## Use Geo IP service when available
    r = requests.get("http://127.0.0.1:8085/:geo-ip-get?ip=%s" % ip_addr)
    if r.status_code == 200 and ";" in r.text:
        _, country, region, city, *_ = r.text.split(';')
        return city, region, country

    return None

    # cachefile = os.path.join(IP2LCACHE, ip_addr)
    #
    # if os.path.exists(cachefile):
    #     try:
    #         _, country, region, city, *_ = open(cachefile, 'r').read().split(';')
    #         return city, region, country
    #     except ValueError:
    #         # cache entry is malformed: should be
    #         # [ccode];country;region;city;[lat];[long];...
    #         return None
    # else:
    #     _debug_log("[_ipcache] %s not found" % ip_addr)
    # return None


def _ip2location(ip_addr):
    """ Convert IP address `ip_addr` to a location name using ip2location.
        Return list of location data fields:

            [ccode, country, region, city, rest...]

        Return `None` if an error occured.
    """

    # if IP2LOCATION_KEY is not set, do not query,
    # because the query wont be processed anyway
    if not IP2LOCATION_KEY:
        return None
    try:
        _debug_log("[_ip2location] %s search" % ip_addr)
        r = requests.get(
            'http://api.ip2location.com/?ip=%s&key=%s&package=WS3'  # WS5 provides latlong
            % (ip_addr, IP2LOCATION_KEY))
        r.raise_for_status()
        location = r.text

        parts = location.split(';')
        if len(parts) >= 4:
            #       ccode,    country,  region,   city,       (rest)
            _debug_log("[_ip2location] %s found" % ip_addr)
            return [parts[3], parts[2], parts[1], parts[0]] + parts[4:]
        return None
    except requests.exceptions.RequestException:
        return None


def _ipinfo(ip_addr):
    if not IPINFO_TOKEN:
        return None
    try:
        r = requests.get(
            'https://ipinfo.io/%s/json?token=%s'
            % (ip_addr, IPINFO_TOKEN))
        r.raise_for_status()
        r_json = r.json()
        # can't do two unpackings on one line
        city, region, country, ccode  = r_json["city"], r_json["region"], '', r_json["country"],
        lat, long = r_json["loc"].split(',')
        # NOTE: ipinfo only provides ISO codes for countries
        country = pycountry.countries.get(alpha_2=ccode).name
    except (requests.exceptions.RequestException, ValueError):
        # latter is thrown by failure to parse json in reponse
        return None
    return [city, region, country, ccode, lat, long]


def _geoip(ip_addr):
    try:
        _debug_log("[_geoip] %s search" % ip_addr)
        response = GEOIP_READER.city(ip_addr)
        # print(response.subdivisions)
        city, region, country, ccode, lat, long = response.city.name, response.subdivisions[0].names["en"], response.country.name, response.country.iso_code, response.location.latitude, response.location.longitude
        _debug_log("[_geoip] %s found" % ip_addr)
    except IndexError:
        # Tuple error
        try:
            city, region, country, ccode, lat, long = response.city.name, None, response.country.name, response.country.iso_code, response.location.latitude, response.location.longitude
        except IndexError:
            return None
    except (geoip2.errors.AddressNotFoundError):
        return None
    return [city, region, country, ccode, lat, long]


def _country_name_workaround(country):
    # workaround for strange bug with the country name
    # maybe some other countries has this problem too
    country = COUNTRY_MAP.get(country) or country
    return country


def _get_location(ip_addr):
    """
    Return location triple (CITY, REGION, COUNTRY) for `ip_addr`
    """
    location = _ipcache(ip_addr)
    if location:
        return location

    # location from iplocators have the following order:
    # (CITY, REGION, COUNTRY, CCODE, LAT, LONG)
    for method in IPLOCATION_ORDER:
        if method == 'geoip':
            location = _geoip(ip_addr)
        elif method == 'ip2location':
            location = _ip2location(ip_addr)
        elif method == 'ipinfo':
            location = _ipinfo(ip_addr)
        else:
            print("ERROR: invalid iplocation method specified: %s" % method)
        if location is not None:
            break

    if location is not None and all(location):
        _ipcachewrite(ip_addr, location)
        # cache write used to happen before workaround, preserve that
        location[2] = _country_name_workaround(location[2])
        return location[:3]  # city, region, country
        # ccode is cached but not needed for location

    # temporary disabled it because of geoip services capcacity
    #
    #if city is None and response.location:
    #    coord = "%s, %s" % (response.location.latitude, response.location.longitude)
    #    try:
    #        location = geolocator.reverse(coord, language='en')
    #        city = location.raw.get('address', {}).get('city')
    #    except:
    #        pass

    # No methods resulted in a location - return default
    return NOT_FOUND_LOCATION, None, None


def _location_canonical_name(location):
    "Find canonical name for `location`"

    location = _location_normalize(location)
    if location.lower() in LOCATION_ALIAS:
        return LOCATION_ALIAS[location.lower()]
    return location


def _load_aliases(aliases_filename):
    """
    Load aliases from the aliases file
    """
    aliases_db = {}
    with open(aliases_filename, 'r') as f_aliases:
        for line in f_aliases.readlines():
            try:
                from_, to_ = line.decode('utf-8').split(':', 1)
            except AttributeError:
                from_, to_ = line.split(':', 1)

            aliases_db[_location_normalize(from_)] = _location_normalize(to_)
    return aliases_db


def _load_iata_codes(iata_codes_filename):
    """
    Load IATA codes from the IATA codes file
    """
    with open(iata_codes_filename, 'r') as f_iata_codes:
        result = []
        for line in f_iata_codes.readlines():
            result.append(line.strip())
    return set(result)


LOCATION_ALIAS = _load_aliases(ALIASES)
LOCATION_BLACK_LIST = [x.strip() for x in open(BLACKLIST, 'r').readlines()]
IATA_CODES = _load_iata_codes(IATA_CODES_FILE)


def is_location_blocked(location):
    """
    Return True if this location is blocked
    or False if it is allowed
    """
    return location is not None and location.lower() in LOCATION_BLACK_LIST


def _get_hemisphere(location):
    """
    Return hemisphere of the location (True = North, False = South).
    Assume North and return True if location can't be found.
    """
    if all(location):
        location_string = ", ".join(location)
    else:
        return True

    geolocation = _geolocator(location_string)
    if geolocation is None:
        return True
    return float(geolocation["latitude"]) > 0


def _fully_qualified_location(location, region, country):
    """ Return fully qualified location name with `region` and `country`,
        as a string.
    """

    # If country is not specified, location stays as is
    if not country:
        return location

    # Canonify/shorten country name
    if country == "United Kingdom of Great Britain and Northern Ireland":
        country = "United Kingdom"
    elif country == "Russian Federation":
        country = "Russia"
    elif country == "United States of America":
        country = "United States"

    # In United States region is important, because there are a lot of
    # locations with the same name in different regions.
    # In the rest of the world, usage of region name may decrease chances
    # or correct name resolution, so for the moment `region` is used
    # only for the United States
    if country == "United States" and region:
        location += ", %s, %s" % (region, country)
    else:
        location += ", %s" % country
    return location


def location_processing(location, ip_addr):
    """
    """

    # if location is starting with ~
    # or has non ascii symbols
    # it should be handled like a search term (for geolocator)
    override_location_name = None
    full_address = None
    hide_full_address = False
    force_show_full_address = location is not None and location.startswith('~')

    # location ~ means that it should be detected automatically,
    # and shown in the location line below the report
    if location == '~':
        location = None

    if location and location.lstrip('~ ').startswith('@'):
        try:
            if (location.lstrip('~ ')[1:] == ""):
                location, region, country = NOT_FOUND_LOCATION, None, None

            else:
                location, region, country = _get_location(
                    socket.gethostbyname(
                        location.lstrip('~ ')[1:]))
                location = '~' + location
                location = _fully_qualified_location(location, region, country)
                hide_full_address = not force_show_full_address

        except:
            location, region, country = NOT_FOUND_LOCATION, None, None

    query_source_location = _get_location(ip_addr)

    # For moon queries, hemisphere must be found
    # True for North, False for South
    hemisphere = False
    if location is not None and (location.lower()+"@").startswith("moon@"):
        hemisphere = _get_hemisphere(query_source_location)

    country = None
    if not location or location == 'MyLocation':
        location = ip_addr

    if _is_ip(location):
        location, region, country = _get_location(location)
        # location is just city here

        # here too
        if location:
            location = '~' + location
            location = _fully_qualified_location(location, region, country)
            hide_full_address = not force_show_full_address

    if location and not location.startswith('~'):
        tmp_location = _location_canonical_name(location)
        if tmp_location != location:
            override_location_name = location
            location = tmp_location

    # up to this point it is possible that the name
    # contains some unicode symbols
    # here we resolve them
    if location is not None and location != NOT_FOUND_LOCATION:
        location = "~" + location.lstrip('~ ')
        if not override_location_name:
            override_location_name = location.lstrip('~')

    # if location is not None and location.upper() in IATA_CODES:
    #     location = '~%s' % location

    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:
                override_location_name = location[1:].replace('+', ' ')
            location = "%s,%s" % (geolocation['latitude'], geolocation['longitude'])
            country = None
            if not hide_full_address:
                full_address = geolocation['address']
            else:
                full_address = None
        else:
            location = NOT_FOUND_LOCATION #location[1:]

    return location, \
            override_location_name, \
            full_address, \
            country, \
            query_source_location, \
            hemisphere


def _main_():
    """ Validate cache entries. Print names of invalid cache entries
        and move it to the "broken-entries" directory."""

    import glob
    import shutil

    for filename in glob.glob(os.path.join(IP2LCACHE, "*")):
        ip_address = os.path.basename(filename)
        data = _ipcache(ip_address)
        if data:
            city, region, country = data
            if any(x in city for x in "0123456789"):
                print(city)
                shutil.move(filename, os.path.join("/wttr.in/cache/ip2l-broken-format", ip_address))

def _trace_ip():

    print(_geoip("108.5.186.108"))
    print(_get_location("108.5.186.108"))
    print(location_processing("", "108.5.186.108"))

if __name__ == "__main__":
    _trace_ip()
    #_main_()
    #print(_geoip("173.216.90.56"))



File: lib/proxy_log.py

"""
Logger of proxy queries

"""

# pylint: disable=consider-using-with,too-few-public-methods

import datetime

class Logger:

    """
    Generic logger.
    For specific loggers, _shorten_query() should be rewritten.
    """

    def __init__(self, filename_access, filename_errors):

        self._filename_access = filename_access
        self._filename_errors = filename_errors
        self._log_access = open(filename_access, "a", encoding="utf-8")
        self._log_errors = open(filename_errors, "a", encoding="utf-8")

    def _shorten_query(self, query):
        return query

    def log(self, query, error):
        """
        Log `query` and `error`
        """

        message = str(datetime.datetime.now())
        query = self._shorten_query(query)
        if error != "":
            message += " ERR " + query + " " + error
            self._log_errors.write(message+"\n")
        else:
            message +=  " OK  " + query
            self._log_access.write(message+"\n")


class LoggerWWO(Logger):
    """
    WWO logger.
    """

    def _shorten_query(self, query):
        return "".join([x for x in query.split("&") if x.startswith("q=")])



File: lib/buttons.py

TWITTER_BUTTON = """
<a href="https://twitter.com/igor_chubin?ref_src=twsrc%5Etfw" class="twitter-follow-button" data-show-count="false">Follow @igor_chubin</a><script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
"""

GITHUB_BUTTON = """
<a aria-label="Star chubin/wttr.in on GitHub" data-count-aria-label="# stargazers on GitHub" data-count-api="/repos/chubin/wttr.in#stargazers_count" data-count-href="/chubin/wttr.in/stargazers" data-show-count="true" data-icon="octicon-star" href="https://github.com/chubin/wttr.in" class="github-button">wttr.in</a>
"""

GITHUB_BUTTON_2 = """
<!-- Place this tag where you want the button to render. -->
<a aria-label="Star schachmat/wego on GitHub" data-count-aria-label="# stargazers on GitHub" data-count-api="/repos/schachmat/wego#stargazers_count" data-count-href="/schachmat/wego/stargazers" data-show-count="true" data-icon="octicon-star" href="https://github.com/schachmat/wego" class="github-button">wego</a>
"""

GITHUB_BUTTON_3 = """
<!-- Place this tag where you want the button to render. -->
<a aria-label="Star chubin/pyphoon on GitHub" data-count-aria-label="# stargazers on GitHub" data-count-api="/repos/chubin/pyphoon#stargazers_count" data-count-href="/chubin/pyphoon/stargazers" data-show-count="true" data-icon="octicon-star" href="https://github.com/chubin/pyphoon" class="github-button">pyphoon</a>
"""

GITHUB_BUTTON_FOOTER = """
<!-- Place this tag right after the last button or just before your close body tag. -->
<script async defer id="github-bjs" src="https://buttons.github.io/buttons.js"></script>
"""

def add_buttons(output):
    """
    Add buttons to html output
    """

    return output.replace('</body>',
                          (TWITTER_BUTTON
                           + GITHUB_BUTTON
                           + GITHUB_BUTTON_3
                           + GITHUB_BUTTON_2
                           + GITHUB_BUTTON_FOOTER) + '</body>')



File: lib/wttr_srv.py

#!/usr/bin/env python
# vim: set encoding=utf-8

"""
Main wttr.in rendering function implementation
"""

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

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, remove_ansi, TRANSLATION_TABLE, \
                    BASH_FUNCTION_FILE, TRANSLATION_FILE, LOG_FILE, \
                    NOT_FOUND_LOCATION, \
                    MALFORMED_RESPONSE_HTML_PAGE, \
                    PLAIN_TEXT_AGENTS, PLAIN_TEXT_PAGES, \
                    MY_EXTERNAL_IP, QUERY_LIMITS
from location import is_location_blocked, location_processing
from limits import Limits
from view.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.INFO, format='%(asctime)s %(message)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`
    """
    text = ""
    if name == ":help":
        text = open(get_help_file(lang), 'r').read()
        text = text.replace('FULL_TRANSLATION', ' '.join(FULL_TRANSLATION))
        text = text.replace('PARTIAL_TRANSLATION', ' '.join(PARTIAL_TRANSLATION))
    elif name == ":bash.function":
        text = open(BASH_FUNCTION_FILE, 'r').read()
    elif name == ":iterm2":
        text = open("share/iterm2.txt", 'r').read()
    elif name == ":translation":
        text = open(TRANSLATION_FILE, 'r').read()
        text = text\
                .replace('NUMBER_OF_LANGUAGES', str(len(SUPPORTED_LANGS)))\
                .replace('SUPPORTED_LANGUAGES', ' '.join(SUPPORTED_LANGS))
    return text

def _client_ip_address(request):
    """Return client ip address for flask `request`.
    """

    if request.headers.getlist("X-PNG-Query-For"):
        ip_addr = request.headers.getlist("X-PNG-Query-For")[0]
        if ip_addr.startswith('::ffff:'):
            ip_addr = ip_addr[7:]
    elif request.headers.getlist("X-Forwarded-For"):
        ip_addr = request.headers.getlist("X-Forwarded-For")[0]
        if ip_addr.startswith('::ffff:'):
            ip_addr = ip_addr[7:]
    else:
        ip_addr = request.remote_addr

    return ip_addr

def _parse_language_header(header):
    """
    >>> _parse_language_header("en-US,en;q=0.9")
    >>> _parse_language_header("en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7")
    >>> _parse_language_header("xx, fr-CA;q=0.8, da-DK;q=0.9")
    'da'
    """

    def _parse_accept_language(accept_language):
        languages = accept_language.split(",")
        locale_q_pairs = []

        for language in languages:
            try:
                if language.split(";")[0] == language:
                    # no q => q = 1
                    locale_q_pairs.append((language.strip(), 1))
                else:
                    locale = language.split(";")[0].strip()
                    weight = float(language.split(";")[1].split("=")[1])
                    locale_q_pairs.append((locale, weight))
            except (IndexError, ValueError):
                pass

        return locale_q_pairs

    def _find_supported_language(accepted_languages):

        def supported_langs():
            """Yields all pairs in the Accept-Language header
            supported in SUPPORTED_LANGS or None if 'en' is the preferred"""
            for lang_tuple in accepted_languages:
                lang = lang_tuple[0]
                if '-' in lang:
                    lang = lang.split('-', 1)[0]
                if lang in SUPPORTED_LANGS:
                    yield lang, lang_tuple[1]
                elif lang == 'en':
                    yield None, lang_tuple[1]
        try:
            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_view(request):
    """
    Return preferred answer language based on
    domain name, query arguments and headers
    """

    lang = None
    view_name = None
    hostname = request.headers['Host']
    if hostname != 'wttr.in' and hostname.endswith('.wttr.in'):
        lang = hostname[:-8]
        if lang.startswith("v2") or lang.startswith("v3"):
            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:
        lang = _parse_language_header(header_accept_language)

    return lang, view_name

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 ('view' in query
            and not query["view"].startswith("v2")
            and not query["view"].startswith("v3")) \
        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

def _cyclic_location_selection(locations, period):
    """Return one of `locations` (: separated list)
    basing on the current time and query interval `period`
    """

    locations = locations.split(':')
    max_len = max(len(x) for x in locations)
    locations = [x.rjust(max_len) for x in locations]

    try:
        period = int(period)
    except ValueError:
        period = 1

    index = int(time.time()/period) % len(locations)
    return locations[index]


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).
    """

    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
    loc = (parsed_query['orig_location'] or "").lower()
    if parsed_query.get("view"):
        if not parsed_query.get("location"):
            parsed_query["location"] = loc

        output = wttr_line(query, parsed_query)
    elif loc == 'moon' or loc.startswith('moon@'):
        output = get_moon(parsed_query)
    else:
        output = get_wetter(parsed_query)

    if parsed_query.get('png_filename'):
        if parsed_query.get("view") != "v3":
            # 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, cache._update_answer(output), options=parsed_query)
            output = result.get()
    else:
        if query.get('days', '3') != '0' \
            and not query.get('no-follow-line') \
            and ((parsed_query.get("view") or "v2")[:2] in ["v2", "v3"]):
            if parsed_query['html_output']:
                output = add_buttons(output)
            else:
                message = get_message('FOLLOW_ME', parsed_query['lang'])
                if parsed_query.get('no-terminal', False):
                    message = remove_ansi(message)
                if parsed_query.get('dumb', False):
                    message = message.translate(TRANSLATION_TABLE)
                output += '\n' + message + '\n'

    return cache.store(cache_signature, 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:

        `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

    Parameters priorities (from low to high):

        * HTTP-header
        * Domain name
        * URL
        * Filename

    Return: dictionary with parsed parameters
    """

    if location and 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
        location = location[:-4]
    if location and ':' in location and location[0] != ":":
        location = _cyclic_location_selection(location, query.get('period', 1))

    parsed_query = {
        'ip_addr': _client_ip_address(request),
        'user_agent': request.headers.get('User-Agent', '').lower(),
        'request_url': request.url,
        }

    if png_filename:
        parsed_query["png_filename"] = png_filename
        parsed_query.update(parse_query.parse_wttrin_png_name(png_filename))

    lang, _view = get_answer_language_and_view(request)

    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)

    parsed_query["html_output"] = get_output_format(query, parsed_query)
    parsed_query["json_output"] = (parsed_query.get("view", "") or "").startswith("j")

    if not fast_mode: # not png_filename and not fast_mode:
        location, override_location_name, full_address, country, query_source_location, hemisphere = \
                location_processing(parsed_query["location"], parsed_query["ip_addr"])

        us_ip = query_source_location[2] in ["United States", "United States of America"] \
                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,
            'hemisphere': hemisphere})

    parsed_query.update(query)
    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 HTML, ANSI or other format.
    """

    def _wrap_response(response_text, html_output, json_output, png_filename=None):
        if not isinstance(response_text, str) and \
           not isinstance(response_text, bytes):
            return response_text

        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)
            if html_output:
                response.mimetype = "text/html"
            elif json_output:
                response.mimetype = "application/json"
            else:
                response.mimetype = "text/plain"
        return response

    if is_location_blocked(location):
        return ("", 403) # Forbidden

    try:
        LIMITS.check_ip(_client_ip_address(request))
    except RuntimeError as exception:
        return (str(exception), 429) # Too many requests

    query = parse_query.parse_query(request.args)

    # 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)

    http_code = 200
    try:
        if not response:
            parsed_query = parse_request(location, request, query)
            response = _response(parsed_query, query)
            #if not response or (isinstance(response, str) and not response.strip()):
            #    return RuntimeError("Empty answer")

            if parsed_query["location"] == NOT_FOUND_LOCATION:
                http_code = 404

    # pylint: disable=broad-except
    except Exception as exception:
        logging.error("Exception has occured", exc_info=1)
        if parsed_query['html_output']:
            response = MALFORMED_RESPONSE_HTML_PAGE
            http_code = 500 # Internal Server Error
        else:
            response = get_message('CAPACITY_LIMIT_REACHED', parsed_query['lang'])
            http_code = 503 # Service Unavailable

        # if exception is occured, we return not a png file but text
        if "png_filename" in parsed_query:
            del parsed_query["png_filename"]
    return (_wrap_response(
        response, parsed_query['html_output'], parsed_query['json_output'],
        png_filename=parsed_query.get('png_filename')), http_code)

if __name__ == "__main__":
    import doctest
    doctest.testmod()



File: lib/airports.py

import csv

AIRPORTS_DAT_FILE = '/home/igor/wttrin-geo/share/airports.dat'

def load_aiports_index():
    file_ = open(AIRPORTS_DAT_FILE, "r")
    reader = csv.reader(file_)
    airport_index = {}

    for line in reader:
        airport_index[line[4]] = line

    return airport_index

AIRPORTS_INDEX = load_aiports_index()

def get_airport_gps_location(iata_code):
    if iata_code in AIRPORTS_INDEX:
        airport = AIRPORTS_INDEX[iata_code]
        return '%s,%s airport' % (airport[6], airport[7]) #, airport[1])
    return None




File: lib/metno.py

#!/bin/env python
# vim: fileencoding=utf-8
from datetime import datetime, timedelta
import json
import logging
import os
import re
import sys

import timezonefinder
from pytz import timezone

from constants import WWO_CODE

logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
logger = logging.getLogger(__name__)


def metno_request(path, query_string):
    # We'll need to sanitize the inbound request - ideally the
    # premium/v1/weather.ashx portion would have always been here, though
    # it seems as though the proxy was built after the majority of the app
    # and not refactored. For WAPI we'll strip this and the API key out,
    # then manage it on our own.
    logger.debug('Original path: ' + path)
    logger.debug('Original query: ' + query_string)

    path = path.replace('premium/v1/weather.ashx',
                        'weatherapi/locationforecast/2.0/complete')
    query_string = re.sub(r'key=[^&]*&', '', query_string)
    query_string = re.sub(r'format=[^&]*&', '', query_string)
    days = int(re.search(r'num_of_days=([0-9]+)&', query_string).group(1))
    query_string = re.sub(r'num_of_days=[0-9]+&', '', query_string)
    # query_string = query_string.replace('key=', '?key=' + WAPI_KEY)
    # TP is for hourly forecasting, which isn't available in the free api.
    query_string = re.sub(r'tp=[0-9]*&', '', query_string)
    # This assumes lang=... is at the end. Also note that the API doesn't
    # localize, and we're not either. TODO: add language support
    query_string = re.sub(r'lang=[^&]*$', '', query_string)
    query_string = re.sub(r'&$', '', query_string)

    logger.debug('qs: ' + query_string)
    # Deal with coordinates. Need to be rounded to 4 decimals for metno ToC
    # and in a different query string format
    coord_match = re.search(r'q=[^&]*', query_string)
    coords_str = coord_match.group(0)
    coords = re.findall(r'[-0-9.]+', coords_str)
    lat = str(round(float(coords[0]), 4))
    lng = str(round(float(coords[1]), 4))
    logger.debug('lat: ' + lat)
    logger.debug('lng: ' + lng)
    query_string = re.sub(r'q=[^&]*', 'lat=' + lat + '&lon=' + lng + '&',
                          query_string)
    logger.debug('Return path: ' + path)
    logger.debug('Return query: ' + query_string)

    return path, query_string, days


def celsius_to_f(celsius):
    return round((1.8 * celsius) + 32, 1)


def to_weather_code(symbol_code):
    logger.debug(symbol_code)
    code = re.sub(r'_.*', '', symbol_code)
    logger.debug(code)
    # symbol codes: https://api.met.no/weatherapi/weathericon/2.0/documentation
    # they also have _day, _night and _polartwilight variants
    # See json from https://api.met.no/weatherapi/weathericon/2.0/legends
    # WWO codes: https://github.com/chubin/wttr.in/blob/master/lib/constants.py
    #            http://www.worldweatheronline.com/feed/wwoConditionCodes.txt
    weather_code_map = {
            "clearsky": 113,
            "cloudy": 119,
            "fair": 116,
            "fog": 143,
            "heavyrain": 302,
            "heavyrainandthunder": 389,
            "heavyrainshowers": 305,
            "heavyrainshowersandthunder": 386,
            "heavysleet": 314,  # There's a ton of 'LightSleet' in WWO_CODE...
            "heavysleetandthunder": 377,
            "heavysleetshowers": 362,
            "heavysleetshowersandthunder": 374,
            "heavysnow": 230,
            "heavysnowandthunder": 392,
            "heavysnowshowers": 371,
            "heavysnowshowersandthunder": 392,
            "lightrain": 266,
            "lightrainandthunder": 200,
            "lightrainshowers": 176,
            "lightrainshowersandthunder": 386,
            "lightsleet": 281,
            "lightsleetandthunder": 377,
            "lightsleetshowers": 284,
            "lightsnow": 320,
            "lightsnowandthunder": 392,
            "lightsnowshowers": 368,
            "lightssleetshowersandthunder": 365,
            "lightssnowshowersandthunder": 392,
            "partlycloudy": 116,
            "rain": 293,
            "rainandthunder": 389,
            "rainshowers": 299,
            "rainshowersandthunder": 386,
            "sleet": 185,
            "sleetandthunder": 392,
            "sleetshowers": 263,
            "sleetshowersandthunder": 392,
            "snow": 329,
            "snowandthunder": 392,
            "snowshowers": 230,
            "snowshowersandthunder": 392,
    }
    if code not in weather_code_map:
        logger.debug('not found')
        return -1  # not found
    logger.debug(weather_code_map[code])
    return weather_code_map[code]


def to_description(symbol_code):
    desc = WWO_CODE[str(to_weather_code(symbol_code))]
    logger.debug(desc)
    return desc


def to_16_point(degrees):
    # 360 degrees / 16 = 22.5 degrees of arc or 11.25 degrees around the point
    if degrees > (360 - 11.25) or degrees <= 11.25:
        return 'N'
    if degrees > 11.25 and degrees <= (11.25 + 22.5):
        return 'NNE'
    if degrees > (11.25 + (22.5 * 1)) and degrees <= (11.25 + (22.5 * 2)):
        return 'NE'
    if degrees > (11.25 + (22.5 * 2)) and degrees <= (11.25 + (22.5 * 3)):
        return 'ENE'
    if degrees > (11.25 + (22.5 * 3)) and degrees <= (11.25 + (22.5 * 4)):
        return 'E'
    if degrees > (11.25 + (22.5 * 4)) and degrees <= (11.25 + (22.5 * 5)):
        return 'ESE'
    if degrees > (11.25 + (22.5 * 5)) and degrees <= (11.25 + (22.5 * 6)):
        return 'SE'
    if degrees > (11.25 + (22.5 * 6)) and degrees <= (11.25 + (22.5 * 7)):
        return 'SSE'
    if degrees > (11.25 + (22.5 * 7)) and degrees <= (11.25 + (22.5 * 8)):
        return 'S'
    if degrees > (11.25 + (22.5 * 8)) and degrees <= (11.25 + (22.5 * 9)):
        return 'SSW'
    if degrees > (11.25 + (22.5 * 9)) and degrees <= (11.25 + (22.5 * 10)):
        return 'SW'
    if degrees > (11.25 + (22.5 * 10)) and degrees <= (11.25 + (22.5 * 11)):
        return 'WSW'
    if degrees > (11.25 + (22.5 * 11)) and degrees <= (11.25 + (22.5 * 12)):
        return 'W'
    if degrees > (11.25 + (22.5 * 12)) and degrees <= (11.25 + (22.5 * 13)):
        return 'WNW'
    if degrees > (11.25 + (22.5 * 13)) and degrees <= (11.25 + (22.5 * 14)):
        return 'NW'
    if degrees > (11.25 + (22.5 * 14)) and degrees <= (11.25 + (22.5 * 15)):
        return 'NNW'


def meters_to_miles(meters):
    return round(meters * 0.00062137, 2)


def mm_to_inches(mm):
    return round(mm / 25.4, 2)


def hpa_to_mb(hpa):
    return hpa


def hpa_to_in(hpa):
    return round(hpa * 0.02953, 2)

def hpa_to_mmHg(hpa):
    return round(hpa * 0.75006157584566 , 3)

def group_hours_to_days(lat, lng, hourlies, days_to_return):
    tf = timezonefinder.TimezoneFinder()
    timezone_str = tf.certain_timezone_at(lat=lat, lng=lng)
    logger.debug('got TZ: ' + timezone_str)
    tz = timezone(timezone_str)
    start_day_gmt = datetime.fromisoformat(hourlies[0]['time']
                                           .replace('Z', '+00:00'))
    start_day_local = start_day_gmt.astimezone(tz)
    end_day_local = (start_day_local + timedelta(days=days_to_return - 1)).date()
    logger.debug('series starts at gmt time: ' + str(start_day_gmt))
    logger.debug('series starts at local time: ' + str(start_day_local))
    logger.debug('series ends on day: ' + str(end_day_local))
    days = {}

    for hour in hourlies:
        current_day_gmt = datetime.fromisoformat(hour['time']
                                                 .replace('Z', '+00:00'))
        current_local = current_day_gmt.astimezone(tz)
        current_day_local = current_local.date()
        if current_day_local > end_day_local:
            continue
        if current_day_local not in days:
            days[current_day_local] = {'hourly': []}
        hour['localtime'] = current_local.time()
        days[current_day_local]['hourly'].append(hour)

    # Need a second pass to build the min/max/avg data
    for date, day in days.items():
        minTempC = -999
        maxTempC = 1000
        avgTempC = None
        n = 0
        maxUvIndex = 0
        for hour in day['hourly']:
            temp = hour['data']['instant']['details']['air_temperature']
            if temp > minTempC:
                minTempC = temp
            if temp < maxTempC:
                maxTempC = temp
            if avgTempC is None:
                avgTempC = temp
                n = 1
            else:
                avgTempC = ((avgTempC * n) + temp) / (n + 1)
                n = n + 1

            uv = hour['data']['instant']['details']
            if 'ultraviolet_index_clear_sky' in uv:
                if uv['ultraviolet_index_clear_sky'] > maxUvIndex:
                    maxUvIndex = uv['ultraviolet_index_clear_sky']
        day["maxtempC"] = str(maxTempC)
        day["maxtempF"] = str(celsius_to_f(maxTempC))
        day["mintempC"] = str(minTempC)
        day["mintempF"] = str(celsius_to_f(minTempC))
        day["avgtempC"] = str(round(avgTempC, 1))
        day["avgtempF"] = str(celsius_to_f(avgTempC))
        # day["totalSnow_cm": "not implemented",
        # day["sunHour": "12",  # This would come from astonomy data
        day["uvIndex"] = str(maxUvIndex)

    return days

def _convert_hour(hour):
    # Whatever is upstream is expecting data in the shape of WWO. This method will
    # morph from metno to hourly WWO response format.
    # Note that WWO is providing data every 3 hours. Metno provides every hour
    #   {
    # "time": "0",
    # "tempC": "19",
    # "tempF": "66",
    # "windspeedMiles": "6",
    # "windspeedKmph": "9",
    # "winddirDegree": "276",
    # "winddir16Point": "W",
    # "weatherCode": "119",
    # "weatherIconUrl": [
    #   {
    #     "value": "http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0003_white_cloud.png"
    #   }
    # ],
    # "weatherDesc": [
    #   {
    #     "value": "Cloudy"
    #   }
    # ],
    # "precipMM": "0.0",
    # "precipInches": "0.0",
    # "humidity": "62",
    # "visibility": "10",
    # "visibilityMiles": "6",
    # "pressure": "1017",
    # "pressureInches": "31",
    # "cloudcover": "66",
    # "HeatIndexC": "19",
    # "HeatIndexF": "66",
    # "DewPointC": "12",
    # "DewPointF": "53",
    # "WindChillC": "19",
    # "WindChillF": "66",
    # "WindGustMiles": "8",
    # "WindGustKmph": "13",
    # "FeelsLikeC": "19",
    # "FeelsLikeF": "66",
    # "chanceofrain": "0",
    # "chanceofremdry": "93",
    # "chanceofwindy": "0",
    # "chanceofovercast": "89",
    # "chanceofsunshine": "18",
    # "chanceoffrost": "0",
    # "chanceofhightemp": "0",
    # "chanceoffog": "0",
    # "chanceofsnow": "0",
    # "chanceofthunder": "0",
    # "uvIndex": "1"
    details = hour['data']['instant']['details']
    if 'next_1_hours' in hour['data']:
        next_hour = hour['data']['next_1_hours']
    elif 'next_6_hours' in hour['data']:
        next_hour = hour['data']['next_6_hours']
    elif 'next_12_hours' in hour['data']:
        next_hour = hour['data']['next_12_hours']
    else:
        next_hour = {}

    # Need to dig out symbol_code and precipitation_amount
    symbol_code = 'clearsky_day'  # Default to sunny
    if 'summary' in next_hour and 'symbol_code' in next_hour['summary']:
        symbol_code = next_hour['summary']['symbol_code']
    precipitation_amount = 0  # Default to no rain
    if 'details' in next_hour and 'precipitation_amount' in next_hour['details']:
        precipitation_amount = next_hour['details']['precipitation_amount']

    uvIndex = 0  # default to 0 index
    if 'ultraviolet_index_clear_sky' in details:
        uvIndex = details['ultraviolet_index_clear_sky']
    localtime = ''
    if 'localtime' in hour:
        localtime = "{h:02.0f}".format(h=hour['localtime'].hour) + \
                    "{m:02.0f}".format(m=hour['localtime'].minute)
        logger.debug(str(hour['localtime']))
    # time property is local time, 4 digit 24 hour, with no :, e.g. 2100
    return {
        'time': localtime,
        'observation_time': hour['time'],  # Need to figure out WWO TZ
        # temp_C is used in we-lang.go calcs in such a way
        # as to expect a whole number
        'temp_C': str(int(round(details['air_temperature'], 0))),
        # temp_F can be more precise - not used in we-lang.go calcs
        'temp_F': str(celsius_to_f(details['air_temperature'])),
        'weatherCode': str(to_weather_code(symbol_code)),
        'weatherIconUrl': [{
            'value': 'not yet implemented',
        }],
        'weatherDesc': [{
            'value': to_description(symbol_code),
        }],
        # similiarly, windspeedMiles is not used by we-lang.go, but kmph is
        "windspeedMiles": str(meters_to_miles(details['wind_speed'])),
        "windspeedKmph": str(int(round(details['wind_speed'], 0))),
        "winddirDegree": str(details['wind_from_direction']),
        "winddir16Point": to_16_point(details['wind_from_direction']),
        "precipMM": str(precipitation_amount),
        "precipInches": str(mm_to_inches(precipitation_amount)),
        "humidity": str(details['relative_humidity']),
        "visibility": 'not yet implemented',  # str(details['vis_km']),
        "visibilityMiles": 'not yet implemented',  # str(details['vis_miles']),
        "pressure": str(hpa_to_mb(details['air_pressure_at_sea_level'])),
        "pressure_mmHg": str(hpa_to_mmHg(details['air_pressure_at_sea_level'])),
        "pressureInches": str(hpa_to_in(details['air_pressure_at_sea_level'])),
        "cloudcover": 'not yet implemented',  # Convert from cloud_area_fraction?? str(details['cloud']),
        # metno doesn't have FeelsLikeC, but we-lang.go is using it in calcs,
        # so we shall set it to temp_C
        "FeelsLikeC": str(int(round(details['air_temperature'], 0))),
        "FeelsLikeF": 'not yet implemented',  # str(details['feelslike_f']),
        "uvIndex": str(uvIndex),
    }


def _convert_hourly(hours):
    converted_hours = []
    for hour in hours:
        converted_hours.append(_convert_hour(hour))
    return converted_hours


# Whatever is upstream is expecting data in the shape of WWO. This method will
# morph from metno to WWO response format.
def create_standard_json_from_metno(content, days_to_return):
    try:
        forecast = json.loads(content)         # pylint: disable=invalid-name
    except (ValueError, TypeError) as exception:
        logger.error("---")
        logger.error(exception)
        logger.error("---")
        return {}, ''
    hourlies = forecast['properties']['timeseries']
    current = hourlies[0]
    # We are assuming these units:
    # "units": {
    #   "air_pressure_at_sea_level": "hPa",
    #   "air_temperature": "celsius",
    #   "air_temperature_max": "celsius",
    #   "air_temperature_min": "celsius",
    #   "cloud_area_fraction": "%",
    #   "cloud_area_fraction_high": "%",
    #   "cloud_area_fraction_low": "%",
    #   "cloud_area_fraction_medium": "%",
    #   "dew_point_temperature": "celsius",
    #   "fog_area_fraction": "%",
    #   "precipitation_amount": "mm",
    #   "relative_humidity": "%",
    #   "ultraviolet_index_clear_sky": "1",
    #   "wind_from_direction": "degrees",
    #   "wind_speed": "m/s"
    # }
    content = {
        'data': {
            'request': [{
                'type': 'feature',
                'query': str(forecast['geometry']['coordinates'][1]) + ',' +
                        str(forecast['geometry']['coordinates'][0])
            }],
            'current_condition': [
                _convert_hour(current)
            ],
            'weather': []
        }
    }

    days = group_hours_to_days(forecast['geometry']['coordinates'][1],
                               forecast['geometry']['coordinates'][0],
                               hourlies, days_to_return)

    # TODO: Astronomy needs to come from this:
    # https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2020-10-07&offset=-05:00
    # and obviously can be cached for a while
    # https://api.met.no/weatherapi/sunrise/2.0/documentation
    # Note that full moon/new moon/first quarter/last quarter aren't returned
    # and the moonphase value should match these from WWO:
    # New Moon
    # Waxing Crescent
    # First Quarter
    # Waxing Gibbous
    # Full Moon
    # Waning Gibbous
    # Last Quarter
    # Waning Crescent

    for date, day in days.items():
        content['data']['weather'].append({
                "date": str(date),
                "astronomy": [],
                "maxtempC": day['maxtempC'],
                "maxtempF": day['maxtempF'],
                "mintempC": day['mintempC'],
                "mintempF": day['mintempF'],
                "avgtempC": day['avgtempC'],
                "avgtempF": day['avgtempF'],
                "totalSnow_cm": "not implemented",
                "sunHour": "12",  # This would come from astonomy data
                "uvIndex": day['uvIndex'],
                'hourly': _convert_hourly(day['hourly']),
        })

    # for day in forecast.
    return json.dumps(content)


if __name__ == "__main__":
    # if len(sys.argv) == 1:
    #     for deg in range(0, 360):
    #         print('deg: ' + str(deg) + '; 16point: ' + to_16_point(deg))
    if len(sys.argv) == 2:
        req = sys.argv[1].split('?')
        # to_description(sys.argv[1])
        metno_request(req[0], req[1])
    elif len(sys.argv) == 3:
        with open(sys.argv[1], 'r') as contentf:
            content = create_standard_json_from_metno(contentf.read(),
                                                      int(sys.argv[2]))
        print(content)
    else:
        print('usage: metno <content file> <days>')



File: lib/weather_data.py

"""
Weather data source
"""

import json
import requests
from globals import WWO_KEY

def get_weather_data(location, lang):
    """
    Get weather data for `location`
    """
    key = WWO_KEY
    url = ('/premium/v1/weather.ashx'
           '?key=%s&q=%s&format=json'
           '&num_of_days=3&tp=3&lang=%s') % (key, location, lang)
    url = 'http://127.0.0.1:5001' + url

    response = requests.get(url, timeout=10)
    try:
        data = json.loads(response.content)
    except ValueError:
        data = {}
    return data



File: lib/parse_query.py

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):
    """
    """

    # what units should be used
    # metric or imperial
    # based on query and location source (imperial for US by default)
    if query.get('use_metric', False) and not query.get('use_imperial', False):
        query['use_imperial'] = False
        query['use_metric'] = True
    elif query.get('use_imperial', False) and not query.get('use_metric', False):
        query['use_imperial'] = True
        query['use_metric'] = False
    elif lang == 'us':
        # slack uses m by default, to override it speciy us.wttr.in
        query['use_imperial'] = True
        query['use_metric'] = False
    else:
        if us_ip:
            query['use_imperial'] = True
            query['use_metric'] = False
        else:
            query['use_imperial'] = False
            query['use_metric'] = True

    return query

def parse_query(args):
    result = {}

    reserved_args = ["lang"]

    q = ""

    for key, val in args.items():
        if len(val) == 0:
            q += key
            continue
        if val == 'True':
            val = True
        if val == 'False':
            val = False
        result[key] = val

    if q is None:
        return result
    if 'A' in q:
        result['force-ansi'] = True
    if 'd' in q:
        result['dumb'] = True
    if 'n' in q:
        result['narrow'] = True
    if 'm' in q:
        result['use_metric'] = True
    if 'M' in q:
        result['use_metric'] = True
        result['use_ms_for_wind'] = True
    if 'u' in q:
        result['use_imperial'] = True
    if 'I' in q:
        result['inverted_colors'] = True
    if 't' in q:
        result['transparency'] = '150'
    if 'T' in q:
        result['no-terminal'] = True
    if 'p' in q:
        result['padding'] = True

    for days in "0123":
        if days in q:
            result['days'] = days

    if 'q' in q:
        result['no-caption'] = True
    if 'Q' in q:
        result['no-city'] = True
    if 'F' in q:
        result['no-follow-line'] = True

    for key, val in args.items():
        if val == 'True':
            val = True
        if val == 'False':
            val = False
        if val:
            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):
    """
    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]

    one_letter_options = ""
    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:
            one_letter_options += part

    for letter in one_letter_options:
        to_be_parsed[letter] = ''

    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



File: lib/fmt/__init__.py




File: lib/fmt/png.py

#!/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 is:

* render_ansi(png_file, text, options=None)

`render_ansi` is the main function of the module,
which does rendering of stream into a PNG-file.

The module uses PIL for graphical tasks, and pyte for rendering
of ANSI stream into terminal representation.
"""

from __future__ import print_function

import sys
import io
import os
import glob

from PIL import Image, ImageFont, ImageDraw
import pyte.screens
import emoji
import grapheme

from . import unicodedata2

sys.path.insert(0, "..")
import constants
import globals

COLS = 180
ROWS = 100
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",
    '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 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, inverse=False):
    """Convert pyte color to PIL color

    Return: tuple of color values (R,G,B)
    """

    if color == 'default':
        if inverse:
            return 'black'
        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 emoji.is_emoji(char):
        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)[:-3]
        emojilib[character] = \
            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
    """

    if not options:
        options = {}

    current_grapheme = 0

    buf = _strip_buf(buf)
    cols = max(len(x) for x in buf)
    rows = len(buf)

    bg_color = 0
    if "background" in options:
        bg_color = _color_mapping(options["background"], options.get("inverted_colors"))

    image = Image.new('RGB', (cols * CHAR_WIDTH, rows * CHAR_HEIGHT), color=bg_color)

    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, options.get("inverted_colors"))
            if char.bg != 'default':
                draw.rectangle(
                    ((x_pos, y_pos),
                     (x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)),
                    fill=_color_mapping(char.bg, options.get("inverted_colors")))

            if char.data == "!":
                try:
                    data = graphemes[current_grapheme]
                except IndexError:
                    pass
                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

    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



File: lib/fmt/unicodedata2.py

# 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 *

script_data = {
"names":['Common', 'Latin', 'Greek', 'Cyrillic', 'Armenian', 'Hebrew', 'Arabic',
'Syriac', 'Thaana', 'Devanagari', 'Bengali', 'Gurmukhi', 'Gujarati', 'Oriya',
'Tamil', 'Telugu', 'Kannada', 'Malayalam', 'Sinhala', 'Thai', 'Lao', 'Tibetan',
'Myanmar', 'Georgian', 'Hangul', 'Ethiopic', 'Cherokee', 'Canadian_Aboriginal',
'Ogham', 'Runic', 'Khmer', 'Mongolian', 'Hiragana', 'Katakana', 'Bopomofo',
'Han', 'Yi', 'Old_Italic', 'Gothic', 'Deseret', 'Inherited', 'Tagalog',
'Hanunoo', 'Buhid', 'Tagbanwa', 'Limbu', 'Tai_Le', 'Linear_B', 'Ugaritic',
'Shavian', 'Osmanya', 'Cypriot', 'Braille', 'Buginese', 'Coptic', 'New_Tai_Lue',
'Glagolitic', 'Tifinagh', 'Syloti_Nagri', 'Old_Persian', 'Kharoshthi',
'Balinese', 'Cuneiform', 'Phoenician', 'Phags_Pa', 'Nko', 'Sundanese', 'Lepcha',
'Ol_Chiki', 'Vai', 'Saurashtra', 'Kayah_Li', 'Rejang', 'Lycian', 'Carian',
'Lydian', 'Cham', 'Tai_Tham', 'Tai_Viet', 'Avestan', 'Egyptian_Hieroglyphs',
'Samaritan', 'Lisu', 'Bamum', 'Javanese', 'Meetei_Mayek', 'Imperial_Aramaic',
'Old_South_Arabian', 'Inscriptional_Parthian', 'Inscriptional_Pahlavi',
'Old_Turkic', 'Kaithi', 'Batak', 'Brahmi', 'Mandaic', 'Chakma',
'Meroitic_Cursive', 'Meroitic_Hieroglyphs', 'Miao', 'Sharada', 'Sora_Sompeng',
'Takri'],
"cats":['Cc', 'Zs', 'Po', 'Sc', 'Ps', 'Pe', 'Sm', 'Pd', 'Nd', 'Sk', 'Pc', 'So', 'Pi',
'Cf', 'No', 'L', 'Pf', 'Lm', 'Mc', 'Lo', 'Zl', 'Zp', 'Nl', 'Mn', 'Me'],
"idx":[
(0x0,0x1f,0,0), (0x20,0x20,0,1), (0x21,0x23,0,2), (0x24,0x24,0,3),
(0x25,0x27,0,2), (0x28,0x28,0,4), (0x29,0x29,0,5), (0x2a,0x2a,0,2),
(0x2b,0x2b,0,6), (0x2c,0x2c,0,2), (0x2d,0x2d,0,7), (0x2e,0x2f,0,2),
(0x30,0x39,0,8), (0x3a,0x3b,0,2), (0x3c,0x3e,0,6), (0x3f,0x40,0,2),
(0x41,0x5a,1,15), (0x5b,0x5b,0,4), (0x5c,0x5c,0,2), (0x5d,0x5d,0,5),
(0x5e,0x5e,0,9), (0x5f,0x5f,0,10), (0x60,0x60,0,9), (0x61,0x7a,1,15),
(0x7b,0x7b,0,4), (0x7c,0x7c,0,6), (0x7d,0x7d,0,5), (0x7e,0x7e,0,6),
(0x7f,0x9f,0,0), (0xa0,0xa0,0,1), (0xa1,0xa1,0,2), (0xa2,0xa5,0,3),
(0xa6,0xa6,0,11), (0xa7,0xa7,0,2), (0xa8,0xa8,0,9), (0xa9,0xa9,0,11),
(0xaa,0xaa,1,19), (0xab,0xab,0,12), (0xac,0xac,0,6), (0xad,0xad,0,13),
(0xae,0xae,0,11), (0xaf,0xaf,0,9), (0xb0,0xb0,0,11), (0xb1,0xb1,0,6),
(0xb2,0xb3,0,14), (0xb4,0xb4,0,9), (0xb5,0xb5,0,15), (0xb6,0xb7,0,2),
(0xb8,0xb8,0,9), (0xb9,0xb9,0,14), (0xba,0xba,1,19), (0xbb,0xbb,0,16),
(0xbc,0xbe,0,14), (0xbf,0xbf,0,2), (0xc0,0xd6,1,15), (0xd7,0xd7,0,6),
(0xd8,0xf6,1,15), (0xf7,0xf7,0,6), (0xf8,0x1ba,1,15), (0x1bb,0x1bb,1,19),
(0x1bc,0x1bf,1,15), (0x1c0,0x1c3,1,19), (0x1c4,0x293,1,15), (0x294,0x294,1,19),
(0x295,0x2af,1,15), (0x2b0,0x2b8,1,17), (0x2b9,0x2c1,0,17), (0x2c2,0x2c5,0,9),
(0x2c6,0x2d1,0,17), (0x2d2,0x2df,0,9), (0x2e0,0x2e4,1,17), (0x2e5,0x2e9,0,9),
(0x2ea,0x2eb,34,9), (0x2ec,0x2ec,0,17), (0x2ed,0x2ed,0,9), (0x2ee,0x2ee,0,17),
(0x2ef,0x2ff,0,9), (0x300,0x36f,40,23), (0x370,0x373,2,15), (0x374,0x374,0,17),
(0x375,0x375,2,9), (0x376,0x377,2,15), (0x37a,0x37a,2,17), (0x37b,0x37d,2,15),
(0x37e,0x37e,0,2), (0x384,0x384,2,9), (0x385,0x385,0,9), (0x386,0x386,2,15),
(0x387,0x387,0,2), (0x388,0x38a,2,15), (0x38c,0x38c,2,15), (0x38e,0x3a1,2,15),
(0x3a3,0x3e1,2,15), (0x3e2,0x3ef,54,15), (0x3f0,0x3f5,2,15), (0x3f6,0x3f6,2,6),
(0x3f7,0x3ff,2,15), (0x400,0x481,3,15), (0x482,0x482,3,11), (0x483,0x484,3,23),
(0x485,0x486,40,23), (0x487,0x487,3,23), (0x488,0x489,3,24), (0x48a,0x527,3,15),
(0x531,0x556,4,15), (0x559,0x559,4,17), (0x55a,0x55f,4,2), (0x561,0x587,4,15),
(0x589,0x589,0,2), (0x58a,0x58a,4,7), (0x58f,0x58f,4,3), (0x591,0x5bd,5,23),
(0x5be,0x5be,5,7), (0x5bf,0x5bf,5,23), (0x5c0,0x5c0,5,2), (0x5c1,0x5c2,5,23),
(0x5c3,0x5c3,5,2), (0x5c4,0x5c5,5,23), (0x5c6,0x5c6,5,2), (0x5c7,0x5c7,5,23),
(0x5d0,0x5ea,5,19), (0x5f0,0x5f2,5,19), (0x5f3,0x5f4,5,2), (0x600,0x604,6,13),
(0x606,0x608,6,6), (0x609,0x60a,6,2), (0x60b,0x60b,6,3), (0x60c,0x60c,0,2),
(0x60d,0x60d,6,2), (0x60e,0x60f,6,11), (0x610,0x61a,6,23), (0x61b,0x61b,0,2),
(0x61e,0x61e,6,2), (0x61f,0x61f,0,2), (0x620,0x63f,6,19), (0x640,0x640,0,17),
(0x641,0x64a,6,19), (0x64b,0x655,40,23), (0x656,0x65e,6,23),
(0x65f,0x65f,40,23), (0x660,0x669,0,8), (0x66a,0x66d,6,2), (0x66e,0x66f,6,19),
(0x670,0x670,40,23), (0x671,0x6d3,6,19), (0x6d4,0x6d4,6,2), (0x6d5,0x6d5,6,19),
(0x6d6,0x6dc,6,23), (0x6dd,0x6dd,0,13), (0x6de,0x6de,6,11), (0x6df,0x6e4,6,23),
(0x6e5,0x6e6,6,17), (0x6e7,0x6e8,6,23), (0x6e9,0x6e9,6,11), (0x6ea,0x6ed,6,23),
(0x6ee,0x6ef,6,19), (0x6f0,0x6f9,6,8), (0x6fa,0x6fc,6,19), (0x6fd,0x6fe,6,11),
(0x6ff,0x6ff,6,19), (0x700,0x70d,7,2), (0x70f,0x70f,7,13), (0x710,0x710,7,19),
(0x711,0x711,7,23), (0x712,0x72f,7,19), (0x730,0x74a,7,23), (0x74d,0x74f,7,19),
(0x750,0x77f,6,19), (0x780,0x7a5,8,19), (0x7a6,0x7b0,8,23), (0x7b1,0x7b1,8,19),
(0x7c0,0x7c9,65,8), (0x7ca,0x7ea,65,19), (0x7eb,0x7f3,65,23),
(0x7f4,0x7f5,65,17), (0x7f6,0x7f6,65,11), (0x7f7,0x7f9,65,2),
(0x7fa,0x7fa,65,17), (0x800,0x815,81,19), (0x816,0x819,81,23),
(0x81a,0x81a,81,17), (0x81b,0x823,81,23), (0x824,0x824,81,17),
(0x825,0x827,81,23), (0x828,0x828,81,17), (0x829,0x82d,81,23),
(0x830,0x83e,81,2), (0x840,0x858,94,19), (0x859,0x85b,94,23),
(0x85e,0x85e,94,2), (0x8a0,0x8a0,6,19), (0x8a2,0x8ac,6,19), (0x8e4,0x8fe,6,23),
(0x900,0x902,9,23), (0x903,0x903,9,18), (0x904,0x939,9,19), (0x93a,0x93a,9,23),
(0x93b,0x93b,9,18), (0x93c,0x93c,9,23), (0x93d,0x93d,9,19), (0x93e,0x940,9,18),
(0x941,0x948,9,23), (0x949,0x94c,9,18), (0x94d,0x94d,9,23), (0x94e,0x94f,9,18),
(0x950,0x950,9,19), (0x951,0x952,40,23), (0x953,0x957,9,23), (0x958,0x961,9,19),
(0x962,0x963,9,23), (0x964,0x965,0,2), (0x966,0x96f,9,8), (0x970,0x970,9,2),
(0x971,0x971,9,17), (0x972,0x977,9,19), (0x979,0x97f,9,19), (0x981,0x981,10,23),
(0x982,0x983,10,18), (0x985,0x98c,10,19), (0x98f,0x990,10,19),
(0x993,0x9a8,10,19), (0x9aa,0x9b0,10,19), (0x9b2,0x9b2,10,19),
(0x9b6,0x9b9,10,19), (0x9bc,0x9bc,10,23), (0x9bd,0x9bd,10,19),
(0x9be,0x9c0,10,18), (0x9c1,0x9c4,10,23), (0x9c7,0x9c8,10,18),
(0x9cb,0x9cc,10,18), (0x9cd,0x9cd,10,23), (0x9ce,0x9ce,10,19),
(0x9d7,0x9d7,10,18), (0x9dc,0x9dd,10,19), (0x9df,0x9e1,10,19),
(0x9e2,0x9e3,10,23), (0x9e6,0x9ef,10,8), (0x9f0,0x9f1,10,19),
(0x9f2,0x9f3,10,3), (0x9f4,0x9f9,10,14), (0x9fa,0x9fa,10,11),
(0x9fb,0x9fb,10,3), (0xa01,0xa02,11,23), (0xa03,0xa03,11,18),
(0xa05,0xa0a,11,19), (0xa0f,0xa10,11,19), (0xa13,0xa28,11,19),
(0xa2a,0xa30,11,19), (0xa32,0xa33,11,19), (0xa35,0xa36,11,19),
(0xa38,0xa39,11,19), (0xa3c,0xa3c,11,23), (0xa3e,0xa40,11,18),
(0xa41,0xa42,11,23), (0xa47,0xa48,11,23), (0xa4b,0xa4d,11,23),
(0xa51,0xa51,11,23), (0xa59,0xa5c,11,19), (0xa5e,0xa5e,11,19),
(0xa66,0xa6f,11,8), (0xa70,0xa71,11,23), (0xa72,0xa74,11,19),
(0xa75,0xa75,11,23), (0xa81,0xa82,12,23), (0xa83,0xa83,12,18),
(0xa85,0xa8d,12,19), (0xa8f,0xa91,12,19), (0xa93,0xaa8,12,19),
(0xaaa,0xab0,12,19), (0xab2,0xab3,12,19), (0xab5,0xab9,12,19),
(0xabc,0xabc,12,23), (0xabd,0xabd,12,19), (0xabe,0xac0,12,18),
(0xac1,0xac5,12,23), (0xac7,0xac8,12,23), (0xac9,0xac9,12,18),
(0xacb,0xacc,12,18), (0xacd,0xacd,12,23), (0xad0,0xad0,12,19),
(0xae0,0xae1,12,19), (0xae2,0xae3,12,23), (0xae6,0xaef,12,8),
(0xaf0,0xaf0,12,2), (0xaf1,0xaf1,12,3), (0xb01,0xb01,13,23),
(0xb02,0xb03,13,18), (0xb05,0xb0c,13,19), (0xb0f,0xb10,13,19),
(0xb13,0xb28,13,19), (0xb2a,0xb30,13,19), (0xb32,0xb33,13,19),
(0xb35,0xb39,13,19), (0xb3c,0xb3c,13,23), (0xb3d,0xb3d,13,19),
(0xb3e,0xb3e,13,18), (0xb3f,0xb3f,13,23), (0xb40,0xb40,13,18),
(0xb41,0xb44,13,23), (0xb47,0xb48,13,18), (0xb4b,0xb4c,13,18),
(0xb4d,0xb4d,13,23), (0xb56,0xb56,13,23), (0xb57,0xb57,13,18),
(0xb5c,0xb5d,13,19), (0xb5f,0xb61,13,19), (0xb62,0xb63,13,23),
(0xb66,0xb6f,13,8), (0xb70,0xb70,13,11), (0xb71,0xb71,13,19),
(0xb72,0xb77,13,14), (0xb82,0xb82,14,23), (0xb83,0xb83,14,19),
(0xb85,0xb8a,14,19), (0xb8e,0xb90,14,19), (0xb92,0xb95,14,19),
(0xb99,0xb9a,14,19), (0xb9c,0xb9c,14,19), (0xb9e,0xb9f,14,19),
(0xba3,0xba4,14,19), (0xba8,0xbaa,14,19), (0xbae,0xbb9,14,19),
(0xbbe,0xbbf,14,18), (0xbc0,0xbc0,14,23), (0xbc1,0xbc2,14,18),
(0xbc6,0xbc8,14,18), (0xbca,0xbcc,14,18), (0xbcd,0xbcd,14,23),
(0xbd0,0xbd0,14,19), (0xbd7,0xbd7,14,18), (0xbe6,0xbef,14,8),
(0xbf0,0xbf2,14,14), (0xbf3,0xbf8,14,11), (0xbf9,0xbf9,14,3),
(0xbfa,0xbfa,14,11), (0xc01,0xc03,15,18), (0xc05,0xc0c,15,19),
(0xc0e,0xc10,15,19), (0xc12,0xc28,15,19), (0xc2a,0xc33,15,19),
(0xc35,0xc39,15,19), (0xc3d,0xc3d,15,19), (0xc3e,0xc40,15,23),
(0xc41,0xc44,15,18), (0xc46,0xc48,15,23), (0xc4a,0xc4d,15,23),
(0xc55,0xc56,15,23), (0xc58,0xc59,15,19), (0xc60,0xc61,15,19),
(0xc62,0xc63,15,23), (0xc66,0xc6f,15,8), (0xc78,0xc7e,15,14),
(0xc7f,0xc7f,15,11), (0xc82,0xc83,16,18), (0xc85,0xc8c,16,19),
(0xc8e,0xc90,16,19), (0xc92,0xca8,16,19), (0xcaa,0xcb3,16,19),
(0xcb5,0xcb9,16,19), (0xcbc,0xcbc,16,23), (0xcbd,0xcbd,16,19),
(0xcbe,0xcbe,16,18), (0xcbf,0xcbf,16,23), (0xcc0,0xcc4,16,18),
(0xcc6,0xcc6,16,23), (0xcc7,0xcc8,16,18), (0xcca,0xccb,16,18),
(0xccc,0xccd,16,23), (0xcd5,0xcd6,16,18), (0xcde,0xcde,16,19),
(0xce0,0xce1,16,19), (0xce2,0xce3,16,23), (0xce6,0xcef,16,8),
(0xcf1,0xcf2,16,19), (0xd02,0xd03,17,18), (0xd05,0xd0c,17,19),
(0xd0e,0xd10,17,19), (0xd12,0xd3a,17,19), (0xd3d,0xd3d,17,19),
(0xd3e,0xd40,17,18), (0xd41,0xd44,17,23), (0xd46,0xd48,17,18),
(0xd4a,0xd4c,17,18), (0xd4d,0xd4d,17,23), (0xd4e,0xd4e,17,19),
(0xd57,0xd57,17,18), (0xd60,0xd61,17,19), (0xd62,0xd63,17,23),
(0xd66,0xd6f,17,8), (0xd70,0xd75,17,14), (0xd79,0xd79,17,11),
(0xd7a,0xd7f,17,19), (0xd82,0xd83,18,18), (0xd85,0xd96,18,19),
(0xd9a,0xdb1,18,19), (0xdb3,0xdbb,18,19), (0xdbd,0xdbd,18,19),
(0xdc0,0xdc6,18,19), (0xdca,0xdca,18,23), (0xdcf,0xdd1,18,18),
(0xdd2,0xdd4,18,23), (0xdd6,0xdd6,18,23), (0xdd8,0xddf,18,18),
(0xdf2,0xdf3,18,18), (0xdf4,0xdf4,18,2), (0xe01,0xe30,19,19),
(0xe31,0xe31,19,23), (0xe32,0xe33,19,19), (0xe34,0xe3a,19,23),
(0xe3f,0xe3f,0,3), (0xe40,0xe45,19,19), (0xe46,0xe46,19,17),
(0xe47,0xe4e,19,23), (0xe4f,0xe4f,19,2), (0xe50,0xe59,19,8), (0xe5a,0xe5b,19,2),
(0xe81,0xe82,20,19), (0xe84,0xe84,20,19), (0xe87,0xe88,20,19),
(0xe8a,0xe8a,20,19), (0xe8d,0xe8d,20,19), (0xe94,0xe97,20,19),
(0xe99,0xe9f,20,19), (0xea1,0xea3,20,19), (0xea5,0xea5,20,19),
(0xea7,0xea7,20,19), (0xeaa,0xeab,20,19), (0xead,0xeb0,20,19),
(0xeb1,0xeb1,20,23), (0xeb2,0xeb3,20,19), (0xeb4,0xeb9,20,23),
(0xebb,0xebc,20,23), (0xebd,0xebd,20,19), (0xec0,0xec4,20,19),
(0xec6,0xec6,20,17), (0xec8,0xecd,20,23), (0xed0,0xed9,20,8),
(0xedc,0xedf,20,19), (0xf00,0xf00,21,19), (0xf01,0xf03,21,11),
(0xf04,0xf12,21,2), (0xf13,0xf13,21,11), (0xf14,0xf14,21,2),
(0xf15,0xf17,21,11), (0xf18,0xf19,21,23), (0xf1a,0xf1f,21,11),
(0xf20,0xf29,21,8), (0xf2a,0xf33,21,14), (0xf34,0xf34,21,11),
(0xf35,0xf35,21,23), (0xf36,0xf36,21,11), (0xf37,0xf37,21,23),
(0xf38,0xf38,21,11), (0xf39,0xf39,21,23), (0xf3a,0xf3a,21,4),
(0xf3b,0xf3b,21,5), (0xf3c,0xf3c,21,4), (0xf3d,0xf3d,21,5), (0xf3e,0xf3f,21,18),
(0xf40,0xf47,21,19), (0xf49,0xf6c,21,19), (0xf71,0xf7e,21,23),
(0xf7f,0xf7f,21,18), (0xf80,0xf84,21,23), (0xf85,0xf85,21,2),
(0xf86,0xf87,21,23), (0xf88,0xf8c,21,19), (0xf8d,0xf97,21,23),
(0xf99,0xfbc,21,23), (0xfbe,0xfc5,21,11), (0xfc6,0xfc6,21,23),
(0xfc7,0xfcc,21,11), (0xfce,0xfcf,21,11), (0xfd0,0xfd4,21,2),
(0xfd5,0xfd8,0,11), (0xfd9,0xfda,21,2), (0x1000,0x102a,22,19),
(0x102b,0x102c,22,18), (0x102d,0x1030,22,23), (0x1031,0x1031,22,18),
(0x1032,0x1037,22,23), (0x1038,0x1038,22,18), (0x1039,0x103a,22,23),
(0x103b,0x103c,22,18), (0x103d,0x103e,22,23), (0x103f,0x103f,22,19),
(0x1040,0x1049,22,8), (0x104a,0x104f,22,2), (0x1050,0x1055,22,19),
(0x1056,0x1057,22,18), (0x1058,0x1059,22,23), (0x105a,0x105d,22,19),
(0x105e,0x1060,22,23), (0x1061,0x1061,22,19), (0x1062,0x1064,22,18),
(0x1065,0x1066,22,19), (0x1067,0x106d,22,18), (0x106e,0x1070,22,19),
(0x1071,0x1074,22,23), (0x1075,0x1081,22,19), (0x1082,0x1082,22,23),
(0x1083,0x1084,22,18), (0x1085,0x1086,22,23), (0x1087,0x108c,22,18),
(0x108d,0x108d,22,23), (0x108e,0x108e,22,19), (0x108f,0x108f,22,18),
(0x1090,0x1099,22,8), (0x109a,0x109c,22,18), (0x109d,0x109d,22,23),
(0x109e,0x109f,22,11), (0x10a0,0x10c5,23,15), (0x10c7,0x10c7,23,15),
(0x10cd,0x10cd,23,15), (0x10d0,0x10fa,23,19), (0x10fb,0x10fb,0,2),
(0x10fc,0x10fc,23,17), (0x10fd,0x10ff,23,19), (0x1100,0x11ff,24,19),
(0x1200,0x1248,25,19), (0x124a,0x124d,25,19), (0x1250,0x1256,25,19),
(0x1258,0x1258,25,19), (0x125a,0x125d,25,19), (0x1260,0x1288,25,19),
(0x128a,0x128d,25,19), (0x1290,0x12b0,25,19), (0x12b2,0x12b5,25,19),
(0x12b8,0x12be,25,19), (0x12c0,0x12c0,25,19), (0x12c2,0x12c5,25,19),
(0x12c8,0x12d6,25,19), (0x12d8,0x1310,25,19), (0x1312,0x1315,25,19),
(0x1318,0x135a,25,19), (0x135d,0x135f,25,23), (0x1360,0x1368,25,2),
(0x1369,0x137c,25,14), (0x1380,0x138f,25,19), (0x1390,0x1399,25,11),
(0x13a0,0x13f4,26,19), (0x1400,0x1400,27,7), (0x1401,0x166c,27,19),
(0x166d,0x166e,27,2), (0x166f,0x167f,27,19), (0x1680,0x1680,28,1),
(0x1681,0x169a,28,19), (0x169b,0x169b,28,4), (0x169c,0x169c,28,5),
(0x16a0,0x16ea,29,19), (0x16eb,0x16ed,0,2), (0x16ee,0x16f0,29,22),
(0x1700,0x170c,41,19), (0x170e,0x1711,41,19), (0x1712,0x1714,41,23),
(0x1720,0x1731,42,19), (0x1732,0x1734,42,23), (0x1735,0x1736,0,2),
(0x1740,0x1751,43,19), (0x1752,0x1753,43,23), (0x1760,0x176c,44,19),
(0x176e,0x1770,44,19), (0x1772,0x1773,44,23), (0x1780,0x17b3,30,19),
(0x17b4,0x17b5,30,23), (0x17b6,0x17b6,30,18), (0x17b7,0x17bd,30,23),
(0x17be,0x17c5,30,18), (0x17c6,0x17c6,30,23), (0x17c7,0x17c8,30,18),
(0x17c9,0x17d3,30,23), (0x17d4,0x17d6,30,2), (0x17d7,0x17d7,30,17),
(0x17d8,0x17da,30,2), (0x17db,0x17db,30,3), (0x17dc,0x17dc,30,19),
(0x17dd,0x17dd,30,23), (0x17e0,0x17e9,30,8), (0x17f0,0x17f9,30,14),
(0x1800,0x1801,31,2), (0x1802,0x1803,0,2), (0x1804,0x1804,31,2),
(0x1805,0x1805,0,2), (0x1806,0x1806,31,7), (0x1807,0x180a,31,2),
(0x180b,0x180d,31,23), (0x180e,0x180e,31,1), (0x1810,0x1819,31,8),
(0x1820,0x1842,31,19), (0x1843,0x1843,31,17), (0x1844,0x1877,31,19),
(0x1880,0x18a8,31,19), (0x18a9,0x18a9,31,23), (0x18aa,0x18aa,31,19),
(0x18b0,0x18f5,27,19), (0x1900,0x191c,45,19), (0x1920,0x1922,45,23),
(0x1923,0x1926,45,18), (0x1927,0x1928,45,23), (0x1929,0x192b,45,18),
(0x1930,0x1931,45,18), (0x1932,0x1932,45,23), (0x1933,0x1938,45,18),
(0x1939,0x193b,45,23), (0x1940,0x1940,45,11), (0x1944,0x1945,45,2),
(0x1946,0x194f,45,8), (0x1950,0x196d,46,19), (0x1970,0x1974,46,19),
(0x1980,0x19ab,55,19), (0x19b0,0x19c0,55,18), (0x19c1,0x19c7,55,19),
(0x19c8,0x19c9,55,18), (0x19d0,0x19d9,55,8), (0x19da,0x19da,55,14),
(0x19de,0x19df,55,11), (0x19e0,0x19ff,30,11), (0x1a00,0x1a16,53,19),
(0x1a17,0x1a18,53,23), (0x1a19,0x1a1b,53,18), (0x1a1e,0x1a1f,53,2),
(0x1a20,0x1a54,77,19), (0x1a55,0x1a55,77,18), (0x1a56,0x1a56,77,23),
(0x1a57,0x1a57,77,18), (0x1a58,0x1a5e,77,23), (0x1a60,0x1a60,77,23),
(0x1a61,0x1a61,77,18), (0x1a62,0x1a62,77,23), (0x1a63,0x1a64,77,18),
(0x1a65,0x1a6c,77,23), (0x1a6d,0x1a72,77,18), (0x1a73,0x1a7c,77,23),
(0x1a7f,0x1a7f,77,23), (0x1a80,0x1a89,77,8), (0x1a90,0x1a99,77,8),
(0x1aa0,0x1aa6,77,2), (0x1aa7,0x1aa7,77,17), (0x1aa8,0x1aad,77,2),
(0x1b00,0x1b03,61,23), (0x1b04,0x1b04,61,18), (0x1b05,0x1b33,61,19),
(0x1b34,0x1b34,61,23), (0x1b35,0x1b35,61,18), (0x1b36,0x1b3a,61,23),
(0x1b3b,0x1b3b,61,18), (0x1b3c,0x1b3c,61,23), (0x1b3d,0x1b41,61,18),
(0x1b42,0x1b42,61,23), (0x1b43,0x1b44,61,18), (0x1b45,0x1b4b,61,19),
(0x1b50,0x1b59,61,8), (0x1b5a,0x1b60,61,2), (0x1b61,0x1b6a,61,11),
(0x1b6b,0x1b73,61,23), (0x1b74,0x1b7c,61,11), (0x1b80,0x1b81,66,23),
(0x1b82,0x1b82,66,18), (0x1b83,0x1ba0,66,19), (0x1ba1,0x1ba1,66,18),
(0x1ba2,0x1ba5,66,23), (0x1ba6,0x1ba7,66,18), (0x1ba8,0x1ba9,66,23),
(0x1baa,0x1baa,66,18), (0x1bab,0x1bab,66,23), (0x1bac,0x1bad,66,18),
(0x1bae,0x1baf,66,19), (0x1bb0,0x1bb9,66,8), (0x1bba,0x1bbf,66,19),
(0x1bc0,0x1be5,92,19), (0x1be6,0x1be6,92,23), (0x1be7,0x1be7,92,18),
(0x1be8,0x1be9,92,23), (0x1bea,0x1bec,92,18), (0x1bed,0x1bed,92,23),
(0x1bee,0x1bee,92,18), (0x1bef,0x1bf1,92,23), (0x1bf2,0x1bf3,92,18),
(0x1bfc,0x1bff,92,2), (0x1c00,0x1c23,67,19), (0x1c24,0x1c2b,67,18),
(0x1c2c,0x1c33,67,23), (0x1c34,0x1c35,67,18), (0x1c36,0x1c37,67,23),
(0x1c3b,0x1c3f,67,2), (0x1c40,0x1c49,67,8), (0x1c4d,0x1c4f,67,19),
(0x1c50,0x1c59,68,8), (0x1c5a,0x1c77,68,19), (0x1c78,0x1c7d,68,17),
(0x1c7e,0x1c7f,68,2), (0x1cc0,0x1cc7,66,2), (0x1cd0,0x1cd2,40,23),
(0x1cd3,0x1cd3,0,2), (0x1cd4,0x1ce0,40,23), (0x1ce1,0x1ce1,0,18),
(0x1ce2,0x1ce8,40,23), (0x1ce9,0x1cec,0,19), (0x1ced,0x1ced,40,23),
(0x1cee,0x1cf1,0,19), (0x1cf2,0x1cf3,0,18), (0x1cf4,0x1cf4,40,23),
(0x1cf5,0x1cf6,0,19), (0x1d00,0x1d25,1,15), (0x1d26,0x1d2a,2,15),
(0x1d2b,0x1d2b,3,15), (0x1d2c,0x1d5c,1,17), (0x1d5d,0x1d61,2,17),
(0x1d62,0x1d65,1,17), (0x1d66,0x1d6a,2,17), (0x1d6b,0x1d77,1,15),
(0x1d78,0x1d78,3,17), (0x1d79,0x1d9a,1,15), (0x1d9b,0x1dbe,1,17),
(0x1dbf,0x1dbf,2,17), (0x1dc0,0x1de6,40,23), (0x1dfc,0x1dff,40,23),
(0x1e00,0x1eff,1,15), (0x1f00,0x1f15,2,15), (0x1f18,0x1f1d,2,15),
(0x1f20,0x1f45,2,15), (0x1f48,0x1f4d,2,15), (0x1f50,0x1f57,2,15),
(0x1f59,0x1f59,2,15), (0x1f5b,0x1f5b,2,15), (0x1f5d,0x1f5d,2,15),
(0x1f5f,0x1f7d,2,15), (0x1f80,0x1fb4,2,15), (0x1fb6,0x1fbc,2,15),
(0x1fbd,0x1fbd,2,9), (0x1fbe,0x1fbe,2,15), (0x1fbf,0x1fc1,2,9),
(0x1fc2,0x1fc4,2,15), (0x1fc6,0x1fcc,2,15), (0x1fcd,0x1fcf,2,9),
(0x1fd0,0x1fd3,2,15), (0x1fd6,0x1fdb,2,15), (0x1fdd,0x1fdf,2,9),
(0x1fe0,0x1fec,2,15), (0x1fed,0x1fef,2,9), (0x1ff2,0x1ff4,2,15),
(0x1ff6,0x1ffc,2,15), (0x1ffd,0x1ffe,2,9), (0x2000,0x200a,0,1),
(0x200b,0x200b,0,13), (0x200c,0x200d,40,13), (0x200e,0x200f,0,13),
(0x2010,0x2015,0,7), (0x2016,0x2017,0,2), (0x2018,0x2018,0,12),
(0x2019,0x2019,0,16), (0x201a,0x201a,0,4), (0x201b,0x201c,0,12),
(0x201d,0x201d,0,16), (0x201e,0x201e,0,4), (0x201f,0x201f,0,12),
(0x2020,0x2027,0,2), (0x2028,0x2028,0,20), (0x2029,0x2029,0,21),
(0x202a,0x202e,0,13), (0x202f,0x202f,0,1), (0x2030,0x2038,0,2),
(0x2039,0x2039,0,12), (0x203a,0x203a,0,16), (0x203b,0x203e,0,2),
(0x203f,0x2040,0,10), (0x2041,0x2043,0,2), (0x2044,0x2044,0,6),
(0x2045,0x2045,0,4), (0x2046,0x2046,0,5), (0x2047,0x2051,0,2),
(0x2052,0x2052,0,6), (0x2053,0x2053,0,2), (0x2054,0x2054,0,10),
(0x2055,0x205e,0,2), (0x205f,0x205f,0,1), (0x2060,0x2064,0,13),
(0x206a,0x206f,0,13), (0x2070,0x2070,0,14), (0x2071,0x2071,1,17),
(0x2074,0x2079,0,14), (0x207a,0x207c,0,6), (0x207d,0x207d,0,4),
(0x207e,0x207e,0,5), (0x207f,0x207f,1,17), (0x2080,0x2089,0,14),
(0x208a,0x208c,0,6), (0x208d,0x208d,0,4), (0x208e,0x208e,0,5),
(0x2090,0x209c,1,17), (0x20a0,0x20b9,0,3), (0x20d0,0x20dc,40,23),
(0x20dd,0x20e0,40,24), (0x20e1,0x20e1,40,23), (0x20e2,0x20e4,40,24),
(0x20e5,0x20f0,40,23), (0x2100,0x2101,0,11), (0x2102,0x2102,0,15),
(0x2103,0x2106,0,11), (0x2107,0x2107,0,15), (0x2108,0x2109,0,11),
(0x210a,0x2113,0,15), (0x2114,0x2114,0,11), (0x2115,0x2115,0,15),
(0x2116,0x2117,0,11), (0x2118,0x2118,0,6), (0x2119,0x211d,0,15),
(0x211e,0x2123,0,11), (0x2124,0x2124,0,15), (0x2125,0x2125,0,11),
(0x2126,0x2126,2,15), (0x2127,0x2127,0,11), (0x2128,0x2128,0,15),
(0x2129,0x2129,0,11), (0x212a,0x212b,1,15), (0x212c,0x212d,0,15),
(0x212e,0x212e,0,11), (0x212f,0x2131,0,15), (0x2132,0x2132,1,15),
(0x2133,0x2134,0,15), (0x2135,0x2138,0,19), (0x2139,0x2139,0,15),
(0x213a,0x213b,0,11), (0x213c,0x213f,0,15), (0x2140,0x2144,0,6),
(0x2145,0x2149,0,15), (0x214a,0x214a,0,11), (0x214b,0x214b,0,6),
(0x214c,0x214d,0,11), (0x214e,0x214e,1,15), (0x214f,0x214f,0,11),
(0x2150,0x215f,0,14), (0x2160,0x2182,1,22), (0x2183,0x2184,1,15),
(0x2185,0x2188,1,22), (0x2189,0x2189,0,14), (0x2190,0x2194,0,6),
(0x2195,0x2199,0,11), (0x219a,0x219b,0,6), (0x219c,0x219f,0,11),
(0x21a0,0x21a0,0,6), (0x21a1,0x21a2,0,11), (0x21a3,0x21a3,0,6),
(0x21a4,0x21a5,0,11), (0x21a6,0x21a6,0,6), (0x21a7,0x21ad,0,11),
(0x21ae,0x21ae,0,6), (0x21af,0x21cd,0,11), (0x21ce,0x21cf,0,6),
(0x21d0,0x21d1,0,11), (0x21d2,0x21d2,0,6), (0x21d3,0x21d3,0,11),
(0x21d4,0x21d4,0,6), (0x21d5,0x21f3,0,11), (0x21f4,0x22ff,0,6),
(0x2300,0x2307,0,11), (0x2308,0x230b,0,6), (0x230c,0x231f,0,11),
(0x2320,0x2321,0,6), (0x2322,0x2328,0,11), (0x2329,0x2329,0,4),
(0x232a,0x232a,0,5), (0x232b,0x237b,0,11), (0x237c,0x237c,0,6),
(0x237d,0x239a,0,11), (0x239b,0x23b3,0,6), (0x23b4,0x23db,0,11),
(0x23dc,0x23e1,0,6), (0x23e2,0x23f3,0,11), (0x2400,0x2426,0,11),
(0x2440,0x244a,0,11), (0x2460,0x249b,0,14), (0x249c,0x24e9,0,11),
(0x24ea,0x24ff,0,14), (0x2500,0x25b6,0,11), (0x25b7,0x25b7,0,6),
(0x25b8,0x25c0,0,11), (0x25c1,0x25c1,0,6), (0x25c2,0x25f7,0,11),
(0x25f8,0x25ff,0,6), (0x2600,0x266e,0,11), (0x266f,0x266f,0,6),
(0x2670,0x26ff,0,11), (0x2701,0x2767,0,11), (0x2768,0x2768,0,4),
(0x2769,0x2769,0,5), (0x276a,0x276a,0,4), (0x276b,0x276b,0,5),
(0x276c,0x276c,0,4), (0x276d,0x276d,0,5), (0x276e,0x276e,0,4),
(0x276f,0x276f,0,5), (0x2770,0x2770,0,4), (0x2771,0x2771,0,5),
(0x2772,0x2772,0,4), (0x2773,0x2773,0,5), (0x2774,0x2774,0,4),
(0x2775,0x2775,0,5), (0x2776,0x2793,0,14), (0x2794,0x27bf,0,11),
(0x27c0,0x27c4,0,6), (0x27c5,0x27c5,0,4), (0x27c6,0x27c6,0,5),
(0x27c7,0x27e5,0,6), (0x27e6,0x27e6,0,4), (0x27e7,0x27e7,0,5),
(0x27e8,0x27e8,0,4), (0x27e9,0x27e9,0,5), (0x27ea,0x27ea,0,4),
(0x27eb,0x27eb,0,5), (0x27ec,0x27ec,0,4), (0x27ed,0x27ed,0,5),
(0x27ee,0x27ee,0,4), (0x27ef,0x27ef,0,5), (0x27f0,0x27ff,0,6),
(0x2800,0x28ff,52,11), (0x2900,0x2982,0,6), (0x2983,0x2983,0,4),
(0x2984,0x2984,0,5), (0x2985,0x2985,0,4), (0x2986,0x2986,0,5),
(0x2987,0x2987,0,4), (0x2988,0x2988,0,5), (0x2989,0x2989,0,4),
(0x298a,0x298a,0,5), (0x298b,0x298b,0,4), (0x298c,0x298c,0,5),
(0x298d,0x298d,0,4), (0x298e,0x298e,0,5), (0x298f,0x298f,0,4),
(0x2990,0x2990,0,5), (0x2991,0x2991,0,4), (0x2992,0x2992,0,5),
(0x2993,0x2993,0,4), (0x2994,0x2994,0,5), (0x2995,0x2995,0,4),
(0x2996,0x2996,0,5), (0x2997,0x2997,0,4), (0x2998,0x2998,0,5),
(0x2999,0x29d7,0,6), (0x29d8,0x29d8,0,4), (0x29d9,0x29d9,0,5),
(0x29da,0x29da,0,4), (0x29db,0x29db,0,5), (0x29dc,0x29fb,0,6),
(0x29fc,0x29fc,0,4), (0x29fd,0x29fd,0,5), (0x29fe,0x2aff,0,6),
(0x2b00,0x2b2f,0,11), (0x2b30,0x2b44,0,6), (0x2b45,0x2b46,0,11),
(0x2b47,0x2b4c,0,6), (0x2b50,0x2b59,0,11), (0x2c00,0x2c2e,56,15),
(0x2c30,0x2c5e,56,15), (0x2c60,0x2c7b,1,15), (0x2c7c,0x2c7d,1,17),
(0x2c7e,0x2c7f,1,15), (0x2c80,0x2ce4,54,15), (0x2ce5,0x2cea,54,11),
(0x2ceb,0x2cee,54,15), (0x2cef,0x2cf1,54,23), (0x2cf2,0x2cf3,54,15),
(0x2cf9,0x2cfc,54,2), (0x2cfd,0x2cfd,54,14), (0x2cfe,0x2cff,54,2),
(0x2d00,0x2d25,23,15), (0x2d27,0x2d27,23,15), (0x2d2d,0x2d2d,23,15),
(0x2d30,0x2d67,57,19), (0x2d6f,0x2d6f,57,17), (0x2d70,0x2d70,57,2),
(0x2d7f,0x2d7f,57,23), (0x2d80,0x2d96,25,19), (0x2da0,0x2da6,25,19),
(0x2da8,0x2dae,25,19), (0x2db0,0x2db6,25,19), (0x2db8,0x2dbe,25,19),
(0x2dc0,0x2dc6,25,19), (0x2dc8,0x2dce,25,19), (0x2dd0,0x2dd6,25,19),
(0x2dd8,0x2dde,25,19), (0x2de0,0x2dff,3,23), (0x2e00,0x2e01,0,2),
(0x2e02,0x2e02,0,12), (0x2e03,0x2e03,0,16), (0x2e04,0x2e04,0,12),
(0x2e05,0x2e05,0,16), (0x2e06,0x2e08,0,2), (0x2e09,0x2e09,0,12),
(0x2e0a,0x2e0a,0,16), (0x2e0b,0x2e0b,0,2), (0x2e0c,0x2e0c,0,12),
(0x2e0d,0x2e0d,0,16), (0x2e0e,0x2e16,0,2), (0x2e17,0x2e17,0,7),
(0x2e18,0x2e19,0,2), (0x2e1a,0x2e1a,0,7), (0x2e1b,0x2e1b,0,2),
(0x2e1c,0x2e1c,0,12), (0x2e1d,0x2e1d,0,16), (0x2e1e,0x2e1f,0,2),
(0x2e20,0x2e20,0,12), (0x2e21,0x2e21,0,16), (0x2e22,0x2e22,0,4),
(0x2e23,0x2e23,0,5), (0x2e24,0x2e24,0,4), (0x2e25,0x2e25,0,5),
(0x2e26,0x2e26,0,4), (0x2e27,0x2e27,0,5), (0x2e28,0x2e28,0,4),
(0x2e29,0x2e29,0,5), (0x2e2a,0x2e2e,0,2), (0x2e2f,0x2e2f,0,17),
(0x2e30,0x2e39,0,2), (0x2e3a,0x2e3b,0,7), (0x2e80,0x2e99,35,11),
(0x2e9b,0x2ef3,35,11), (0x2f00,0x2fd5,35,11), (0x2ff0,0x2ffb,0,11),
(0x3000,0x3000,0,1), (0x3001,0x3003,0,2), (0x3004,0x3004,0,11),
(0x3005,0x3005,35,17), (0x3006,0x3006,0,19), (0x3007,0x3007,35,22),
(0x3008,0x3008,0,4), (0x3009,0x3009,0,5), (0x300a,0x300a,0,4),
(0x300b,0x300b,0,5), (0x300c,0x300c,0,4), (0x300d,0x300d,0,5),
(0x300e,0x300e,0,4), (0x300f,0x300f,0,5), (0x3010,0x3010,0,4),
(0x3011,0x3011,0,5), (0x3012,0x3013,0,11), (0x3014,0x3014,0,4),
(0x3015,0x3015,0,5), (0x3016,0x3016,0,4), (0x3017,0x3017,0,5),
(0x3018,0x3018,0,4), (0x3019,0x3019,0,5), (0x301a,0x301a,0,4),
(0x301b,0x301b,0,5), (0x301c,0x301c,0,7), (0x301d,0x301d,0,4),
(0x301e,0x301f,0,5), (0x3020,0x3020,0,11), (0x3021,0x3029,35,22),
(0x302a,0x302d,40,23), (0x302e,0x302f,24,18), (0x3030,0x3030,0,7),
(0x3031,0x3035,0,17), (0x3036,0x3037,0,11), (0x3038,0x303a,35,22),
(0x303b,0x303b,35,17), (0x303c,0x303c,0,19), (0x303d,0x303d,0,2),
(0x303e,0x303f,0,11), (0x3041,0x3096,32,19), (0x3099,0x309a,40,23),
(0x309b,0x309c,0,9), (0x309d,0x309e,32,17), (0x309f,0x309f,32,19),
(0x30a0,0x30a0,0,7), (0x30a1,0x30fa,33,19), (0x30fb,0x30fb,0,2),
(0x30fc,0x30fc,0,17), (0x30fd,0x30fe,33,17), (0x30ff,0x30ff,33,19),
(0x3105,0x312d,34,19), (0x3131,0x318e,24,19), (0x3190,0x3191,0,11),
(0x3192,0x3195,0,14), (0x3196,0x319f,0,11), (0x31a0,0x31ba,34,19),
(0x31c0,0x31e3,0,11), (0x31f0,0x31ff,33,19), (0x3200,0x321e,24,11),
(0x3220,0x3229,0,14), (0x322a,0x3247,0,11), (0x3248,0x324f,0,14),
(0x3250,0x3250,0,11), (0x3251,0x325f,0,14), (0x3260,0x327e,24,11),
(0x327f,0x327f,0,11), (0x3280,0x3289,0,14), (0x328a,0x32b0,0,11),
(0x32b1,0x32bf,0,14), (0x32c0,0x32cf,0,11), (0x32d0,0x32fe,33,11),
(0x3300,0x3357,33,11), (0x3358,0x33ff,0,11), (0x3400,0x4db5,35,19),
(0x4dc0,0x4dff,0,11), (0x4e00,0x9fcc,35,19), (0xa000,0xa014,36,19),
(0xa015,0xa015,36,17), (0xa016,0xa48c,36,19), (0xa490,0xa4c6,36,11),
(0xa4d0,0xa4f7,82,19), (0xa4f8,0xa4fd,82,17), (0xa4fe,0xa4ff,82,2),
(0xa500,0xa60b,69,19), (0xa60c,0xa60c,69,17), (0xa60d,0xa60f,69,2),
(0xa610,0xa61f,69,19), (0xa620,0xa629,69,8), (0xa62a,0xa62b,69,19),
(0xa640,0xa66d,3,15), (0xa66e,0xa66e,3,19), (0xa66f,0xa66f,3,23),
(0xa670,0xa672,3,24), (0xa673,0xa673,3,2), (0xa674,0xa67d,3,23),
(0xa67e,0xa67e,3,2), (0xa67f,0xa67f,3,17), (0xa680,0xa697,3,15),
(0xa69f,0xa69f,3,23), (0xa6a0,0xa6e5,83,19), (0xa6e6,0xa6ef,83,22),
(0xa6f0,0xa6f1,83,23), (0xa6f2,0xa6f7,83,2), (0xa700,0xa716,0,9),
(0xa717,0xa71f,0,17), (0xa720,0xa721,0,9), (0xa722,0xa76f,1,15),
(0xa770,0xa770,1,17), (0xa771,0xa787,1,15), (0xa788,0xa788,0,17),
(0xa789,0xa78a,0,9), (0xa78b,0xa78e,1,15), (0xa790,0xa793,1,15),
(0xa7a0,0xa7aa,1,15), (0xa7f8,0xa7f9,1,17), (0xa7fa,0xa7fa,1,15),
(0xa7fb,0xa7ff,1,19), (0xa800,0xa801,58,19), (0xa802,0xa802,58,23),
(0xa803,0xa805,58,19), (0xa806,0xa806,58,23), (0xa807,0xa80a,58,19),
(0xa80b,0xa80b,58,23), (0xa80c,0xa822,58,19), (0xa823,0xa824,58,18),
(0xa825,0xa826,58,23), (0xa827,0xa827,58,18), (0xa828,0xa82b,58,11),
(0xa830,0xa835,0,14), (0xa836,0xa837,0,11), (0xa838,0xa838,0,3),
(0xa839,0xa839,0,11), (0xa840,0xa873,64,19), (0xa874,0xa877,64,2),
(0xa880,0xa881,70,18), (0xa882,0xa8b3,70,19), (0xa8b4,0xa8c3,70,18),
(0xa8c4,0xa8c4,70,23), (0xa8ce,0xa8cf,70,2), (0xa8d0,0xa8d9,70,8),
(0xa8e0,0xa8f1,9,23), (0xa8f2,0xa8f7,9,19), (0xa8f8,0xa8fa,9,2),
(0xa8fb,0xa8fb,9,19), (0xa900,0xa909,71,8), (0xa90a,0xa925,71,19),
(0xa926,0xa92d,71,23), (0xa92e,0xa92f,71,2), (0xa930,0xa946,72,19),
(0xa947,0xa951,72,23), (0xa952,0xa953,72,18), (0xa95f,0xa95f,72,2),
(0xa960,0xa97c,24,19), (0xa980,0xa982,84,23), (0xa983,0xa983,84,18),
(0xa984,0xa9b2,84,19), (0xa9b3,0xa9b3,84,23), (0xa9b4,0xa9b5,84,18),
(0xa9b6,0xa9b9,84,23), (0xa9ba,0xa9bb,84,18), (0xa9bc,0xa9bc,84,23),
(0xa9bd,0xa9c0,84,18), (0xa9c1,0xa9cd,84,2), (0xa9cf,0xa9cf,84,17),
(0xa9d0,0xa9d9,84,8), (0xa9de,0xa9df,84,2), (0xaa00,0xaa28,76,19),
(0xaa29,0xaa2e,76,23), (0xaa2f,0xaa30,76,18), (0xaa31,0xaa32,76,23),
(0xaa33,0xaa34,76,18), (0xaa35,0xaa36,76,23), (0xaa40,0xaa42,76,19),
(0xaa43,0xaa43,76,23), (0xaa44,0xaa4b,76,19), (0xaa4c,0xaa4c,76,23),
(0xaa4d,0xaa4d,76,18), (0xaa50,0xaa59,76,8), (0xaa5c,0xaa5f,76,2),
(0xaa60,0xaa6f,22,19), (0xaa70,0xaa70,22,17), (0xaa71,0xaa76,22,19),
(0xaa77,0xaa79,22,11), (0xaa7a,0xaa7a,22,19), (0xaa7b,0xaa7b,22,18),
(0xaa80,0xaaaf,78,19), (0xaab0,0xaab0,78,23), (0xaab1,0xaab1,78,19),
(0xaab2,0xaab4,78,23), (0xaab5,0xaab6,78,19), (0xaab7,0xaab8,78,23),
(0xaab9,0xaabd,78,19), (0xaabe,0xaabf,78,23), (0xaac0,0xaac0,78,19),
(0xaac1,0xaac1,78,23), (0xaac2,0xaac2,78,19), (0xaadb,0xaadc,78,19),
(0xaadd,0xaadd,78,17), (0xaade,0xaadf,78,2), (0xaae0,0xaaea,85,19),
(0xaaeb,0xaaeb,85,18), (0xaaec,0xaaed,85,23), (0xaaee,0xaaef,85,18),
(0xaaf0,0xaaf1,85,2), (0xaaf2,0xaaf2,85,19), (0xaaf3,0xaaf4,85,17),
(0xaaf5,0xaaf5,85,18), (0xaaf6,0xaaf6,85,23), (0xab01,0xab06,25,19),
(0xab09,0xab0e,25,19), (0xab11,0xab16,25,19), (0xab20,0xab26,25,19),
(0xab28,0xab2e,25,19), (0xabc0,0xabe2,85,19), (0xabe3,0xabe4,85,18),
(0xabe5,0xabe5,85,23), (0xabe6,0xabe7,85,18), (0xabe8,0xabe8,85,23),
(0xabe9,0xabea,85,18), (0xabeb,0xabeb,85,2), (0xabec,0xabec,85,18),
(0xabed,0xabed,85,23), (0xabf0,0xabf9,85,8), (0xac00,0xd7a3,24,19),
(0xd7b0,0xd7c6,24,19), (0xd7cb,0xd7fb,24,19), (0xf900,0xfa6d,35,19),
(0xfa70,0xfad9,35,19), (0xfb00,0xfb06,1,15), (0xfb13,0xfb17,4,15),
(0xfb1d,0xfb1d,5,19), (0xfb1e,0xfb1e,5,23), (0xfb1f,0xfb28,5,19),
(0xfb29,0xfb29,5,6), (0xfb2a,0xfb36,5,19), (0xfb38,0xfb3c,5,19),
(0xfb3e,0xfb3e,5,19), (0xfb40,0xfb41,5,19), (0xfb43,0xfb44,5,19),
(0xfb46,0xfb4f,5,19), (0xfb50,0xfbb1,6,19), (0xfbb2,0xfbc1,6,9),
(0xfbd3,0xfd3d,6,19), (0xfd3e,0xfd3e,0,4), (0xfd3f,0xfd3f,0,5),
(0xfd50,0xfd8f,6,19), (0xfd92,0xfdc7,6,19), (0xfdf0,0xfdfb,6,19),
(0xfdfc,0xfdfc,6,3), (0xfdfd,0xfdfd,0,11), (0xfe00,0xfe0f,40,23),
(0xfe10,0xfe16,0,2), (0xfe17,0xfe17,0,4), (0xfe18,0xfe18,0,5),
(0xfe19,0xfe19,0,2), (0xfe20,0xfe26,40,23), (0xfe30,0xfe30,0,2),
(0xfe31,0xfe32,0,7), (0xfe33,0xfe34,0,10), (0xfe35,0xfe35,0,4),
(0xfe36,0xfe36,0,5), (0xfe37,0xfe37,0,4), (0xfe38,0xfe38,0,5),
(0xfe39,0xfe39,0,4), (0xfe3a,0xfe3a,0,5), (0xfe3b,0xfe3b,0,4),
(0xfe3c,0xfe3c,0,5), (0xfe3d,0xfe3d,0,4), (0xfe3e,0xfe3e,0,5),
(0xfe3f,0xfe3f,0,4), (0xfe40,0xfe40,0,5), (0xfe41,0xfe41,0,4),
(0xfe42,0xfe42,0,5), (0xfe43,0xfe43,0,4), (0xfe44,0xfe44,0,5),
(0xfe45,0xfe46,0,2), (0xfe47,0xfe47,0,4), (0xfe48,0xfe48,0,5),
(0xfe49,0xfe4c,0,2), (0xfe4d,0xfe4f,0,10), (0xfe50,0xfe52,0,2),
(0xfe54,0xfe57,0,2), (0xfe58,0xfe58,0,7), (0xfe59,0xfe59,0,4),
(0xfe5a,0xfe5a,0,5), (0xfe5b,0xfe5b,0,4), (0xfe5c,0xfe5c,0,5),
(0xfe5d,0xfe5d,0,4), (0xfe5e,0xfe5e,0,5), (0xfe5f,0xfe61,0,2),
(0xfe62,0xfe62,0,6), (0xfe63,0xfe63,0,7), (0xfe64,0xfe66,0,6),
(0xfe68,0xfe68,0,2), (0xfe69,0xfe69,0,3), (0xfe6a,0xfe6b,0,2),
(0xfe70,0xfe74,6,19), (0xfe76,0xfefc,6,19), (0xfeff,0xfeff,0,13),
(0xff01,0xff03,0,2), (0xff04,0xff04,0,3), (0xff05,0xff07,0,2),
(0xff08,0xff08,0,4), (0xff09,0xff09,0,5), (0xff0a,0xff0a,0,2),
(0xff0b,0xff0b,0,6), (0xff0c,0xff0c,0,2), (0xff0d,0xff0d,0,7),
(0xff0e,0xff0f,0,2), (0xff10,0xff19,0,8), (0xff1a,0xff1b,0,2),
(0xff1c,0xff1e,0,6), (0xff1f,0xff20,0,2), (0xff21,0xff3a,1,15),
(0xff3b,0xff3b,0,4), (0xff3c,0xff3c,0,2), (0xff3d,0xff3d,0,5),
(0xff3e,0xff3e,0,9), (0xff3f,0xff3f,0,10), (0xff40,0xff40,0,9),
(0xff41,0xff5a,1,15), (0xff5b,0xff5b,0,4), (0xff5c,0xff5c,0,6),
(0xff5d,0xff5d,0,5), (0xff5e,0xff5e,0,6), (0xff5f,0xff5f,0,4),
(0xff60,0xff60,0,5), (0xff61,0xff61,0,2), (0xff62,0xff62,0,4),
(0xff63,0xff63,0,5), (0xff64,0xff65,0,2), (0xff66,0xff6f,33,19),
(0xff70,0xff70,0,17), (0xff71,0xff9d,33,19), (0xff9e,0xff9f,0,17),
(0xffa0,0xffbe,24,19), (0xffc2,0xffc7,24,19), (0xffca,0xffcf,24,19),
(0xffd2,0xffd7,24,19), (0xffda,0xffdc,24,19), (0xffe0,0xffe1,0,3),
(0xffe2,0xffe2,0,6), (0xffe3,0xffe3,0,9), (0xffe4,0xffe4,0,11),
(0xffe5,0xffe6,0,3), (0xffe8,0xffe8,0,11), (0xffe9,0xffec,0,6),
(0xffed,0xffee,0,11), (0xfff9,0xfffb,0,13), (0xfffc,0xfffd,0,11),
(0x10000,0x1000b,47,19), (0x1000d,0x10026,47,19), (0x10028,0x1003a,47,19),
(0x1003c,0x1003d,47,19), (0x1003f,0x1004d,47,19), (0x10050,0x1005d,47,19),
(0x10080,0x100fa,47,19), (0x10100,0x10102,0,2), (0x10107,0x10133,0,14),
(0x10137,0x1013f,0,11), (0x10140,0x10174,2,22), (0x10175,0x10178,2,14),
(0x10179,0x10189,2,11), (0x1018a,0x1018a,2,14), (0x10190,0x1019b,0,11),
(0x101d0,0x101fc,0,11), (0x101fd,0x101fd,40,23), (0x10280,0x1029c,73,19),
(0x102a0,0x102d0,74,19), (0x10300,0x1031e,37,19), (0x10320,0x10323,37,14),
(0x10330,0x10340,38,19), (0x10341,0x10341,38,22), (0x10342,0x10349,38,19),
(0x1034a,0x1034a,38,22), (0x10380,0x1039d,48,19), (0x1039f,0x1039f,48,2),
(0x103a0,0x103c3,59,19), (0x103c8,0x103cf,59,19), (0x103d0,0x103d0,59,2),
(0x103d1,0x103d5,59,22), (0x10400,0x1044f,39,15), (0x10450,0x1047f,49,19),
(0x10480,0x1049d,50,19), (0x104a0,0x104a9,50,8), (0x10800,0x10805,51,19),
(0x10808,0x10808,51,19), (0x1080a,0x10835,51,19), (0x10837,0x10838,51,19),
(0x1083c,0x1083c,51,19), (0x1083f,0x1083f,51,19), (0x10840,0x10855,86,19),
(0x10857,0x10857,86,2), (0x10858,0x1085f,86,14), (0x10900,0x10915,63,19),
(0x10916,0x1091b,63,14), (0x1091f,0x1091f,63,2), (0x10920,0x10939,75,19),
(0x1093f,0x1093f,75,2), (0x10980,0x1099f,97,19), (0x109a0,0x109b7,96,19),
(0x109be,0x109bf,96,19), (0x10a00,0x10a00,60,19), (0x10a01,0x10a03,60,23),
(0x10a05,0x10a06,60,23), (0x10a0c,0x10a0f,60,23), (0x10a10,0x10a13,60,19),
(0x10a15,0x10a17,60,19), (0x10a19,0x10a33,60,19), (0x10a38,0x10a3a,60,23),
(0x10a3f,0x10a3f,60,23), (0x10a40,0x10a47,60,14), (0x10a50,0x10a58,60,2),
(0x10a60,0x10a7c,87,19), (0x10a7d,0x10a7e,87,14), (0x10a7f,0x10a7f,87,2),
(0x10b00,0x10b35,79,19), (0x10b39,0x10b3f,79,2), (0x10b40,0x10b55,88,19),
(0x10b58,0x10b5f,88,14), (0x10b60,0x10b72,89,19), (0x10b78,0x10b7f,89,14),
(0x10c00,0x10c48,90,19), (0x10e60,0x10e7e,6,14), (0x11000,0x11000,93,18),
(0x11001,0x11001,93,23), (0x11002,0x11002,93,18), (0x11003,0x11037,93,19),
(0x11038,0x11046,93,23), (0x11047,0x1104d,93,2), (0x11052,0x11065,93,14),
(0x11066,0x1106f,93,8), (0x11080,0x11081,91,23), (0x11082,0x11082,91,18),
(0x11083,0x110af,91,19), (0x110b0,0x110b2,91,18), (0x110b3,0x110b6,91,23),
(0x110b7,0x110b8,91,18), (0x110b9,0x110ba,91,23), (0x110bb,0x110bc,91,2),
(0x110bd,0x110bd,91,13), (0x110be,0x110c1,91,2), (0x110d0,0x110e8,100,19),
(0x110f0,0x110f9,100,8), (0x11100,0x11102,95,23), (0x11103,0x11126,95,19),
(0x11127,0x1112b,95,23), (0x1112c,0x1112c,95,18), (0x1112d,0x11134,95,23),
(0x11136,0x1113f,95,8), (0x11140,0x11143,95,2), (0x11180,0x11181,99,23),
(0x11182,0x11182,99,18), (0x11183,0x111b2,99,19), (0x111b3,0x111b5,99,18),
(0x111b6,0x111be,99,23), (0x111bf,0x111c0,99,18), (0x111c1,0x111c4,99,19),
(0x111c5,0x111c8,99,2), (0x111d0,0x111d9,99,8), (0x11680,0x116aa,101,19),
(0x116ab,0x116ab,101,23), (0x116ac,0x116ac,101,18), (0x116ad,0x116ad,101,23),
(0x116ae,0x116af,101,18), (0x116b0,0x116b5,101,23), (0x116b6,0x116b6,101,18),
(0x116b7,0x116b7,101,23), (0x116c0,0x116c9,101,8), (0x12000,0x1236e,62,19),
(0x12400,0x12462,62,22), (0x12470,0x12473,62,2), (0x13000,0x1342e,80,19),
(0x16800,0x16a38,83,19), (0x16f00,0x16f44,98,19), (0x16f50,0x16f50,98,19),
(0x16f51,0x16f7e,98,18), (0x16f8f,0x16f92,98,23), (0x16f93,0x16f9f,98,17),
(0x1b000,0x1b000,33,19), (0x1b001,0x1b001,32,19), (0x1d000,0x1d0f5,0,11),
(0x1d100,0x1d126,0,11), (0x1d129,0x1d164,0,11), (0x1d165,0x1d166,0,18),
(0x1d167,0x1d169,40,23), (0x1d16a,0x1d16c,0,11), (0x1d16d,0x1d172,0,18),
(0x1d173,0x1d17a,0,13), (0x1d17b,0x1d182,40,23), (0x1d183,0x1d184,0,11),
(0x1d185,0x1d18b,40,23), (0x1d18c,0x1d1a9,0,11), (0x1d1aa,0x1d1ad,40,23),
(0x1d1ae,0x1d1dd,0,11), (0x1d200,0x1d241,2,11), (0x1d242,0x1d244,2,23),
(0x1d245,0x1d245,2,11), (0x1d300,0x1d356,0,11), (0x1d360,0x1d371,0,14),
(0x1d400,0x1d454,0,15), (0x1d456,0x1d49c,0,15), (0x1d49e,0x1d49f,0,15),
(0x1d4a2,0x1d4a2,0,15), (0x1d4a5,0x1d4a6,0,15), (0x1d4a9,0x1d4ac,0,15),
(0x1d4ae,0x1d4b9,0,15), (0x1d4bb,0x1d4bb,0,15), (0x1d4bd,0x1d4c3,0,15),
(0x1d4c5,0x1d505,0,15), (0x1d507,0x1d50a,0,15), (0x1d50d,0x1d514,0,15),
(0x1d516,0x1d51c,0,15), (0x1d51e,0x1d539,0,15), (0x1d53b,0x1d53e,0,15),
(0x1d540,0x1d544,0,15), (0x1d546,0x1d546,0,15), (0x1d54a,0x1d550,0,15),
(0x1d552,0x1d6a5,0,15), (0x1d6a8,0x1d6c0,0,15), (0x1d6c1,0x1d6c1,0,6),
(0x1d6c2,0x1d6da,0,15), (0x1d6db,0x1d6db,0,6), (0x1d6dc,0x1d6fa,0,15),
(0x1d6fb,0x1d6fb,0,6), (0x1d6fc,0x1d714,0,15), (0x1d715,0x1d715,0,6),
(0x1d716,0x1d734,0,15), (0x1d735,0x1d735,0,6), (0x1d736,0x1d74e,0,15),
(0x1d74f,0x1d74f,0,6), (0x1d750,0x1d76e,0,15), (0x1d76f,0x1d76f,0,6),
(0x1d770,0x1d788,0,15), (0x1d789,0x1d789,0,6), (0x1d78a,0x1d7a8,0,15),
(0x1d7a9,0x1d7a9,0,6), (0x1d7aa,0x1d7c2,0,15), (0x1d7c3,0x1d7c3,0,6),
(0x1d7c4,0x1d7cb,0,15), (0x1d7ce,0x1d7ff,0,8), (0x1ee00,0x1ee03,6,19),
(0x1ee05,0x1ee1f,6,19), (0x1ee21,0x1ee22,6,19), (0x1ee24,0x1ee24,6,19),
(0x1ee27,0x1ee27,6,19), (0x1ee29,0x1ee32,6,19), (0x1ee34,0x1ee37,6,19),
(0x1ee39,0x1ee39,6,19), (0x1ee3b,0x1ee3b,6,19), (0x1ee42,0x1ee42,6,19),
(0x1ee47,0x1ee47,6,19), (0x1ee49,0x1ee49,6,19), (0x1ee4b,0x1ee4b,6,19),
(0x1ee4d,0x1ee4f,6,19), (0x1ee51,0x1ee52,6,19), (0x1ee54,0x1ee54,6,19),
(0x1ee57,0x1ee57,6,19), (0x1ee59,0x1ee59,6,19), (0x1ee5b,0x1ee5b,6,19),
(0x1ee5d,0x1ee5d,6,19), (0x1ee5f,0x1ee5f,6,19), (0x1ee61,0x1ee62,6,19),
(0x1ee64,0x1ee64,6,19), (0x1ee67,0x1ee6a,6,19), (0x1ee6c,0x1ee72,6,19),
(0x1ee74,0x1ee77,6,19), (0x1ee79,0x1ee7c,6,19), (0x1ee7e,0x1ee7e,6,19),
(0x1ee80,0x1ee89,6,19), (0x1ee8b,0x1ee9b,6,19), (0x1eea1,0x1eea3,6,19),
(0x1eea5,0x1eea9,6,19), (0x1eeab,0x1eebb,6,19), (0x1eef0,0x1eef1,6,6),
(0x1f000,0x1f02b,0,11), (0x1f030,0x1f093,0,11), (0x1f0a0,0x1f0ae,0,11),
(0x1f0b1,0x1f0be,0,11), (0x1f0c1,0x1f0cf,0,11), (0x1f0d1,0x1f0df,0,11),
(0x1f100,0x1f10a,0,14), (0x1f110,0x1f12e,0,11), (0x1f130,0x1f16b,0,11),
(0x1f170,0x1f19a,0,11), (0x1f1e6,0x1f1ff,0,11), (0x1f200,0x1f200,32,11),
(0x1f201,0x1f202,0,11), (0x1f210,0x1f23a,0,11), (0x1f240,0x1f248,0,11),
(0x1f250,0x1f251,0,11), (0x1f300,0x1f320,0,11), (0x1f330,0x1f335,0,11),
(0x1f337,0x1f37c,0,11), (0x1f380,0x1f393,0,11), (0x1f3a0,0x1f3c4,0,11),
(0x1f3c6,0x1f3ca,0,11), (0x1f3e0,0x1f3f0,0,11), (0x1f400,0x1f43e,0,11),
(0x1f440,0x1f440,0,11), (0x1f442,0x1f4f7,0,11), (0x1f4f9,0x1f4fc,0,11),
(0x1f500,0x1f53d,0,11), (0x1f540,0x1f543,0,11), (0x1f550,0x1f567,0,11),
(0x1f5fb,0x1f640,0,11), (0x1f645,0x1f64f,0,11), (0x1f680,0x1f6c5,0,11),
(0x1f700,0x1f773,0,11), (0x20000,0x2a6d6,35,19), (0x2a700,0x2b734,35,19),
(0x2b740,0x2b81d,35,19), (0x2f800,0x2fa1d,35,19), (0xe0001,0xe0001,0,13),
(0xe0020,0xe007f,0,13), (0xe0100,0xe01ef,40,23)
]}

def script_cat(chr):
    """ For the unicode character chr return a tuple (Scriptname, Category). """
    l = 0
    r = len(script_data['idx']) - 1
    c = ord(chr)
    while r >= l:
        m = (l + r) >> 1
        if c < script_data['idx'][m][0]:
            r = m - 1
        elif c > script_data['idx'][m][1]:
            l = m + 1
        else:
            return (
                script_data['names'][script_data['idx'][m][2]], 
                script_data['cats'][script_data['idx'][m][3]])
    return 'Unknown', 'Zzzz'

def script(chr):
    a, _ = script_cat(chr)
    return a

def category(chr):
    _, a = script_cat(chr)
    return a

def _compile_scripts_txt():
    # build indexes from 'scripts.txt'

    idx = []
    names = []
    cats = []

    import urllib2, re, textwrap

    url = 'http://www.unicode.org/Public/UNIDATA/Scripts.txt'
    f = urllib2.urlopen(url)
    for ln in f:
        p = re.findall(r'([0-9A-F]+)(?:\.\.([0-9A-F]+))?\W+(\w+)\s*#\s*(\w+)', ln)
        if p:
            a, b, name, cat = p[0]
            if name not in names:
                names.append(name)
            if cat not in cats:
                cats.append(cat)
            idx.append((int(a, 16), int(b or a, 16), names.index(name), cats.index(cat)))
    idx.sort()

    print('script_data = {\n"names":%s,\n"cats":%s,\n"idx":[\n%s\n]}' % (
        '\n'.join(textwrap.wrap(repr(names), 80)),
        '\n'.join(textwrap.wrap(repr(cats), 80)),
        '\n'.join(textwrap.wrap(', '.join('(0x%x,0x%x,%d,%d)' % c for c in idx), 80))))        



File: lib/view/moon.py

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(parsed_query):

    location = parsed_query['orig_location']
    html = parsed_query['html_output']
    lang = parsed_query['lang']
    hemisphere = parsed_query['hemisphere']

    date = None
    if '@' in location:
        date = location[location.index('@')+1:]
        location = location[:location.index('@')]

    cmd = [globals.PYPHOON]
    if lang:
        cmd += ["-l", lang]

    if not hemisphere:
        cmd += ["-s", "south"]

    if date:
        try:
            dateutil.parser.parse(date)
        except Exception as e:
            print("ERROR: %s" % e)
        else:
            cmd += [date]

    p = Popen(cmd, stdout=PIPE, stderr=PIPE)
    stdout = p.communicate()[0]
    stdout = stdout.decode("utf-8")

    if parsed_query.get('no-terminal', False):
        stdout = globals.remove_ansi(stdout)

    if parsed_query.get('dumb', False):
        stdout = stdout.translate(globals.TRANSLATION_TABLE)

    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



File: lib/view/wttr.py

# vim: set encoding=utf-8
# pylint: disable=wrong-import-position

"""
Main view (wttr.in) implementation.
The module is a wrapper for the modified Wego program.
"""

import sys
import re

from gevent.subprocess import Popen, PIPE

sys.path.insert(0, "..")
from translations import get_message, SUPPORTED_LANGS
from globals import WEGO, TRANSLATION_TABLE, NOT_FOUND_LOCATION, \
                    DEFAULT_LOCATION, ANSI2HTML, error, remove_ansi


def get_wetter(parsed_query):

    location = parsed_query['location']
    html = parsed_query['html_output']
    lang = parsed_query['lang']

    location_not_found = False
    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)
        first_line, stdout = _wego_postprocessing(location, parsed_query, stdout)

    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(DEFAULT_LOCATION, parsed_query)
        location_not_found = True

        not_found_header = """
>>>    _  _    ___  _  _     
>>>   | || |  / _ \| || |      
>>>   | || |_| | | | || |_      
>>>   |__   _| |_| |__   _|      
>>>      |_|  \___/   |_|    
>>>                       
>>>   404 %s: %s
>>>                 
""" % (get_message("UNKNOWN_LOCATION", lang).upper(), parsed_query['override_location_name'])

        not_found_header = "\n".join("\033[48;5;91m" + x + "   \033[0m"
                                     for x in not_found_header.splitlines()[1:])

        not_found_footer = get_message('NOT_FOUND_MESSAGE', lang)
        not_found_footer = "\n".join("\033[48;5;91m " + x + " \033[0m"
                                     for x in not_found_footer.splitlines() if x) + "\n"

        first_line, stdout = _wego_postprocessing(location, parsed_query, stdout)
        stdout = not_found_header + "\n----\n" + stdout + not_found_footer

    if html:
        return _htmlize(stdout, first_line, parsed_query)
    return stdout

def _wego_wrapper(location, parsed_query):

    lang = parsed_query['lang']
    if location == DEFAULT_LOCATION:
        location_name = DEFAULT_LOCATION.capitalize()
    else:
        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 parsed_query.get('dumb', False):
        stdout = stdout.translate(TRANSLATION_TABLE)

    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(
            '<body class="">', '<body class="" style="background:white;color:#777777">')

    title = "<title>%s</title>" % title
    opengraph = _get_opengraph(parsed_query)
    stdout = re.sub("<head>", "<head>" + 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 (
        '<meta property="og:image" content="%(pic_url)s_0pq.png" />'
        '<meta property="og:site_name" content="wttr.in" />'
        '<meta property="og:type" content="profile" />'
        '<meta property="og:url" content="%(url)s" />'
    ) % {
        'pic_url': pic_url,
        'url': url,
    }



File: lib/view/line.py

#vim: fileencoding=utf-8

"""
One-line output mode.

Initial implementation of one-line output mode.

[ ] forecast
[ ] spark
[ ] several locations
[ ] location handling
[ ] more preconfigured format lines
[ ] add information about this mode to /:help
"""

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, WEATHER_SYMBOL_WI_NIGHT, WEATHER_SYMBOL_WI_DAY, WIND_DIRECTION, WIND_DIRECTION_WI, WEATHER_SYMBOL_WIDTH_VTE, WEATHER_SYMBOL_PLAIN
from weather_data import get_weather_data
from . import v2
from . import v3
from . import prometheus

PRECONFIGURED_FORMAT = {
    '1':    r'%c %t\n',
    '2':    r'%c 🌡️%t 🌬️%w\n',
    '3':    r'%l: %c %t\n',
    '4':    r'%l: %c 🌡️%t 🌬️%w\n',
    '69':   r'nice',
}

MOON_PHASES = (
    u"🌑", u"🌒", u"🌓", u"🌔", u"🌕", u"🌖", u"🌗", u"🌘"
)

def convert_to_fahrenheit(temp):
    "Convert Celcius `temp` to Fahrenheit"

    return (temp*9.0/5)+32

def render_temperature(data, query):
    """
    temperature (t)
    """

    if query.get('use_imperial', False):
        temperature = u'%s°F' % data['temp_F']
    else:
        temperature = u'%s°C' % data['temp_C']

    if temperature[0] != '-':
        temperature = '+' + temperature

    return temperature

def render_feel_like_temperature(data, query):
    """
    feel like temperature (f)
    """

    if query.get('use_imperial', False):
        temperature = u'%s°F' % data['FeelsLikeF']
    else:
        temperature = u'%s°C' % data['FeelsLikeC']

    if temperature[0] != '-':
        temperature = '+' + temperature

    return temperature

def render_condition(data, query):
    """Emoji encoded weather condition (c)
    """

    if query.get("view") == "v2n":
        weather_condition = WEATHER_SYMBOL_WI_NIGHT.get(
                WWO_CODE.get(
                    data['weatherCode'], "Unknown"))
        spaces = "  "
    elif query.get("view") == "v2d":
        weather_condition = WEATHER_SYMBOL_WI_DAY.get(
                WWO_CODE.get(
                    data['weatherCode'], "Unknown"))
        spaces = "  "
    else:
        weather_condition = WEATHER_SYMBOL.get(
                WWO_CODE.get(
                    data['weatherCode'], "Unknown"))
        spaces = " "*(3 - WEATHER_SYMBOL_WIDTH_VTE.get(weather_condition, 1))

    return weather_condition + spaces

def render_condition_fullname(data, query):
    """
    condition_fullname (C)
    """

    found = None
    for key, val in data.items():
        if key.startswith('lang_'):
            found = val
            break
    if not found:
        found = data['weatherDesc']

    try:
        weather_condition = found[0]['value']
    except KeyError:
        weather_condition = ''

    return weather_condition

def render_condition_plain(data, query):
    """Plain text weather condition (x)
    """

    weather_condition = WEATHER_SYMBOL_PLAIN[WWO_CODE[data['weatherCode']]]

    return weather_condition

def render_humidity(data, query):
    """
    humidity (h)
    """

    humidity = data.get('humidity', '')
    if humidity:
        humidity += '%'
    return humidity

def render_precipitation(data, query):
    """
    precipitation (p)
    """

    answer = data.get('precipMM', '')
    if answer:
        answer += 'mm'
    return answer

def render_precipitation_chance(data, query):
    """
    precipitation chance (o)
    """

    answer = data.get('chanceofrain', '')
    if answer:
        answer += '%'
    return answer

def render_pressure(data, query):
    """
    pressure (P)
    """

    answer = data.get('pressure', '')
    if answer:
        answer += 'hPa'
    return answer

def render_uv_index(data, query):
    """
    UV Index (u)
    """

    answer = data.get('uvIndex', '')
    return answer

def render_wind(data, query):
    """
    wind (w)
    """

    try:
        degree = data["winddirDegree"]
    except KeyError:
        degree = ""

    try:
        degree = int(degree)
    except ValueError:
        degree = ""

    if degree:
        if query.get("view") in ["v2n", "v2d"]:
            wind_direction = WIND_DIRECTION_WI[int(((degree+22.5)%360)/45.0)]
        else:
            wind_direction = WIND_DIRECTION[int(((degree+22.5)%360)/45.0)]
    else:
        wind_direction = ""

    if query.get('use_ms_for_wind', False):
        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'
        wind = u'%s%s%s' % (wind_direction, data['windspeedMiles'], unit)
    else:
        unit = 'km/h'
        wind = u'%s%s%s' % (wind_direction, data['windspeedKmph'], unit)

    return wind

def render_location(data, query):
    """
    location (l)
    """

    return (data['override_location'] or data['location'])

def render_moonphase(_, query):
    """moonpahse(m)
    A symbol describing the phase of the moon
    """
    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)
    """
    moon_phase = moon.phase(date=datetime.datetime.today())
    return str(int(moon_phase))

##################################
# this part should be rewritten
# this is just a temporary solution

def get_geodata(location):
    # text = requests.get("http://localhost:8004/%s" % location).text
    text = requests.get("http://127.0.0.1:8083/:geo-location?location=%s" % location).text
    return json.loads(text)


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")

def render_local_time(data, query, local_time_of):
    """local_time (T)
    Local time"""
    return "%{{NOW("+ local_time_of("TZ") +")}}"

def render_local_timezone(data, query, local_time_of):
    """local_time (Z)
    Local time"""
    return local_time_of("TZ")

##################################

FORMAT_SYMBOL = {
    'c':    render_condition,
    'C':    render_condition_fullname,
    'x':    render_condition_plain,
    'h':    render_humidity,
    't':    render_temperature,
    'f':    render_feel_like_temperature,
    'w':    render_wind,
    'l':    render_location,
    'm':    render_moonphase,
    'M':    render_moonday,
    'p':    render_precipitation,
    'o':    render_precipitation_chance,
    'P':    render_pressure,
    "u":    render_uv_index,
    }

FORMAT_SYMBOL_ASTRO = {
    'D':    render_dawn,
    'd':    render_dusk,
    'S':    render_sunrise,
    's':    render_sunset,
    'z':    render_zenith,

    'T':    render_local_time,
    'Z':    render_local_timezone,
}

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: city.timezone if x == "TZ" else \
                                     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`
        using `data` from external scope.
        """

        symbol_string = match.group(0)
        symbol = symbol_string[-1]

        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)

        return ''

    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, ensure_ascii=False)

    output = "\n".join(
        re.sub('"[^"]*worldweatheronline[^"]*"', '""', line) if "worldweatheronline" in line else line
        for line in output.splitlines()) + "\n"

    return output

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' % parsed_query["location"]

    format_line = parsed_query.get("view", "")
    if format_line in PRECONFIGURED_FORMAT:
        format_line = PRECONFIGURED_FORMAT[format_line]

    if format_line in ["j1", "j2"]:
        # j2 is a lightweight j1, without 'hourly' in 'weather' (weather forecast)
        if "weather" in data["data"] and format_line == "j2":
            for i in range(len(data["data"]["weather"])):
                del data["data"]["weather"][i]["hourly"]
        return render_json(data['data'])
    if format_line == "p1":
        return prometheus.render_prometheus(data['data'])
    if format_line[:2] == "v2":
        return v2.main(query, parsed_query, data)
    if format_line[:2] == "v3":
        return v3.main(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_name"]
    output = render_line(format_line, current_condition, query)
    output = output.rstrip("\n").replace(r"\n", "\n")
    return output

def wttr_line(query, parsed_query):
    """
    Return 1line weather information for `location`
    in format `line_format`
    """
    location = parsed_query['location']
    lang = parsed_query['lang']

    data = get_weather_data(location, lang)
    output = format_weather_data(query, parsed_query, data)
    return output

def main():
    """
    Function for standalone module usage
    """

    location = sys.argv[1]
    query = {
        'line': sys.argv[2],
        }
    parsed_query = {
        "location": location,
        "orig_location": location,
        "language": "en",
        "format": "v2",
        }

    sys.stdout.write(wttr_line(query, parsed_query))

if __name__ == '__main__':
    main()



File: lib/view/__init__.py




File: lib/view/prometheus.py

"""
Rendering weather data in the Prometheus format.

"""

from datetime import datetime

from fields import DESCRIPTION

def _render_current(data, for_day="current", already_seen=[]):
    "Converts data into prometheus style format"

    output = []

    for field_name, val in DESCRIPTION.items():

        help, name = val

        try:
            value = data[field_name]
            if field_name == "weatherDesc":
                value = value[0]["value"]
        except (IndexError, KeyError):
            try:
                value = data["astronomy"][0][field_name]
                if value.endswith(" AM") or value.endswith(" PM"):
                    value = _convert_time_to_minutes(value)
            except (IndexError, KeyError, ValueError):
                continue

        try:
            if name == "observation_time":
                value = _convert_time_to_minutes(value)
        except ValueError:
            continue

        description = ""
        try:
            float(value)
        except ValueError:
            description = f", description=\"{value}\""
            value = "1"

        if name not in already_seen:
            output.append(f"# HELP {name} {help}")
            already_seen.append(name)

        output.append(f"{name}{{forecast=\"{for_day}\"{description}}} {value}")

    return "\n".join(output)+"\n"

def _convert_time_to_minutes(time_str):
    "Convert time from midnight to minutes"
    return int((datetime.strptime(time_str, "%I:%M %p")
        - datetime.strptime("12:00 AM", "%I:%M %p")).total_seconds())//60

def render_prometheus(data):
    """
    Convert `data` into Prometheus format
    and return it as string.
    """

    already_seen = []
    answer = _render_current(
        data["current_condition"][0], already_seen=already_seen)
    for i in range(3):
        answer += _render_current(
            data["weather"][i], for_day="%sd" % i, already_seen=already_seen)
    return answer



File: lib/view/v2.py

# vim: fileencoding=utf-8
# vim: foldmethod=marker foldenable:

"""
[X] emoji
[ ] wego icon
[ ] v2.wttr.in
[X] astronomical (sunset)
[X] time
[X] frames
[X] colorize rain data
[ ] date + locales
[X] wind color
[ ] highlight current date
[ ] bind to real site
[ ] max values: temperature
[X] max value: rain
[ ] comment github
[ ] commit

"""

import sys

import re
import math
import json
import datetime
import io

import requests
import diagram
import pyjq
import pytz
import numpy as np
from astral import LocationInfo
from astral import moon, sun
from scipy.interpolate import interp1d
from babel.dates import format_datetime

from globals import WWO_KEY, TRANSLATION_TABLE, remove_ansi
import constants
import translations
import parse_query
from . import line as wttr_line

if not sys.version_info >= (3, 0):
    reload(sys) # noqa: F821
    sys.setdefaultencoding("utf-8")

# data processing {{{

def get_data(config):
    """
    Fetch data for `query_string`
    """

    url = (
        'http://'
        'localhost:5001/premium/v1/weather.ashx'
        '?key=%s'
        '&q=%s&format=json&num_of_days=3&tp=3&lang=None'
    ) % (WWO_KEY, config["location"])
    text = requests.get(url).text
    parsed_data = json.loads(text)
    return parsed_data

def interpolate_data(input_data, max_width):
    """
    Resample `input_data` to number of `max_width` counts
    """

    input_data = list(input_data)
    input_data_len = len(input_data)
    x = list(range(input_data_len))
    y = input_data
    xvals = np.linspace(0, input_data_len-1, max_width)
    yinterp = interp1d(x, y, kind='cubic')
    return yinterp(xvals)

def jq_query(query, data_parsed):
    """
    Apply `query` to structued data `data_parsed`
    """

    pyjq_data = pyjq.all(query, data_parsed)
    data = list(map(float, pyjq_data))
    return data

# }}}
# utils {{{
def colorize(string, color_code, html_output=False):
    if html_output:
        return "<font color='#777777'>%s</font>" % (string)
    else:
        return "\033[%sm%s\033[0m" % (color_code, string)
# }}}
# draw_spark {{{


def draw_spark(data, height, width, color_data):
    """
    Spark-style visualize `data` in a region `height` x `width`
    """

    _BARS = u' _▁▂▃▄▅▇█'

    def _box(height, row, value, max_value):
        row_height = 1.0 * max_value / height
        if row_height * row >= value:
            return _BARS[0]
        if row_height * (row+1) <= value:
            return _BARS[-1]

        return _BARS[int(1.0*(value - row_height*row)/(row_height*1.0)*len(_BARS))]

    max_value = max(data)

    output = ""
    color_code = 20
    for i in range(height):
        for j in range(width):
            character = _box(height, height-i-1, data[j], max_value)
            if data[j] != 0:
                chance_of_rain = color_data[j]/100.0 * 2
                if chance_of_rain > 1:
                    chance_of_rain = 1
                color_index = int(5*chance_of_rain)
                color_code = 16 + color_index # int(math.floor((20-16) * 1.0 * (height-1-i)/height*(max_value/data[j])))
            output += "\033[38;5;%sm%s\033[0m" % (color_code, character)
        output += "\n"

    # labeling max value
    if max_value == 0:
        max_line = " "*width
    else:
        max_line = ""
        for j in range(width):
            if data[j] == max_value:
                max_line = "%3.2fmm|%s%%" % (max_value, int(color_data[j]))
                orig_max_line = max_line

                # aligning it
                if len(max_line)//2 < j and len(max_line)//2 + j < width:
                    spaces = " "*(j - len(max_line)//2)
                    max_line = spaces + max_line # + spaces
                    max_line = max_line + " "*(width - len(max_line))
                elif len(max_line)//2 + j >= width:
                    max_line = " "*(width - len(max_line)) + max_line

                max_line = max_line.replace(orig_max_line, colorize(orig_max_line, "38;5;33"))

                break

    if max_line:
        output = "\n" + max_line + "\n" + output + "\n"

    return output

# }}}
# draw_diagram {{{
def draw_diagram(data, height, width):

    option = diagram.DOption()
    option.size = diagram.Point([width, height])
    option.mode = 'g'

    stream = io.BytesIO()
    gram = diagram.DGWrapper(
        data=[list(data), range(len(data))],
        dg_option=option,
        ostream=stream)
    gram.show()
    return stream.getvalue().decode("utf-8")
# }}}
# draw_date {{{


def draw_date(config, geo_data):
    """
    """

    tzinfo = pytz.timezone(geo_data["timezone"])

    locale = config.get("locale", "en_US")
    datetime_day_start = datetime.datetime.utcnow()

    answer = ""
    for day in range(3):
        datetime_ = datetime_day_start + datetime.timedelta(hours=24*day)
        date = format_datetime(datetime_, "EEE dd MMM", locale=locale, tzinfo=tzinfo)

        spaces = ((24-len(date))//2)*" "
        date = spaces + date + spaces
        date = " "*(24-len(date)) + date
        answer += date
    answer += "\n"

    for _ in range(3):
        answer += " "*23 + u"╷"
    return answer[:-1] + " "


# }}}
# draw_time {{{


def draw_time(geo_data):
    """
    """

    tzinfo = pytz.timezone(geo_data["timezone"])

    line = ["", ""]

    for _ in range(3):
        part = u"─"*5 + u"┴" + u"─"*5
        line[0] += part + u"┼" + part + u"╂"
    line[0] += "\n"

    for _ in range(3):
        line[1] += "     6    12    18      "
    line[1] += "\n"

    # highlight current time
    hour_number = \
        (datetime.datetime.now(tzinfo)
         - datetime.datetime.now(tzinfo).replace(hour=0, minute=0, second=0, microsecond=0)
        ).seconds//3600

    for line_number, _ in enumerate(line):
        line[line_number] = \
                line[line_number][:hour_number] \
                + colorize(line[line_number][hour_number], "46") \
                + line[line_number][hour_number+1:]

    return "".join(line)


# }}}
# draw_astronomical {{{
def draw_astronomical(city_name, geo_data, config):
    datetime_day_start = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)

    city = LocationInfo()
    city.latitude = geo_data["latitude"]
    city.longitude = geo_data["longitude"]
    city.timezone = geo_data["timezone"]

    answer = ""
    moon_line = ""
    for time_interval in range(72):

        current_date = (
            datetime_day_start
            + datetime.timedelta(hours=1*time_interval)).replace(tzinfo=pytz.timezone(geo_data["timezone"]))

        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:
            char = u"─"
        elif sunset <= current_date and current_date <= dusk:
            char = u"─"
        elif sunrise <= current_date and current_date <= sunset:
            char = u"━"

        answer += char

        if config.get("view") in ["v2n", "v2d"]:
            moon_phases = constants.MOON_PHASES_WI
            moon_phases = [" %s" % x for x in moon_phases]
        else:
            moon_phases = constants.MOON_PHASES

        # moon
        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 = moon_phases[
                int(math.floor(moon_phase*1.0/28.0*8+0.5)) % len(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"
    answer += "\n"
    return answer
# }}}
# draw_emoji {{{
def draw_emoji(data, config):
    answer = ""
    if config.get("view") == "v2n":
        weather_symbol = constants.WEATHER_SYMBOL_WI_NIGHT
        weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE_WI
    elif config.get("view") == "v2d":
        weather_symbol = constants.WEATHER_SYMBOL_WI_DAY
        weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE_WI
    else:
        weather_symbol = constants.WEATHER_SYMBOL
        weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE

    for i in data:
        emoji = weather_symbol.get(
            constants.WWO_CODE.get(
                str(int(i)), "Unknown"))
        space = " "*(3-weather_symbol_width_vte.get(emoji, 1))
        answer += space[:1] + emoji + space[1:]
    answer += "\n"
    return answer
# }}}
# draw_wind {{{
def draw_wind(data, color_data, config):

    def _color_code_for_wind_speed(wind_speed):

        color_codes = [
            (3,  241),  # 82
            (6,  242),  # 118
            (9,  243),  # 154
            (12, 246),  # 190
            (15, 250),  # 226
            (19, 253),  # 220
            (23, 214),
            (27, 208),
            (31, 202),
            (-1, 196)
        ]

        for this_wind_speed, this_color_code in color_codes:
            if wind_speed <= this_wind_speed:
                return this_color_code
        return color_codes[-1][1]

    answer = ""
    answer_line2 = ""

    if config.get("view") in ["v2n", "v2d"]:
        wind_direction_list = constants.WIND_DIRECTION_WI
    else:
        wind_direction_list = constants.WIND_DIRECTION

    for j, degree in enumerate(data):

        degree = int(degree)
        if degree:
            wind_direction = wind_direction_list[int(((degree+22.5)%360)/45.0)]
        else:
            wind_direction = ""

        color_code = "38;5;%s" % _color_code_for_wind_speed(int(color_data[j]))
        answer += " %s " % colorize(wind_direction, color_code)

        # wind_speed
        wind_speed = int(color_data[j])
        wind_speed_str = colorize(str(wind_speed), color_code)
        if wind_speed < 10:
            wind_speed_str = " " + wind_speed_str + " "
        elif wind_speed < 100:
            wind_speed_str = " " + wind_speed_str
        answer_line2 += wind_speed_str

    answer += "\n"
    answer += answer_line2 + "\n"
    return answer
# }}}
# panel implementation {{{

def add_frame(output, width, config):
    """
    Add frame arond `output` that has width `width`
    """

    empty_line = " "*width
    output = "\n".join(u"│"+(x or empty_line)+u"│" for x in output.splitlines()) + "\n"

    weather_report = \
        translations.CAPTION[config.get("lang") or  "en"] \
        + " " \
        + (config["override_location_name"] or config["location"])

    caption = u"┤ " + " " + weather_report + " " + u" ├"
    output = u"┌" + caption + u"─"*(width-len(caption)) + u"┐\n" \
                + output + \
             u"└" + u"─"*width + u"┘\n"

    return output

def generate_panel(data_parsed, geo_data, config):
    """
    """

    max_width = 72

    if config.get("use_imperial"):
        feels_like_query = "[.data.weather[] | .hourly[]] | .[].FeelsLikeF"
        temp_query = "[.data.weather[] | .hourly[]] | .[].tempF"
        wind_speed_query = "[.data.weather[] | .hourly[]] | .[].windspeedMiles"
    else:
        feels_like_query = "[.data.weather[] | .hourly[]] | .[].FeelsLikeC"
        temp_query = "[.data.weather[] | .hourly[]] | .[].tempC"
        wind_speed_query = "[.data.weather[] | .hourly[]] | .[].windspeedKmph"

    precip_mm_query = "[.data.weather[] | .hourly[]] | .[].precipMM"
    precip_chance_query = "[.data.weather[] | .hourly[]] | .[].chanceofrain"
    weather_code_query = "[.data.weather[] | .hourly[]] | .[].weatherCode"
    wind_direction_query = "[.data.weather[] | .hourly[]] | .[].winddirDegree"

    output = ""

    output += "\n\n"

    output += draw_date(config, geo_data)
    output += "\n"
    output += "\n"
    output += "\n"

    #data = jq_query(feels_like_query, data_parsed)
    data = jq_query(temp_query, data_parsed)
    data_interpolated = interpolate_data(data, max_width)
    output += draw_diagram(data_interpolated, 10, max_width)

    output += "\n"

    output += draw_time(geo_data)

    data = jq_query(precip_mm_query, data_parsed)
    color_data = jq_query(precip_chance_query, data_parsed)
    data_interpolated = interpolate_data(data, max_width)
    color_data_interpolated = interpolate_data(color_data, max_width)
    output += draw_spark(data_interpolated, 5, max_width, color_data_interpolated)
    output += "\n"

    data = jq_query(weather_code_query, data_parsed)
    output += draw_emoji(data, config)

    data = jq_query(wind_direction_query, data_parsed)
    color_data = jq_query(wind_speed_query, data_parsed)
    output += draw_wind(data, color_data, config)
    output += "\n"

    output += draw_astronomical(config["location"], geo_data, config)
    output += "\n"

    output = add_frame(output, max_width, config)
    return output


# }}}
# textual information {{{
def textual_information(data_parsed, geo_data, config, html_output=False):
    """
    Add textual information about current weather and
    astronomical conditions
    """

    def _shorten_full_location(full_location, city_only=False):

        def _count_runes(string):
            return len(string.encode('utf-16-le')) // 2

        words = full_location.split(",")

        output = words[0]
        if city_only:
            return output

        for word in words[1:]:
            if _count_runes(output + "," + word) > 50:
                return output
            output += "," + word

        return output

    def _colorize(text, color):
        return colorize(text, color, html_output=html_output)

    city = LocationInfo()
    city.latitude = geo_data["latitude"]
    city.longitude = geo_data["longitude"]
    city.timezone = geo_data["timezone"]

    output = []
    timezone = city.timezone

    datetime_day_start = datetime.datetime.now()\
            .replace(hour=0, minute=0, second=0, microsecond=0)

    format_line = "%c %C, %t, %h, %w, %P"
    current_condition = data_parsed['data']['current_condition'][0]
    query = config
    weather_line = wttr_line.render_line(format_line, current_condition, query)
    output.append('Weather: %s' % weather_line)

    output.append('Timezone: %s' % timezone)

    local_tz = pytz.timezone(timezone)

    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 = [
        re.sub("^([A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"), x)
        for x in tmp_output]

    output.append(
        "%20s" % tmp_output[0] \
        + " | %20s " % tmp_output[1] \
        + " | %20s" % tmp_output[2])
    output.append(
        "%20s" % tmp_output[3] \
        + " | %20s " % tmp_output[4] \
        + " | %20s" % tmp_output[5])

    city_only = False
    suffix = ""
    if "Simferopol" in timezone:
        city_only = True
        suffix = ", Крым"

    latitude = float(geo_data["latitude"])
    longitude = float(geo_data["longitude"])

    if config["full_address"]:
        output.append('Location: %s%s [%5.4f,%5.4f]' \
                % (
                    _shorten_full_location(config["full_address"], city_only=city_only),
                    suffix,
                    latitude,
                    longitude,
                ))

    output = [
        re.sub("^( *[A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"),
               re.sub("^( +[A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"),
                      re.sub(r"(\|)", lambda m: _colorize(m.group(1), "2"), x)))
        for x in output]

    return "".join("%s\n" % x for x in output)

# }}}
# get_geodata {{{
def get_geodata(location):
    text = requests.get("http://127.0.0.1:8083/:geo-location?location=%s" % location).text
    return json.loads(text)
# }}}

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(parsed_query)
    else:
        data_parsed = data

    if html_output:
        parsed_query["text"] = "no"
        filename = "b_" + parse_query.serialize(parsed_query) + ".png"
        output = """
<html>
<head>
<title>Weather report for {orig_location}</title>
<link rel="stylesheet" type="text/css" href="/files/style.css" />
</head>
<body>
  <img src="/{filename}" width="592" height="532"/>
<pre>
{textual_information}
</pre>
</body>
</html>
""".format(
        filename=filename, 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" and parsed_query.get("text") != "no":
            output += textual_information(data_parsed, geo_data, parsed_query)
        if parsed_query.get('no-terminal', False):
            output = remove_ansi(output)
        if parsed_query.get('dumb', False):
            output = output.translate(TRANSLATION_TABLE)
    return output

if __name__ == '__main__':
    sys.stdout.write(main(sys.argv[1]))




*wttr.in — the right way to ~check~ `curl` the weather!*

wttr.in is a console-oriented weather forecast service that supports various information
representation methods like terminal-oriented ANSI-sequences for console HTTP clients
(curl, httpie, or wget), HTML for web browsers, or PNG for graphical viewers.

Originally started as a small project, a wrapper for [wego](https://github.com/schachmat/wego),
intended to demonstrate the power of the console-oriented services,
*wttr.in* became a popular weather reporting service, handling tens of millions of queries daily.

You can see it running here: [wttr.in](https://wttr.in).

[Documentation](https://wttr.in/:help) | [Usage](https://github.com/chubin/wttr.in#usage) | [One-line output](https://github.com/chubin/wttr.in#one-line-output) | [Data-rich output format](https://github.com/chubin/wttr.in#data-rich-output-format-v2) | [Map view](https://github.com/chubin/wttr.in#map-view-v3) | [Output formats](https://github.com/chubin/wttr.in#different-output-formats) | [Moon phases](https://github.com/chubin/wttr.in#moon-phases) | [Internationalization](https://github.com/chubin/wttr.in#internationalization-and-localization) | [Installation](https://github.com/chubin/wttr.in#installation)

## Usage

You can access the service from a shell or from a Web browser like this:

    $ curl wttr.in
    Weather for City: Paris, France

         \   /     Clear
          .-.      10 – 11 °C
       ― (   ) ―   ↑ 11 km/h
          `-’      10 km
         /   \     0.0 mm


Here is an example weather report:

![Weather Report](San_Francisco.png)

Or in PowerShell:

```PowerShell
Invoke-RestMethod https://wttr.in
```

Want to get the weather information for a specific location? You can add the desired location to the URL in your
request like this:

    $ curl wttr.in/London
    $ curl wttr.in/Moscow
    $ curl wttr.in/Salt+Lake+City

If you omit the location name, you will get the report for your current location based on your IP address.

Use 3-letter airport codes in order to get the weather information at a certain airport:

    $ curl wttr.in/muc      # Weather for IATA: muc, Munich International Airport, Germany
    $ curl wttr.in/ham      # Weather for IATA: ham, Hamburg Airport, Germany

Let's say you'd like to get the weather for a geographical location other than a town or city - maybe an attraction
in a city, a mountain name, or some special location. Add the character `~` before the name to look up that special
location name before the weather is then retrieved:

	$ curl wttr.in/~Vostok+Station
	$ curl wttr.in/~Eiffel+Tower
	$ curl wttr.in/~Kilimanjaro

For these examples, you'll see a line below the weather forecast output that shows the geolocation
results of looking up the location:

	Location: Vostok Station, станция Восток, AAT, Antarctica [-78.4642714,106.8364678]
    Location: Tour Eiffel, 5, Avenue Anatole France, Gros-Caillou, 7e, Paris, Île-de-France, 75007, France [48.8582602,2.29449905432]
	Location: Kilimanjaro, Northern, Tanzania [-3.4762789,37.3872648]

You can also use IP-addresses (direct) or domain names (prefixed with `@`) to specify a location:

    $ curl wttr.in/@github.com
    $ curl wttr.in/@msu.ru

To get detailed information online, you can access the [/:help](https://wttr.in/:help) page:

    $ curl wttr.in/:help

### Weather Units

By default the USCS units are used for the queries from the USA and the metric system for the rest of the world.
You can override this behavior by adding `?u`, `?m` or `?M`   to a URL like this:

    $ curl wttr.in/Amsterdam?u  # USCS (used by default in US)
    $ curl wttr.in/Amsterdam?m  # metric (SI) (used by default everywhere except US)
    $ curl wttr.in/Amsterdam?M  # metric (SI), but show wind speed in m/s

If you have several options to pass, write them without delimiters in between for the one-letter options,
and use `&` as a delimiter for the long options with values:

    $ curl 'wttr.in/Amsterdam?m2&lang=nl'

It would be a rough equivalent of `-m2 --lang nl` for the GNU CLI syntax.

## Supported output formats and views

wttr.in currently supports five output formats:

* ANSI for the terminal;
* Plain-text for the terminal and scripts;
* HTML for the browser;
* PNG for the graphical viewers;
* JSON for scripts and APIs;
* Prometheus metrics for scripts and APIs.

The ANSI and HTML formats are selected based on the User-Agent string.

To force plain text, which disables colors:

    $ curl wttr.in/?T

To restrict output to glyphs available in standard console fonts (e.g. Consolas and Lucida Console):

    $ curl wttr.in/?d

The PNG format can be forced by adding `.png` to the end of the query:

    $ wget wttr.in/Paris.png

You can use all of the options with the PNG-format like in an URL, but you have
to separate them with `_` instead of `?` and `&`:

    $ wget wttr.in/Paris_0tqp_lang=fr.png

Useful options for the PNG format:

* `t` for transparency (`transparency=150`);
* transparency=0..255 for a custom transparency level.

Transparency is a useful feature when weather PNGs are used to add weather data to pictures:

    $ convert source.jpg <( curl wttr.in/Oymyakon_tqp0.png ) -geometry +50+50 -composite target.jpg

In this example:

* `source.jpg` - source file;
* `target.jpg` - target file;
* `Oymyakon` - name of the location;
* `tqp0` - options (recommended).

![Picture with weather data](https://pbs.twimg.com/media/C69-wsIW0AAcAD5.jpg)

You can embed a special wttr.in widget, that displays the weather condition for the current or a selected location, into a HTML page using the [wttr-switcher](https://github.com/midzer/wttr-switcher). That is how it looks like: [wttr-switcher-example](https://midzer.github.io/wttr-switcher/) or on a real world web site: https://feuerwehr-eisolzried.de/.

![Embedded wttr.in example at feuerwehr-eisolzried.de](https://user-images.githubusercontent.com/3875145/65265457-50eac180-db11-11e9-8f9b-2e1711dfc436.png)

## One-line output

One-line output format is convenient to be used to show weather info
in status bar of different programs, such as *tmux*, *weechat*, etc.

For one-line output format, specify additional URL parameter `format`:

```
$ curl wttr.in/Nuremberg?format=3
Nuremberg: 🌦 +11⁰C
```

Available preconfigured formats: 1, 2, 3, 4 and the custom format using the percent notation (see below).

You can specify multiple locations separated with `:` (for repeating queries):

```
$ curl wttr.in/Nuremberg:Hamburg:Berlin?format=3
Nuremberg: 🌦 +11⁰C
```
Or to process all this queries at once:

```
$ curl -s 'wttr.in/{Nuremberg,Hamburg,Berlin}?format=3'
Nuremberg: 🌦 +11⁰C
Hamburg: 🌦 +8⁰C
Berlin: 🌦 +8⁰C
```

To specify your own custom output format, use the special `%`-notation:

```
    c    Weather condition,
    C    Weather condition textual name,
    x    Weather condition, plain-text symbol,
    h    Humidity,
    t    Temperature (Actual),
    f    Temperature (Feels Like),
    w    Wind,
    l    Location,
    m    Moon phase 🌑🌒🌓🌔🌕🌖🌗🌘,
    M    Moon day,
    p    Precipitation (mm/3 hours),
    P    Pressure (hPa),
    u    UV index (1-12),

    D    Dawn*,
    S    Sunrise*,
    z    Zenith*,
    s    Sunset*,
    d    Dusk*,
    T    Current time*,
    Z    Local timezone.

(*times are shown in the local timezone)
```

So, these two calls are the same:

```
    $ curl wttr.in/London?format=3
    London: ⛅️ +7⁰C
    $ curl wttr.in/London?format="%l:+%c+%t\n"
    London: ⛅️ +7⁰C
```

### tmux

When using in `tmux.conf`, you have to escape `%` with `%`, i.e. write there `%%` instead of `%`.

The output does not contain new line by default, when the %-notation is used, but it does contain it when preconfigured format (`1`,`2`,`3` etc.)
are used. To have the new line in the output when the %-notation is used, use '\n' and single quotes when doing a query from the shell.

In programs, that are querying the service automatically (such as tmux), it is better to use some reasonable update interval. In tmux, you can configure it with `status-interval`.

If several, `:` separated locations, are specified in the query, specify update period
as an additional query parameter `period=`:
```
set -g status-interval 60
WEATHER='#(curl -s wttr.in/London:Stockholm:Moscow\?format\="%%l:+%%c%%20%%t%%60%%w&period=60")'
set -g status-right "$WEATHER ..."
```
![wttr.in in tmux status bar](https://wttr.in/files/example-tmux-status-line.png)

### WeeChat

To embed in to an IRC ([WeeChat](https://github.com/weechat/weechat)) client's existing status bar:

```
/alias add wttr /exec -pipe "/mute /set plugins.var.wttr" url:wttr.in/Montreal?format=%l:+%c+%f+%h+%p+%P+%m+%w+%S+%s;/wait 3 /item refresh wttr
/trigger add wttr timer 60000;0;0 "" "" "/wttr"
/item add wttr "" "${plugins.var.wttr}"
/eval /set weechat.bar.status.items ${weechat.bar.status.items},spacer,wttr
/eval /set weechat.startup.command_after_plugins ${weechat.startup.command_after_plugins};/wttr
/wttr
```
![wttr.in in WeeChat status bar](https://i.imgur.com/XkYiRU7.png)


### conky

Conky usage example:

```
${texeci 1800 curl wttr.in/kyiv_0pq_lang=uk.png
  | convert - -transparent black $HOME/.config/conky/out.png}
${image $HOME/.config/conky/out.png -p 0,0}
```

![wttr.in in conky](https://user-images.githubusercontent.com/3875145/172178453-9e9ed9e3-9815-426a-9a21-afdd6e279fc8.png)


### IRC

IRC integration example:

* https://github.com/OpenSourceTreasure/Mirc-ASCII-weather-translate-pixel-editor

### Emojis support

To see emojis in terminal, you need:

1. Terminal support for emojis (was added to Cairo 1.15.8);
2. Font with emojis support.

For the emoji font, we recommend *Noto Color Emoji*, and a good alternative option would be the *Emoji One* font;
both of them support all necessary emoji glyphs.

Font configuration:

```xml
$ cat ~/.config/fontconfig/fonts.conf
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
<fontconfig>
  <alias>
    <family>serif</family>
    <prefer>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>
  <alias>
    <family>sans-serif</family>
    <prefer>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>
  <alias>
    <family>monospace</family>
    <prefer>
      <family>Noto Color Emoji</family>
    </prefer>
  </alias>
</fontconfig>
```

(to apply the configuration, run `fc-cache -f -v`).

In some cases, `tmux` and the terminal understanding of some emoji characters may differ, which may
cause strange effects similar to that described in #579.

## Data-rich output format (v2)

In the experimental data-rich output format, that is available under the view code `v2`,
a lot of additional weather and astronomical information is available:

* Temperature, and precipitation changes forecast throughout the days;
* Moonphase for today and the next three days;
* The current weather condition, temperature, humidity, wind speed and direction, pressure;
* Timezone;
* Dawn, sunrise, noon, sunset, dusk time for he selected location;
* Precise geographical coordinates for the selected location.

```
  $ curl v2.wttr.in/München
```

or

```
  $ curl wttr.in/München?format=v2
```

or, if you prefer Nerd Fonts instead of Emoji, `v2d` (day) or `v2n` (night):

```
  $ curl v2d.wttr.in/München
```


![data-reach output format](https://wttr.in/files/example-wttr-v2.png)

(The mode is experimental, and it has several limitations currently:

* It works only in terminal;
* Only English is supported).

Currently, you need some tweaks for some terminals, to get the best possible visualization.

### URXVT

Depending on your configuration you might be taking all steps, or only a few. URXVT currently doesn't support emoji related fonts, but we can get almost the same effect using *Font-Symbola*. So add to your `.Xresources` file the following line:
```
    xft:symbola:size=10:minspace=False
```
You can add it _after_ your preferred font and it will only show up when required.
Then, if you see or feel like you're having spacing issues, add this: `URxvt.letterSpace: 0`
For some reason URXVT sometimes stops deciding right the word spacing and we need to force it this way.

The result, should look like:

![URXVT Emoji line](https://user-images.githubusercontent.com/24360204/63842949-1d36d480-c975-11e9-81dd-998d1329bd8a.png)

## Map view (v3)

In the experimental map view, that is available under the view code `v3`,
weather information about a geographical region is available:

```
    $ curl v3.wttr.in/Bayern.sxl
```

![v3.wttr.in/Bayern](https://v3.wttr.in/Bayern.png)

or directly in browser:

*   https://v3.wttr.in/Bayern

The map view currently supports three formats:

* PNG (for browser and messengers);
* Sixel (terminal inline images support);
* IIP (terminal with iterm2 inline images protocol support).

Terminal with inline images protocols support:

⟶ *Detailed article: [Images in terminal](doc/terminal-images.md)*

| Terminal              | Environment    | Images support | Protocol |
| --------------------- | --------- | ------------- | --------- |
| uxterm                |   X11     |   yes         |   Sixel   |
| mlterm                |   X11     |   yes         |   Sixel   |
| kitty                 |   X11     |   yes         |   Kitty   |
| wezterm               |   X11     |   yes         |   IIP     |
| Darktile              |   X11     |   yes         |   Sixel   |
| Jexer                 |   X11     |   yes         |   Sixel   |
| GNOME Terminal        |   X11     |   [in-progress](https://gitlab.gnome.org/GNOME/vte/-/issues/253) |   Sixel   |
| alacritty             |   X11     |   [in-progress](https://github.com/alacritty/alacritty/issues/910) |  Sixel   |
| foot                  |  Wayland  |   yes         |   Sixel   |
| DomTerm               |   Web     |   yes         |   Sixel   |
| Yaft                  |   FB      |   yes         |   Sixel   |
| iTerm2                |   Mac OS X|   yes         |   IIP     |
| mintty                | Windows   |   yes         |   Sixel   |
| Windows Terminal  |   Windows     |   [in-progress](https://github.com/microsoft/terminal/issues/448) |   Sixel   |
| [RLogin](http://nanno.dip.jp/softlib/man/rlogin/) | Windows | yes         |   Sixel   |   |


## Different output formats

### 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:
```json
{
	"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).

### Prometheus Metrics Output

The [Prometheus](https://github.com/prometheus/prometheus) Metrics format is a feature providing access to *wttr.in* data through an easy-to-parse format for monitoring systems, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.

To fetch information in Prometheus format, use the following syntax:

    $ curl wttr.in/Detroit?format=p1

This will fetch information on the Detroit region in Prometheus Metrics format. The `p1` format code is used to allow for the use of other layouts for the Prometheus Metrics output.

A possible configuration for Prometheus could look like this:

```yaml
    - job_name: 'wttr_in_detroit'
        static_configs:
            - targets: ['wttr.in']
        metrics_path: '/Detroit'
        params:
            format: ['p1']
```

The result will look something like the following:


    # HELP temperature_feels_like_celsius Feels Like Temperature in Celsius
    temperature_feels_like_celsius{forecast="current"} 7
    # HELP temperature_feels_like_fahrenheit Feels Like Temperature in Fahrenheit
    temperature_feels_like_fahrenheit{forecast="current"} 45
    [truncated]
...


## 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
in the full-output mode:

    $ curl wttr.in/Moon

Get the moon phase for a particular date by adding `@YYYY-MM-DD`:

    $ curl wttr.in/Moon@2016-12-25

The moon phase information uses [pyphoon](https://github.com/chubin/pyphoon) as its backend.

To get the moon phase information in the online mode, use `%m`:

    $ curl wttr.in/London?format=%m
    🌖

Keep in mind that the Unicode representation of moon phases suffers 2 caveats:

- With some fonts, the representation `🌘` is ambiguous, for it either seem
  almost-shadowed or almost-lit, depending on whether your terminal is in
  light mode or dark mode. Relying on colored fonts like `noto-fonts` works
  around this problem.

- The representation `🌘` is also ambiguous, for it means "last quarter" in
  northern hemisphere, but "first quarter" in souther hemisphere. It also means
  nothing in tropical zones. This is a limitation that
  [Unicode](https://www.unicode.org/L2/L2017/17304-moon-var.pdf) is aware about.
  But it has not been worked around at `wttr.in` yet.

See #247, #364 for the corresponding tracking issues,
and [pyphoon#1](https://github.com/chubin/pyphoon/issues/1) for pyphoon. Any help is welcome.

## Internationalization and localization

wttr.in supports multilingual locations names that can be specified in any language in the world
(it may be surprising, but many locations in the world don't have an English name).

The query string should be specified in Unicode (hex-encoded or not). Spaces in the query string
must be replaced with `+`:

    $ curl wttr.in/станция+Восток
    Weather report: станция Восток

                   Overcast
          .--.     -65 – -47 °C
       .-(    ).   ↑ 23 km/h
      (___.__)__)  15 km
                   0.0 mm

The language used for the output (except the location name) does not depend on the input language
and it is either English (by default) or the preferred language of the browser (if the query
was issued from a browser) that is specified in the query headers (`Accept-Language`).

The language can be set explicitly when using console clients by using command-line options like this:

    curl -H "Accept-Language: fr" wttr.in
    http GET wttr.in Accept-Language:ru

The preferred language can be forced using the `lang` option:

    $ curl wttr.in/Berlin?lang=de

The third option is to choose the language using the DNS name used in the query:

    $ curl de.wttr.in/Berlin

wttr.in is currently translated into 54 languages, and the number of supported languages is constantly growing.

See [/:translation](https://wttr.in/:translation) to learn more about the translation process,
to see the list of supported languages and contributors, or to know how you can help to translate wttr.in
in your language.

![Queries to wttr.in in various languages](https://pbs.twimg.com/media/C7hShiDXQAES6z1.jpg)

## Installation

To install the application:

1. Install external dependencies
2. Install Python dependencies used by the service
3. Configure IP2Location (optional)
4. Get a WorldWeatherOnline API and configure wego
5. Configure wttr.in
6. Configure the HTTP-frontend service

### Install external dependencies

wttr.in has the following external dependencies:

* [golang](https://golang.org/doc/install), wego dependency
* [wego](https://github.com/schachmat/wego), weather client for terminal

After you install [golang](https://golang.org/doc/install), install `wego`:
```bash
go install github.com/schachmat/wego@latest
```

### Install Python dependencies

Python requirements:

* Flask
* geoip2
* geopy
* requests
* gevent

If you want to get weather reports as PNG files, you'll also need to install:

* PIL
* pyte (>=0.6)
* necessary fonts

You can install most of them using `pip`.

Some python package use LLVM, so install it first:

```bash
apt-get install llvm-7 llvm-7-dev
```
If `virtualenv` is used:
```bash
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/).

### Configure IP2Location (optional)

If you want to use the IP2location service for IP-addresses that are not covered by GeoLite2,
you have to obtain a API key of that service, and after that save into the `~/.ip2location.key` file:

```
$ echo 'YOUR_IP2LOCATION_KEY' > ~/.ip2location.key
```

If you don't have this file, the service will be silently skipped (it is not a big problem,
because the MaxMind database is pretty good).

### Installation with Docker

* Install Docker
* Build Docker Image
* These files should be mounted by the user at runtime:

```
/root/.wegorc
/root/.ip2location.key (optional)
/app/airports.dat
/app/GeoLite2-City.mmdb
```

### Get a WorldWeatherOnline key and configure wego

To get a WorldWeatherOnline API key, you must register here:

    https://developer.worldweatheronline.com/auth/register

After you have a WorldWeatherOnline key, you can save it into the
WWO key file: `~/.wwo.key`

Also, you have to specify the key in the `wego` configuration:

```json
$ cat ~/.wegorc
{
	"APIKey": "00XXXXXXXXXXXXXXXXXXXXXXXXXXX",
	"City": "London",
	"Numdays": 3,
	"Imperial": false,
	"Lang": "en"
}
```

The `City` parameter in `~/.wegorc` is ignored.

### Configure wttr.in

Configure the following environment variables that define the path to the local `wttr.in`
installation, to the GeoLite database, and to the `wego` installation. For example:

```bash
export WTTR_MYDIR="/home/igor/wttr.in"
export WTTR_GEOLITE="/home/igor/wttr.in/GeoLite2-City.mmdb"
export WTTR_WEGO="/home/igor/go/bin/wego"
export WTTR_LISTEN_HOST="0.0.0.0"
export WTTR_LISTEN_PORT="8002"
```


### Configure the HTTP-frontend service

It's recommended that you also configure the web server that will be used to access the service:

```nginx
server {
	listen [::]:80;
	server_name  wttr.in *.wttr.in;
	access_log  /var/log/nginx/wttr.in-access.log  main;
	error_log  /var/log/nginx/wttr.in-error.log;

	location / {
	    proxy_pass         http://127.0.0.1:8002;

	    proxy_set_header   Host             $host;
	    proxy_set_header   X-Real-IP        $remote_addr;
	    proxy_set_header   X-Forwarded-For  $remote_addr;

	    client_max_body_size       10m;
	    client_body_buffer_size    128k;

	    proxy_connect_timeout      90;
	    proxy_send_timeout         90;
	    proxy_read_timeout         90;

	    proxy_buffer_size          4k;
	    proxy_buffers              4 32k;
	    proxy_busy_buffers_size    64k;
	    proxy_temp_file_write_size 64k;

	    expires                    off;
	}
}
```
