Merge branch 'master' into basque_translations_update

This commit is contained in:
Igor Chubin 2020-05-13 10:44:52 +02:00 committed by GitHub
commit b20f31c148
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
48 changed files with 1690 additions and 962 deletions

119
README.md
View file

@ -80,12 +80,12 @@ You can override this behavior by adding `?u` or `?m` to a URL like this:
$ curl wttr.in/Amsterdam?u
$ curl wttr.in/Amsterdam?m
## Supported output formats
## Supported output formats and views
wttr.in currently supports four output formats:
wttr.in currently supports five output formats:
* ANSI for the terminal;
* ANSI for the terminal, one-line mode;
* Plain-text for the terminal and scripts;
* HTML for the browser;
* PNG for the graphical viewers;
* JSON for scripts and APIs.
@ -122,53 +122,6 @@ You can embed a special wttr.in widget, that displays the weather condition for
![Embedded wttr.in example at feuerwehr-eisolzried.de](https://user-images.githubusercontent.com/3875145/65265457-50eac180-db11-11e9-8f9b-2e1711dfc436.png)
## JSON output
The JSON format is a feature providing access to wttr.in data through an easy-to-parse format, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
To fetch information in JSON format, use the following syntax:
$ curl wttr.in/Detroit?format=j1
This will fetch information on the Detroit region in JSON format. The j1 format code is used to allow for the use of other layouts for the JSON output.
The result will look something like the following:
{
"current_condition": [
{
"FeelsLikeC": "25",
"FeelsLikeF": "76",
"cloudcover": "100",
"humidity": "76",
"observation_time": "04:08 PM",
"precipMM": "0.2",
"pressure": "1019",
"temp_C": "22",
"temp_F": "72",
"uvIndex": 5,
"visibility": "16",
"weatherCode": "122",
"weatherDesc": [
{
"value": "Overcast"
}
],
"weatherIconUrl": [
{
"value": ""
}
],
"winddir16Point": "NNE",
"winddirDegree": "20",
"windspeedKmph": "7",
"windspeedMiles": "4"
}
],
...
Most of these values are self-explanatory, aside from `weatherCode`. The `weatherCode` is an enumeration which you can find at either [the WorldWeatherOnline website](https://www.worldweatheronline.com/developer/api/docs/weather-icons.aspx) or [in the wttr.in source code](https://github.com/chubin/wttr.in/blob/master/lib/constants.py).
## One-line output
For one-line output format, specify additional URL parameter `format`:
@ -209,6 +162,14 @@ To specify your own custom output format, use the special `%`-notation:
p precipitation (mm),
o Probability of Precipitation,
P pressure (hPa),
D Dawn*,
S Sunrise*,
z Zenith*,
s Sunset*,
d Dusk*.
(times are shown in the local timezone)
```
So, these two calls are the same:
@ -316,6 +277,54 @@ The result, should look like:
![URXVT Emoji line](https://user-images.githubusercontent.com/24360204/63842949-1d36d480-c975-11e9-81dd-998d1329bd8a.png)
## JSON output
The JSON format is a feature providing access to wttr.in data through an easy-to-parse format, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
To fetch information in JSON format, use the following syntax:
$ curl wttr.in/Detroit?format=j1
This will fetch information on the Detroit region in JSON format. The j1 format code is used to allow for the use of other layouts for the JSON output.
The result will look something like the following:
{
"current_condition": [
{
"FeelsLikeC": "25",
"FeelsLikeF": "76",
"cloudcover": "100",
"humidity": "76",
"observation_time": "04:08 PM",
"precipMM": "0.2",
"pressure": "1019",
"temp_C": "22",
"temp_F": "72",
"uvIndex": 5,
"visibility": "16",
"weatherCode": "122",
"weatherDesc": [
{
"value": "Overcast"
}
],
"weatherIconUrl": [
{
"value": ""
}
],
"winddir16Point": "NNE",
"winddirDegree": "20",
"windspeedKmph": "7",
"windspeedMiles": "4"
}
],
...
Most of these values are self-explanatory, aside from `weatherCode`. The `weatherCode` is an enumeration which you can find at either [the WorldWeatherOnline website](https://www.worldweatheronline.com/developer/api/docs/weather-icons.aspx) or [in the wttr.in source code](https://github.com/chubin/wttr.in/blob/master/lib/constants.py).
## Moon phases
wttr.in can also be used to check the phase of the Moon. This example shows how to see the current Moon phase
@ -446,11 +455,15 @@ If you want to get weather reports as PNG files, you'll also need to install:
You can install most of them using `pip`.
Some python package use LLVM, so install it first:
$ apt-get install llvm-7 llvm-7-dev
If `virtualenv` is used:
$ virtualenv ve
$ ve/bin/pip install -r requirements.txt
$ ve/bin/python bin/srv.py
$ virtualenv -p python3 ve
$ ve/bin/pip3 install -r requirements.txt
$ ve/bin/python3 bin/srv.py
Also, you need to install the geoip2 database.
You can use a free database GeoLite2 that can be downloaded from (http://dev.maxmind.com/geoip/geoip2/geolite2/).

View file

@ -7,6 +7,10 @@ The proxy server acts as a backend for the wttr.in service.
It caches the answers and handles various data sources transforming their
answers into format supported by the wttr.in service.
If WTTRIN_TEST is specified, it works in a special test mode:
it does not fetch and does not store the data in the cache,
but is using the fake data from "test/proxy-data".
"""
from __future__ import print_function
@ -19,6 +23,7 @@ import sys
import os
import time
import json
import hashlib
import requests
import cyrtranslit
@ -35,7 +40,10 @@ from globals import PROXY_CACHEDIR, PROXY_HOST, PROXY_PORT
from translations import PROXY_LANGS
# pylint: enable=wrong-import-position
def is_testmode():
"""Server is running in the wttr.in test mode"""
return "WTTRIN_TEST" in os.environ
def load_translations():
"""
@ -66,38 +74,56 @@ TRANSLATIONS = load_translations()
def _find_srv_for_query(path, query): # pylint: disable=unused-argument
return 'http://api.worldweatheronline.com'
def _cache_file(path, query):
"""Return cache file name for specified `path` and `query`
and for the current time.
Do smooth load on the server, expiration time
is slightly varied basing on the path+query sha1 hash digest.
"""
digest = hashlib.sha1(("%s %s" % (path, query)).encode("utf-8")).hexdigest()
digest_number = ord(digest[0].upper())
expiry_interval = 60*(digest_number+10)
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):
timestamp = time.strftime("%Y%m%d%H", time.localtime())
cache_file = os.path.join(PROXY_CACHEDIR, timestamp, path, query)
if is_testmode():
cache_file = "test/proxy-data/data1"
else:
cache_file = _cache_file(path, query)
try:
return (open(cache_file, 'r').read(),
json.loads(open(cache_file+".headers", 'r').read()))
except IOError:
return None, None
def _touch_empty_file(path, query, content, headers):
timestamp = time.strftime("%Y%m%d%H", time.localtime())
cache_file = os.path.join(PROXY_CACHEDIR + ".empty", timestamp, path, query)
def _touch_empty_file(path, query):
cache_file = _cache_file(path, query)
cache_dir = os.path.dirname(cache_file)
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
open(cache_file, 'w').write("")
def _save_content_and_headers(path, query, content, headers):
timestamp = time.strftime("%Y%m%d%H", time.localtime())
cache_file = os.path.join(PROXY_CACHEDIR, timestamp, path, query)
cache_file = _cache_file(path, query)
cache_dir = os.path.dirname(cache_file)
if not os.path.exists(cache_dir):
os.makedirs(cache_dir)
open(cache_file + ".headers", 'w').write(json.dumps(headers))
open(cache_file, 'w').write(content)
open(cache_file, 'wb').write(content)
def translate(text, lang):
"""
Translate `text` into `lang`
"""
translated = TRANSLATIONS.get(lang, {}).get(text, text)
if text.encode('utf-8') == translated:
if text == translated:
print("%s: %s" % (lang, text))
return translated
@ -108,20 +134,25 @@ def cyr(to_translate):
return cyrtranslit.to_cyrillic(to_translate)
def _patch_greek(original):
return original.decode('utf-8').replace(u"Ηλιόλουστη/ο", u"Ηλιόλουστη").encode('utf-8')
return original.replace(u"Ηλιόλουστη/ο", u"Ηλιόλουστη")
def add_translations(content, lang):
"""
Add `lang` translation to `content` (JSON)
returned by the data source
"""
if content is "{}":
return {}
languages_to_translate = TRANSLATIONS.keys()
try:
d = json.loads(content) # pylint: disable=invalid-name
except ValueError as exception:
except (ValueError, TypeError) as exception:
print("---")
print(exception)
print("---")
return {}
try:
weather_condition = d['data']['current_condition'][0]['weatherDesc'][0]['value']
@ -132,16 +163,16 @@ def add_translations(content, lang):
d['data']['current_condition'][0]['lang_%s' % lang] = \
[{'value': cyr(
d['data']['current_condition'][0]['lang_%s' % lang][0]['value']\
.encode('utf-8'))}]
)}]
elif lang == 'el':
d['data']['current_condition'][0]['lang_%s' % lang] = \
[{'value': _patch_greek(
d['data']['current_condition'][0]['lang_%s' % lang][0]['value']\
.encode('utf-8'))}]
)}]
elif lang == 'sr-lat':
d['data']['current_condition'][0]['lang_%s' % lang] = \
[{'value':d['data']['current_condition'][0]['lang_sr'][0]['value']\
.encode('utf-8')}]
}]
fixed_weather = []
for w in d['data']['weather']: # pylint: disable=invalid-name
@ -153,13 +184,13 @@ def add_translations(content, lang):
[{'value': translate(weather_condition, lang)}]
elif lang == 'sr':
h['lang_%s' % lang] = \
[{'value': cyr(h['lang_%s' % lang][0]['value'].encode('utf-8'))}]
[{'value': cyr(h['lang_%s' % lang][0]['value'])}]
elif lang == 'el':
h['lang_%s' % lang] = \
[{'value': _patch_greek(h['lang_%s' % lang][0]['value'].encode('utf-8'))}]
[{'value': _patch_greek(h['lang_%s' % lang][0]['value'])}]
elif lang == 'sr-lat':
h['lang_%s' % lang] = \
[{'value': h['lang_sr'][0]['value'].encode('utf-8')}]
[{'value': h['lang_sr'][0]['value']}]
fixed_hourly.append(h)
w['hourly'] = fixed_hourly
fixed_weather.append(w)
@ -177,7 +208,7 @@ def proxy(path):
"""
lang = request.args.get('lang', 'en')
query_string = request.query_string
query_string = request.query_string.decode("utf-8")
query_string = query_string.replace('sr-lat', 'sr')
query_string = query_string.replace('lang=None', 'lang=en')
query_string += "&extra=localObsTime"
@ -187,7 +218,6 @@ def proxy(path):
if content is None:
srv = _find_srv_for_query(path, query_string)
url = '%s/%s?%s' % (srv, path, query_string)
print(url)
attempts = 10
response = None
@ -203,14 +233,18 @@ def proxy(path):
except ValueError:
attempts -= 1
_touch_empty_file(path, query_string, content, headers)
_touch_empty_file(path, query_string)
if response:
headers = {}
headers['Content-Type'] = response.headers['content-type']
_save_content_and_headers(path, query_string, response.content, headers)
content = add_translations(response.content, lang)
content = response.content
else:
content = "{}"
else:
print("cache found")
content = add_translations(content, lang)
return content, 200, headers

145
cmd/srv.go Normal file
View file

@ -0,0 +1,145 @@
package main
import (
"context"
"fmt"
"io/ioutil"
"log"
"net"
"net/http"
"time"
"github.com/hashicorp/golang-lru"
)
var lruCache *lru.Cache
type ResponseWithHeader struct {
Body []byte
Header http.Header
StatusCode int // e.g. 200
}
func init() {
var err error
lruCache, err = lru.New(12800)
if err != nil {
panic(err)
}
dialer := &net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}
http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
addr = "127.0.0.1:8002"
return dialer.DialContext(ctx, network, addr)
}
}
func readUserIP(r *http.Request) string {
IPAddress := r.Header.Get("X-Real-Ip")
if IPAddress == "" {
IPAddress = r.Header.Get("X-Forwarded-For")
}
if IPAddress == "" {
IPAddress = r.RemoteAddr
var err error
IPAddress, _, err = net.SplitHostPort(IPAddress)
if err != nil {
fmt.Printf("userip: %q is not IP:port\n", IPAddress)
}
}
return IPAddress
}
// implementation of the cache.get_signature of original wttr.in
func findCacheDigest(req *http.Request) string {
userAgent := req.Header.Get("User-Agent")
queryHost := req.Host
queryString := req.RequestURI
clientIpAddress := readUserIP(req)
lang := req.Header.Get("Accept-Language")
now := time.Now()
secs := now.Unix()
timestamp := secs / 1000
return fmt.Sprintf("%s:%s%s:%s:%s:%d", userAgent, queryHost, queryString, clientIpAddress, lang, timestamp)
}
func get(req *http.Request) ResponseWithHeader {
client := &http.Client{}
queryURL := fmt.Sprintf("http://%s%s", req.Host, req.RequestURI)
proxyReq, err := http.NewRequest(req.Method, queryURL, req.Body)
if err != nil {
// handle error
}
// proxyReq.Header.Set("Host", req.Host)
// proxyReq.Header.Set("X-Forwarded-For", req.RemoteAddr)
for header, values := range req.Header {
for _, value := range values {
proxyReq.Header.Add(header, value)
}
}
res, err := client.Do(proxyReq)
if err != nil {
panic(err)
}
body, err := ioutil.ReadAll(res.Body)
return ResponseWithHeader{
Body: body,
Header: res.Header,
StatusCode: res.StatusCode,
}
}
func copyHeader(dst, src http.Header) {
for k, vv := range src {
for _, v := range vv {
dst.Add(k, v)
}
}
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
var response ResponseWithHeader
cacheDigest := findCacheDigest(r)
cacheBody, ok := lruCache.Get(cacheDigest)
if ok {
response = cacheBody.(ResponseWithHeader)
} else {
fmt.Println(cacheDigest)
response = get(r)
if response.StatusCode == 200 {
lruCache.Add(cacheDigest, response)
}
}
copyHeader(w.Header(), response.Header)
w.WriteHeader(response.StatusCode)
w.Write(response.Body)
})
log.Fatal(http.ListenAndServe(":8081", nil))
}

View file

@ -5,17 +5,27 @@ LRU-Cache implementation for formatted (`format=`) answers
import datetime
import re
import time
import pylru
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 "%{{NOW(" in answer:
if isinstance(answer, str) and "%{{NOW(" in answer:
answer = re.sub(r"%{{NOW\(([^}]*)\)}}", lambda x: _now_in_tz(x.group(1)), answer)
return answer
@ -26,9 +36,9 @@ def get_signature(user_agent, query_string, client_ip_address, lang):
`lang`, and `client_ip_address`
"""
timestamp = int(time.time()) / 1000
signature = "%s:%s:%s:%s:%s" % \
(user_agent, query_string, client_ip_address, lang, timestamp)
signature = "%s:%s:%s:%s" % \
(user_agent, query_string, client_ip_address, lang)
print(signature)
return signature
def get(signature):
@ -38,13 +48,92 @@ def get(signature):
the `_update_answer` function.
"""
if signature in CACHE:
return _update_answer(CACHE[signature])
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`
"""
CACHE[signature] = 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()

View file

@ -52,55 +52,55 @@ WWO_CODE = {
}
WEATHER_SYMBOL = {
"Unknown": u"",
"Cloudy": u"☁️",
"Fog": u"🌫",
"HeavyRain": u"🌧",
"HeavyShowers": u"🌧",
"HeavySnow": u"❄️",
"HeavySnowShowers": u"❄️",
"LightRain": u"🌦",
"LightShowers": u"🌦",
"LightSleet": u"🌧",
"LightSleetShowers": u"🌧",
"LightSnow": u"🌨",
"LightSnowShowers": u"🌨",
"PartlyCloudy": u"⛅️",
"Sunny": u"☀️",
"ThunderyHeavyRain": u"🌩",
"ThunderyShowers": u"",
"ThunderySnowShowers": u"",
"VeryCloudy": u"☁️",
"Unknown": "",
"Cloudy": "☁️",
"Fog": "🌫",
"HeavyRain": "🌧",
"HeavyShowers": "🌧",
"HeavySnow": "❄️",
"HeavySnowShowers": "❄️",
"LightRain": "🌦",
"LightShowers": "🌦",
"LightSleet": "🌧",
"LightSleetShowers": "🌧",
"LightSnow": "🌨",
"LightSnowShowers": "🌨",
"PartlyCloudy": "⛅️",
"Sunny": "☀️",
"ThunderyHeavyRain": "🌩",
"ThunderyShowers": "",
"ThunderySnowShowers": "",
"VeryCloudy": "☁️",
}
WEATHER_SYMBOL_WIDTH_VTE = {
u"": 2,
u"☁️": 1,
u"🌫": 1,
u"🌧": 2,
u"🌧": 2,
u"❄️": 1,
u"❄️": 1,
u"🌦": 1,
u"🌦": 1,
u"🌧": 1,
u"🌧": 1,
u"🌨": 2,
u"🌨": 2,
u"⛅️": 2,
u"☀️": 1,
u"🌩": 2,
u"": 1,
u"": 1,
u"☁️": 1,
"": 2,
"☁️": 1,
"🌫": 2,
"🌧": 2,
"🌧": 2,
"❄️": 1,
"❄️": 1,
"🌦": 1,
"🌦": 1,
"🌧": 1,
"🌧": 1,
"🌨": 2,
"🌨": 2,
"⛅️": 2,
"☀️": 1,
"🌩": 2,
"": 1,
"": 1,
"☁️": 1,
}
WIND_DIRECTION = [
u"", u"", u"", u"", u"", u"", u"", u"",
"", "", "", "", "", "", "", "",
]
MOON_PHASES = (
u"🌑", u"🌒", u"🌓", u"🌔", u"🌕", u"🌖", u"🌗", u"🌘"
"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"
)
WEATHER_SYMBOL_WEGO = {

62
lib/extract_emoji.py Normal file
View file

@ -0,0 +1,62 @@
#!/usr/bin/env python
#vim: fileencoding=utf-8
"""
At the moment, Pillow library does not support colorful emojis,
that is why emojis must be extracted to external files first,
and then they must be handled as usual graphical objects
and not as text.
The files are extracted using Imagemagick.
Usage:
ve/bi/python lib/extract_emoji.py
"""
import subprocess
EMOJIS = [
"",
"☁️",
"🌫",
"🌧",
"🌧",
"❄️",
"❄️",
"🌦",
"🌦",
"🌧",
"🌧",
"🌨",
"🌨",
"⛅️",
"☀️",
"🌩",
"",
"",
"☁️",
"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"
]
def extract_emojis_to_directory(dirname):
"""
Extract emoji from an emoji font, to separate files.
"""
emoji_font = "Noto Color Emoji"
emoji_size = 30
for emoji in EMOJIS:
filename = "%s/%s.png" % (dirname, emoji)
convert_string = [
"convert", "-background", "black", "-size", "%sx%s" % (emoji_size, emoji_size),
"-set", "colorspace", "sRGB",
"pango:<span font=\"%s\" size=\"20000\">%s</span>" % (emoji_font, emoji),
filename
]
subprocess.Popen(convert_string)
if __name__ == '__main__':
extract_emojis_to_directory("share/emoji")

0
lib/fmt/__init__.py Normal file
View file

283
lib/fmt/png.py Normal file
View file

@ -0,0 +1,283 @@
#!/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):
"""Convert pyte color to PIL color
Return: tuple of color values (R,G,B)
"""
if color == 'default':
return 'lightgray'
if color in ['green', 'black', 'cyan', 'blue', 'brown']:
return color
try:
return (
int(color[0:2], 16),
int(color[2:4], 16),
int(color[4:6], 16))
except (ValueError, IndexError):
# if we do not know this color and it can not be decoded as RGB,
# print it and return it as it is (will be displayed as black)
# print color
return color
return color
def _strip_buf(buf):
"""Strips empty spaces from behind and from the right side.
(from the right side is not yet implemented)
"""
def empty_line(line):
"Returns True if the line consists from spaces"
return all(x.data == ' ' for x in line)
def line_len(line):
"Returns len of the line excluding spaces from the right"
last_pos = len(line)
while last_pos > 0 and line[last_pos-1].data == ' ':
last_pos -= 1
return last_pos
number_of_lines = 0
for line in buf[::-1]:
if not empty_line(line):
break
number_of_lines += 1
if number_of_lines:
buf = buf[:-number_of_lines]
max_len = max(line_len(x) for x in buf)
buf = [line[:max_len] for line in buf]
return buf
def _script_category(char):
"""Returns category of a Unicode character
Possible values:
default, Cyrillic, Greek, Han, Hiragana
"""
if char in emoji.UNICODE_EMOJI:
return "Emoji"
cat = unicodedata2.script_cat(char)[0]
if char == u'':
return 'Han'
if cat in ['Latin', 'Common']:
return 'default'
return cat
def _load_emojilib():
"""Load known emojis from a directory, and return dictionary
of PIL Image objects correspodent to the loaded emojis.
Each emoji is resized to the CHAR_HEIGHT size.
"""
emojilib = {}
for filename in glob.glob("share/emoji/*.png"):
character = os.path.basename(filename)[:-4]
emojilib[character] = \
Image.open(filename).resize((CHAR_HEIGHT, CHAR_HEIGHT))
return emojilib
# 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)
image = Image.new('RGB', (cols * CHAR_WIDTH, rows * CHAR_HEIGHT))
buf = buf[-ROWS:]
draw = ImageDraw.Draw(image)
font = {}
for cat in FONT_CAT:
font[cat] = ImageFont.truetype(FONT_CAT[cat], FONT_SIZE)
emojilib = _load_emojilib()
x_pos = 0
y_pos = 0
for line in buf:
x_pos = 0
for char in line:
current_color = _color_mapping(char.fg)
if char.bg != 'default':
draw.rectangle(
((x_pos, y_pos),
(x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)),
fill=_color_mapping(char.bg))
if char.data == "!":
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

View file

@ -1,3 +1,9 @@
# downloaded from https://gist.github.com/2204527
# described/recommended here:
#
# http://stackoverflow.com/questions/9868792/find-out-the-unicode-script-of-a-character
#
from __future__ import print_function
from unicodedata import *

View file

@ -14,6 +14,7 @@ from __future__ import print_function
import logging
import os
import re
MYDIR = os.path.abspath(os.path.dirname(os.path.dirname('__file__')))
@ -28,9 +29,9 @@ PYPHOON = "/home/igor/pyphoon/bin/pyphoon-lolcat"
_DATADIR = "/wttr.in"
_LOGDIR = "/wttr.in/log"
CACHEDIR = os.path.join(_DATADIR, "cache/wego/")
IP2LCACHE = os.path.join(_DATADIR, "cache/ip2l/")
PNG_CACHE = os.path.join(_DATADIR, "cache/png")
LRU_CACHE = os.path.join(_DATADIR, "cache/lru")
LOG_FILE = os.path.join(_LOGDIR, 'main.log')
@ -41,7 +42,6 @@ BLACKLIST = os.path.join(MYDIR, "share/blacklist")
HELP_FILE = os.path.join(MYDIR, 'share/help.txt')
BASH_FUNCTION_FILE = os.path.join(MYDIR, 'share/bash-function.txt')
TRANSLATION_FILE = os.path.join(MYDIR, 'share/translation.txt')
TEST_FILE = os.path.join(MYDIR, 'share/test-NAME.txt')
IATA_CODES_FILE = os.path.join(MYDIR, 'share/list-of-iata-codes.txt')
@ -77,8 +77,8 @@ PLAIN_TEXT_AGENTS = [
"lwp-request",
"wget",
"python-requests",
"OpenBSD ftp",
"PowerShell"
"openbsd ftp",
"powershell",
]
PLAIN_TEXT_PAGES = [':help', ':bash.function', ':translation', ':iterm2']
@ -123,3 +123,7 @@ def get_help_file(lang):
if os.path.exists(help_file):
return help_file
return HELP_FILE
def remove_ansi(sometext):
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
return ansi_escape.sub('', sometext)

View file

@ -9,6 +9,7 @@ precise location description.
"""
from __future__ import print_function
import sys
import os
import json
import socket
@ -35,12 +36,15 @@ def is_ip(ip_addr):
Check if `ip_addr` looks like an IP Address
"""
if sys.version_info[0] < 3:
ip_addr = ip_addr.encode("utf-8")
try:
socket.inet_pton(socket.AF_INET, ip_addr.encode("utf-8"))
socket.inet_pton(socket.AF_INET, ip_addr)
return True
except socket.error:
try:
socket.inet_pton(socket.AF_INET6, ip_addr.encode("utf-8"))
socket.inet_pton(socket.AF_INET6, ip_addr)
return True
except socket.error:
return False
@ -260,7 +264,7 @@ def location_processing(location, ip_addr):
# if location is not None and location.upper() in IATA_CODES:
# location = '~%s' % location
if location is not None and location.startswith('~'):
if location is not None and not location.startswith("~-,") and location.startswith('~'):
geolocation = geolocator(location_canonical_name(location[1:]))
if geolocation is not None:
if not override_location_name:

View file

@ -1,3 +1,36 @@
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):
"""
"""
@ -29,7 +62,6 @@ def parse_query(args):
result = {}
reserved_args = ["lang"]
#q = "&".join(x for x in args.keys() if x not in reserved_args)
q = ""
@ -68,8 +100,6 @@ def parse_query(args):
if days in q:
result['days'] = days
result['no-caption'] = False
result['no-city'] = False
if 'q' in q:
result['no-caption'] = True
if 'Q' in q:
@ -82,7 +112,61 @@ def parse_query(args):
val = True
if val == 'False':
val = False
result[key] = val
if val:
result[key] = val
# currently `view` is alias for `format`
if "format" in result and not result.get("view"):
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

View file

@ -13,17 +13,17 @@ FULL_TRANSLATION = [
PARTIAL_TRANSLATION = [
"az", "bg", "bs", "cy", "cs",
"eo", "es", "fi", "ga", "hi", "hr",
"eo", "es", "eu", "fi", "ga", "hi", "hr",
"hy", "is", "ja", "jv", "ka", "kk",
"ko", "ky", "lt", "lv", "mk", "ml", "nl", "fy",
"nn", "pt", "pt-br", "sk", "sl", "sr", "sr-lat",
"sv", "sw", "te", "uz",
"sv", "sw", "te", "uz",
"zh", "zu", "he",
]
PROXY_LANGS = [
"af", "ar", "az", "be", "bs", "ca",
"cy", "el", "eo", "et", "fa",
"cy", "de", "el", "eo", "et", "eu", "fa", "fr",
"fy", "he", "hr", "hu", "hy",
"ia", "id", "is", "it", "ja", "kk",
"lv", "mk", "nb", "nn", "ro",

0
lib/view/__init__.py Normal file
View file

View file

@ -17,13 +17,17 @@ import sys
import re
import datetime
import json
try:
from astral import Astral, Location
except ImportError:
pass
from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION
import requests
from astral import LocationInfo
from astral import moon
from astral.sun import sun
import pytz
from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION, WEATHER_SYMBOL_WIDTH_VTE
from weather_data import get_weather_data
import spark
from . import v2
PRECONFIGURED_FORMAT = {
'1': u'%c %t',
@ -57,12 +61,13 @@ def render_temperature(data, query):
return temperature
def render_condition(data, query):
"""
condition (c)
"""Emoji encoded weather condition (c)
"""
weather_condition = WEATHER_SYMBOL[WWO_CODE[data['weatherCode']]]
return weather_condition
spaces = " "*(WEATHER_SYMBOL_WIDTH_VTE.get(weather_condition) - 1)
return weather_condition + spaces
def render_condition_fullname(data, query):
"""
@ -140,18 +145,18 @@ def render_wind(data, query):
degree = ""
if degree:
wind_direction = WIND_DIRECTION[((degree+22)%360)/45]
wind_direction = WIND_DIRECTION[((degree+22)%360)//45]
else:
wind_direction = ""
if query.get('use_ms_for_wind', False):
unit = ' m/s'
unit = 'm/s'
wind = u'%s%.1f%s' % (wind_direction, float(data['windspeedKmph'])/36.0*10.0, unit)
elif query.get('use_imperial', False):
unit = ' mph'
unit = 'mph'
wind = u'%s%s%s' % (wind_direction, data['windspeedMiles'], unit)
else:
unit = ' km/h'
unit = 'km/h'
wind = u'%s%s%s' % (wind_direction, data['windspeedKmph'], unit)
return wind
@ -161,35 +166,58 @@ def render_location(data, query):
location (l)
"""
return (data['override_location'] or data['location']) # .title()
return (data['override_location'] or data['location'])
def render_moonphase(_, query):
"""
"""moonpahse(m)
A symbol describing the phase of the moon
"""
astral = Astral()
moon_index = int(
int(32.0*astral.moon_phase(date=datetime.datetime.today())/28+2)%32/4
)
moon_phase = moon.phase(date=datetime.datetime.today())
moon_index = int(int(32.0*moon_phase/28+2)%32/4)
return MOON_PHASES[moon_index]
def render_moonday(_, query):
"""
"""moonday(M)
An number describing the phase of the moon (days after the New Moon)
"""
astral = Astral()
return str(int(astral.moon_phase(date=datetime.datetime.today())))
moon_phase = moon.phase(date=datetime.datetime.today())
return str(int(moon_phase))
def render_sunset(data, query):
location = data['location']
city_name = location
astral = Astral()
location = Location(('Nuremberg', 'Germany',
49.453872, 11.077298, 'Europe/Berlin', 0))
sun = location.sun(date=datetime.datetime.today(), local=True)
##################################
# this part should be rewritten
# this is just a temporary solution
def get_geodata(location):
text = requests.get("http://localhost:8004/%s" % location).text
return json.loads(text)
return str(sun['sunset'])
def render_dawn(data, query, local_time_of):
"""dawn (D)
Local time of dawn"""
return local_time_of("dawn")
def render_dusk(data, query, local_time_of):
"""dusk (d)
Local time of dusk"""
return local_time_of("dusk")
def render_sunrise(data, query, local_time_of):
"""sunrise (S)
Local time of sunrise"""
return local_time_of("sunrise")
def render_sunset(data, query, local_time_of):
"""sunset (s)
Local time of sunset"""
return local_time_of("sunset")
def render_zenith(data, query, local_time_of):
"""zenith (z)
Local time of zenith"""
return local_time_of("noon")
##################################
FORMAT_SYMBOL = {
'c': render_condition,
@ -200,17 +228,48 @@ FORMAT_SYMBOL = {
'l': render_location,
'm': render_moonphase,
'M': render_moonday,
's': render_sunset,
'p': render_precipitation,
'o': render_precipitation_chance,
'P': render_pressure,
}
FORMAT_SYMBOL_ASTRO = {
'D': render_dawn,
'd': render_dusk,
'S': render_sunrise,
's': render_sunset,
'z': render_zenith,
}
def render_line(line, data, query):
"""
Render format `line` using `data`
"""
def get_local_time_of():
location = data["location"]
geo_data = get_geodata(location)
city = LocationInfo()
city.latitude = geo_data["latitude"]
city.longitude = geo_data["longitude"]
city.timezone = geo_data["timezone"]
timezone = city.timezone
local_tz = pytz.timezone(timezone)
datetime_day_start = datetime.datetime.now()\
.replace(hour=0, minute=0, second=0, microsecond=0)
current_sun = sun(city.observer, date=datetime_day_start)
local_time_of = lambda x: current_sun[x]\
.replace(tzinfo=pytz.utc)\
.astimezone(local_tz)\
.strftime("%H:%M:%S")
return local_time_of
def render_symbol(match):
"""
Render one format symbol from re `match`
@ -220,13 +279,22 @@ def render_line(line, data, query):
symbol_string = match.group(0)
symbol = symbol_string[-1]
if symbol not in FORMAT_SYMBOL:
return ''
if symbol in FORMAT_SYMBOL:
render_function = FORMAT_SYMBOL[symbol]
return render_function(data, query)
if symbol in FORMAT_SYMBOL_ASTRO and local_time_of is not None:
render_function = FORMAT_SYMBOL_ASTRO[symbol]
return render_function(data, query, local_time_of)
render_function = FORMAT_SYMBOL[symbol]
return render_function(data, query)
return ''
return re.sub(r'%[^%]*[a-zA-Z]', render_symbol, line)
template_regexp = r'%[^%]*[a-zA-Z]'
for template_code in re.findall(template_regexp, line):
if template_code.lstrip("%") in FORMAT_SYMBOL_ASTRO:
local_time_of = get_local_time_of()
break
return re.sub(template_regexp, render_symbol, line)
def render_json(data):
output = json.dumps(data, indent=4, sort_keys=True)
@ -237,47 +305,41 @@ def render_json(data):
return output
def format_weather_data(format_line, location, override_location, full_address, data, query):
def format_weather_data(query, parsed_query, data):
"""
Format information about current weather `data` for `location`
with specified in `format_line` format
"""
if 'data' not in data:
return 'Unknown location; please try ~%s' % location
return 'Unknown location; please try ~%s' % parsed_query["location"]
format_line = parsed_query.get("view", "")
if format_line in PRECONFIGURED_FORMAT:
format_line = PRECONFIGURED_FORMAT[format_line]
if format_line == "j1":
return render_json(data['data'])
if format_line[:2] == "v2":
return spark.main(location,
override_location=override_location,
full_address=full_address, data=data,
view=format_line)
return v2.main(query, parsed_query, data)
current_condition = data['data']['current_condition'][0]
current_condition['location'] = location
current_condition['override_location'] = override_location
current_condition['location'] = parsed_query["location"]
current_condition['override_location'] = parsed_query["override_location_name"]
output = render_line(format_line, current_condition, query)
return output
def wttr_line(location, override_location_name, full_address, query, lang, fmt):
def wttr_line(query, parsed_query):
"""
Return 1line weather information for `location`
in format `line_format`
"""
location = parsed_query['location']
lang = parsed_query['lang']
format_line = query.get('format', fmt or '')
if format_line in PRECONFIGURED_FORMAT:
format_line = PRECONFIGURED_FORMAT[format_line]
weather_data = get_weather_data(location, lang)
output = format_weather_data(
format_line, location, override_location_name, full_address,
weather_data, query)
output = output.rstrip("\n")+"\n"
return output
data = get_weather_data(location, lang)
output = format_weather_data(query, parsed_query, data)
return output.rstrip("\n")+"\n"
def main():
"""
@ -288,8 +350,14 @@ def main():
query = {
'line': sys.argv[2],
}
parsed_query = {
"location": location,
"orig_location": location,
"language": "en",
"format": "v2",
}
sys.stdout.write(wttr_line(location, location, None, query, 'en', "v1"))
sys.stdout.write(wttr_line(query, parsed_query))
if __name__ == '__main__':
main()

53
lib/view/moon.py Normal file
View file

@ -0,0 +1,53 @@
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']
date = None
if '@' in location:
date = location[location.index('@')+1:]
location = location[:location.index('@')]
cmd = [globals.PYPHOON]
if lang:
cmd += ["-l", lang]
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 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

View file

@ -26,27 +26,23 @@ import re
import math
import json
import datetime
try:
import StringIO
except:
import io as StringIO
import io
import requests
import diagram
import pyjq
import pytz
import numpy as np
try:
from astral import Astral, Location
except ImportError:
pass
from astral import LocationInfo
from astral import moon, sun
from scipy.interpolate import interp1d
from babel.dates import format_datetime
from globals import WWO_KEY
import constants
import translations
import wttr_line
import parse_query
from . import line as wttr_line
if not sys.version_info >= (3, 0):
reload(sys) # noqa: F821
@ -74,9 +70,11 @@ def interpolate_data(input_data, max_width):
Resample `input_data` to number of `max_width` counts
"""
x = list(range(len(input_data)))
input_data = list(input_data)
input_data_len = len(input_data)
x = list(range(input_data_len))
y = input_data
xvals = np.linspace(0, len(input_data)-1, max_width)
xvals = np.linspace(0, input_data_len-1, max_width)
yinterp = interp1d(x, y, kind='cubic')
return yinterp(xvals)
@ -86,13 +84,16 @@ def jq_query(query, data_parsed):
"""
pyjq_data = pyjq.all(query, data_parsed)
data = map(float, pyjq_data)
data = list(map(float, pyjq_data))
return data
# }}}
# utils {{{
def colorize(string, color_code):
return "\033[%sm%s\033[0m" % (color_code, string)
def colorize(string, color_code, html_output=False):
if html_output:
return "<font color='#777777'>%s</font>" % (string)
else:
return "\033[%sm%s\033[0m" % (color_code, string)
# }}}
# draw_spark {{{
@ -140,11 +141,11 @@ def draw_spark(data, height, width, color_data):
orig_max_line = max_line
# aligning it
if len(max_line)/2 < j and len(max_line)/2 + j < width:
spaces = " "*(j - len(max_line)/2)
if len(max_line)//2 < j and len(max_line)//2 + j < width:
spaces = " "*(j - len(max_line)//2)
max_line = spaces + max_line # + spaces
max_line = max_line + " "*(width - len(max_line))
elif len(max_line)/2 + j >= width:
elif len(max_line)//2 + j >= width:
max_line = " "*(width - len(max_line)) + max_line
max_line = max_line.replace(orig_max_line, colorize(orig_max_line, "38;5;33"))
@ -164,13 +165,13 @@ def draw_diagram(data, height, width):
option.size = diagram.Point([width, height])
option.mode = 'g'
stream = StringIO.StringIO()
stream = io.BytesIO()
gram = diagram.DGWrapper(
data=[list(data), range(len(data))],
dg_option=option,
ostream=stream)
gram.show()
return stream.getvalue()
return stream.getvalue().decode("utf-8")
# }}}
# draw_date {{{
@ -189,7 +190,7 @@ def draw_date(config, geo_data):
datetime_ = datetime_day_start + datetime.timedelta(hours=24*day)
date = format_datetime(datetime_, "EEE dd MMM", locale=locale, tzinfo=tzinfo)
spaces = ((24-len(date))/2)*" "
spaces = ((24-len(date))//2)*" "
date = spaces + date + spaces
date = " "*(24-len(date)) + date
answer += date
@ -241,10 +242,7 @@ def draw_time(geo_data):
def draw_astronomical(city_name, geo_data):
datetime_day_start = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
a = Astral()
a.solar_depression = 'civil'
city = Location()
city = LocationInfo()
city.latitude = geo_data["latitude"]
city.longitude = geo_data["longitude"]
city.timezone = geo_data["timezone"]
@ -256,35 +254,54 @@ def draw_astronomical(city_name, geo_data):
current_date = (
datetime_day_start
+ datetime.timedelta(hours=1*time_interval)).replace(tzinfo=pytz.timezone(geo_data["timezone"]))
sun = city.sun(date=current_date, local=False)
dawn = sun['dawn'] # .replace(tzinfo=None)
dusk = sun['dusk'] # .replace(tzinfo=None)
sunrise = sun['sunrise'] # .replace(tzinfo=None)
sunset = sun['sunset'] # .replace(tzinfo=None)
try:
dawn = sun.dawn(city.observer, date=current_date)
except ValueError:
dawn = current_date
try:
dusk = sun.dusk(city.observer, date=current_date)
except ValueError:
dusk = current_date + datetime.timedelta(hours=24)
try:
sunrise = sun.sunrise(city.observer, date=current_date)
except ValueError:
sunrise = current_date
try:
sunset = sun.sunset(city.observer, date=current_date)
except ValueError:
sunset = current_date + datetime.timedelta(hours=24)
char = "."
if current_date < dawn:
char = " "
elif current_date > dusk:
char = " "
elif dawn < current_date and current_date < sunrise:
elif dawn <= current_date and current_date <= sunrise:
char = u""
elif sunset < current_date and current_date < dusk:
elif sunset <= current_date and current_date <= dusk:
char = u""
elif sunrise < current_date and current_date < sunset:
elif sunrise <= current_date and current_date <= sunset:
char = u""
answer += char
# moon
if time_interval % 3 == 0:
moon_phase = city.moon_phase(
if time_interval in [0,23,47,69]: # time_interval % 3 == 0:
moon_phase = moon.phase(
date=datetime_day_start + datetime.timedelta(hours=time_interval))
moon_phase_emoji = constants.MOON_PHASES[int(math.floor(moon_phase*1.0/28.0*8+0.5)) % len(constants.MOON_PHASES)]
if time_interval in [0, 24, 48, 69]:
moon_line += moon_phase_emoji + " "
else:
moon_phase_emoji = constants.MOON_PHASES[
int(math.floor(moon_phase*1.0/28.0*8+0.5)) % len(constants.MOON_PHASES)]
# if time_interval in [0, 24, 48, 69]:
moon_line += moon_phase_emoji # + " "
elif time_interval % 3 == 0:
if time_interval not in [24,28]: #se:
moon_line += " "
else:
moon_line += " "
answer = moon_line + "\n" + answer + "\n"
@ -333,7 +350,7 @@ def draw_wind(data, color_data):
degree = int(degree)
if degree:
wind_direction = constants.WIND_DIRECTION[((degree+22)%360)/45]
wind_direction = constants.WIND_DIRECTION[((degree+22)%360)//45]
else:
wind_direction = ""
@ -364,9 +381,9 @@ def add_frame(output, width, config):
output = "\n".join(u""+(x or empty_line)+u"" for x in output.splitlines()) + "\n"
weather_report = \
translations.CAPTION[config["lang"]] \
translations.CAPTION[config.get("lang") or "en"] \
+ " " \
+ (config["override_location"] or config["location"])
+ (config["override_location_name"] or config["location"])
caption = u"" + " " + weather_report + " " + u""
output = u"" + caption + u""*(width-len(caption)) + u"\n" \
@ -429,7 +446,7 @@ def generate_panel(data_parsed, geo_data, config):
# }}}
# textual information {{{
def textual_information(data_parsed, geo_data, config):
def textual_information(data_parsed, geo_data, config, html_output=False):
"""
Add textual information about current weather and
astronomical conditions
@ -453,8 +470,10 @@ def textual_information(data_parsed, geo_data, config):
return output
def _colorize(text, color):
return colorize(text, color, html_output=html_output)
city = Location()
city = LocationInfo()
city.latitude = geo_data["latitude"]
city.longitude = geo_data["longitude"]
city.timezone = geo_data["timezone"]
@ -464,7 +483,6 @@ def textual_information(data_parsed, geo_data, config):
datetime_day_start = datetime.datetime.now()\
.replace(hour=0, minute=0, second=0, microsecond=0)
sun = city.sun(date=datetime_day_start, local=True)
format_line = "%c %C, %t, %h, %w, %P"
current_condition = data_parsed['data']['current_condition'][0]
@ -474,22 +492,41 @@ def textual_information(data_parsed, geo_data, config):
output.append('Timezone: %s' % timezone)
tmp_output = []
tmp_output.append(' Now: %%{{NOW(%s)}}' % timezone)
tmp_output.append('Dawn: %s'
% str(sun['dawn'].strftime("%H:%M:%S")))
tmp_output.append('Sunrise: %s'
% str(sun['sunrise'].strftime("%H:%M:%S")))
tmp_output.append(' Zenith: %s'
% str(sun['noon'].strftime("%H:%M:%S ")))
tmp_output.append('Sunset: %s'
% str(sun['sunset'].strftime("%H:%M:%S")))
tmp_output.append('Dusk: %s'
% str(sun['dusk'].strftime("%H:%M:%S")))
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"])
color_code = "38;5;246"
tmp_output = [
re.sub("^([A-Za-z]*:)", lambda m: colorize(m.group(1), color_code), x)
re.sub("^([A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"), x)
for x in tmp_output]
output.append(
@ -517,9 +554,9 @@ def textual_information(data_parsed, geo_data, config):
))
output = [
re.sub("^( *[A-Za-z]*:)", lambda m: colorize(m.group(1), color_code),
re.sub("^( +[A-Za-z]*:)", lambda m: colorize(m.group(1), color_code),
re.sub(r"(\|)", lambda m: colorize(m.group(1), color_code), x)))
re.sub("^( *[A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"),
re.sub("^( +[A-Za-z]*:)", lambda m: _colorize(m.group(1), "2"),
re.sub(r"(\|)", lambda m: _colorize(m.group(1), "2"), x)))
for x in output]
return "".join("%s\n" % x for x in output)
@ -531,24 +568,42 @@ def get_geodata(location):
return json.loads(text)
# }}}
def main(location, override_location=None, data=None, full_address=None, view=None):
config = {
"lang": "en",
"locale": "en_US",
"location": location,
"override_location": override_location,
"full_address": full_address,
"view": view,
}
def main(query, parsed_query, data):
parsed_query["locale"] = "en_US"
location = parsed_query["location"]
html_output = parsed_query["html_output"]
geo_data = get_geodata(location)
if data is None:
data_parsed = get_data(config)
data_parsed = get_data(parsed_query)
else:
data_parsed = data
output = generate_panel(data_parsed, geo_data, config)
output += textual_information(data_parsed, geo_data, config)
if html_output:
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)
return output
if __name__ == '__main__':

166
lib/view/wttr.py Normal file
View file

@ -0,0 +1,166 @@
# 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, 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)
if location_not_found or \
(returncode != 0 \
and ('Unable to find any matching weather'
' location to the parsed_query submitted') in stderr):
stdout, stderr, returncode = _wego_wrapper(NOT_FOUND_LOCATION, parsed_query)
location_not_found = True
stdout += get_message('NOT_FOUND_MESSAGE', lang)
first_line, stdout = _wego_postprocessing(location, parsed_query, stdout)
if html:
return _htmlize(stdout, first_line, parsed_query)
return stdout
def _wego_wrapper(location, parsed_query):
lang = parsed_query['lang']
location_name = parsed_query['override_location_name']
cmd = [WEGO, '--city=%s' % location]
if parsed_query.get('inverted_colors'):
cmd += ['-inverse']
if parsed_query.get('use_ms_for_wind'):
cmd += ['-wind_in_ms']
if parsed_query.get('narrow'):
cmd += ['-narrow']
if lang and lang in SUPPORTED_LANGS:
cmd += ['-lang=%s'%lang]
if parsed_query.get('use_imperial', False):
cmd += ['-imperial']
if location_name:
cmd += ['-location_name', location_name]
proc = Popen(cmd, stdout=PIPE, stderr=PIPE)
stdout, stderr = proc.communicate()
stdout = stdout.decode("utf-8")
stderr = stderr.decode("utf-8")
return stdout, stderr, proc.returncode
def _wego_postprocessing(location, parsed_query, stdout):
full_address = parsed_query['full_address']
lang = parsed_query['lang']
if 'days' in parsed_query:
if parsed_query['days'] == '0':
stdout = "\n".join(stdout.splitlines()[:7]) + "\n"
if parsed_query['days'] == '1':
stdout = "\n".join(stdout.splitlines()[:17]) + "\n"
if parsed_query['days'] == '2':
stdout = "\n".join(stdout.splitlines()[:27]) + "\n"
first = stdout.splitlines()[0]
rest = stdout.splitlines()[1:]
if parsed_query.get('no-caption', False):
if ':' in first:
first = first.split(":", 1)[1]
stdout = "\n".join([first.strip()] + rest) + "\n"
if parsed_query.get('no-terminal', False):
stdout = remove_ansi(stdout)
if parsed_query.get('no-city', False):
stdout = "\n".join(stdout.splitlines()[2:]) + "\n"
if full_address \
and parsed_query.get('format', 'txt') != 'png' \
and (not parsed_query.get('no-city')
and not parsed_query.get('no-caption')
and not parsed_query.get('days') == '0'):
line = "%s: %s [%s]\n" % (
get_message('LOCATION', lang),
full_address,
location)
stdout += line
if parsed_query.get('padding', False):
lines = [x.rstrip() for x in stdout.splitlines()]
max_l = max(len(remove_ansi(x)) for x in lines)
last_line = " "*max_l + " .\n"
stdout = " \n" + "\n".join(" %s " %x for x in lines) + "\n" + last_line
return first, stdout
def _htmlize(ansi_output, title, parsed_query):
"""Return HTML representation of `ansi_output`.
Use `title` as the title of the page.
Format page according to query parameters from `parsed_query`."""
cmd = ["bash", ANSI2HTML, "--palette=solarized"]
if not parsed_query.get('inverted_colors'):
cmd += ["--bg=dark"]
proc = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)
stdout, stderr = proc.communicate(ansi_output.encode("utf-8"))
stdout = stdout.decode("utf-8")
stderr = stderr.decode("utf-8")
if proc.returncode != 0:
error(stdout + stderr)
if parsed_query.get('inverted_colors'):
stdout = stdout.replace(
'<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,
}

View file

@ -1,247 +0,0 @@
# vim: set encoding=utf-8
from __future__ import print_function
import gevent
from gevent.pywsgi import WSGIServer
from gevent.queue import Queue
from gevent.monkey import patch_all
from gevent.subprocess import Popen, PIPE, STDOUT
patch_all()
import os
import re
import time
import dateutil.parser
from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS
from globals import WEGO, PYPHOON, CACHEDIR, ANSI2HTML, \
NOT_FOUND_LOCATION, DEFAULT_LOCATION, TEST_FILE, \
log, error
def _is_invalid_location(location):
if '.png' in location:
return True
def remove_ansi(sometext):
ansi_escape = re.compile(r'(\x9B|\x1B\[)[0-?]*[ -\/]*[@-~]')
return ansi_escape.sub('', sometext)
def get_wetter(location, ip, html=False, lang=None, query=None, location_name=None, full_address=None, url=None):
local_url = url
local_location = location
def get_opengraph():
if local_url is None:
url = ""
else:
url = local_url.encode('utf-8')
if local_location is None:
location = ""
else:
location = local_location.encode('utf-8')
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,
'location': location,
}
# '<meta property="og:title" content="Weather report: %(location)s" />'
# '<meta content="Partly cloudy // 6-8 °C // ↑ 9 km/h // 10 km // 0.4 mm" property="og:description" />'
def get_filename(location, lang=None, query=None, location_name=None):
location = location.replace('/', '_')
timestamp = time.strftime( "%Y%m%d%H", time.localtime() )
imperial_suffix = ''
if query.get('use_imperial', False):
imperial_suffix = '-imperial'
lang_suffix = ''
if lang is not None:
lang_suffix = '-lang_%s' % lang
if query != None:
query_line = "_" + "_".join("%s=%s" % (key, value) for (key, value) in query.items())
else:
query_line = ""
if location_name is None:
location_name = ""
return "%s/%s/%s%s%s%s%s" % (CACHEDIR, location, timestamp, imperial_suffix, lang_suffix, query_line, location_name)
def save_weather_data(location, filename, lang=None, query=None, location_name=None, full_address=None):
if _is_invalid_location( location ):
error("Invalid location: %s" % location)
NOT_FOUND_MESSAGE_HEADER = ""
while True:
location_not_found = False
if location in [ "test-thunder" ]:
test_name = location[5:]
test_file = TEST_FILE.replace('NAME', test_name)
stdout = open(test_file, 'r').read()
stderr = ""
break
if location == NOT_FOUND_LOCATION:
location_not_found = True
location = DEFAULT_LOCATION
cmd = [WEGO, '--city=%s' % location]
if query.get('inverted_colors'):
cmd += ['-inverse']
if query.get('use_ms_for_wind'):
cmd += ['-wind_in_ms']
if query.get('narrow'):
cmd += ['-narrow']
if lang and lang in SUPPORTED_LANGS:
cmd += ['-lang=%s'%lang]
if query.get('use_imperial', False):
cmd += ['-imperial']
if location_name:
cmd += ['-location_name', location_name]
p = Popen(cmd, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate()
if p.returncode != 0:
print("ERROR: location not found: %s" % location)
if 'Unable to find any matching weather location to the query submitted' in stderr:
if location != NOT_FOUND_LOCATION:
NOT_FOUND_MESSAGE_HEADER = u"ERROR: %s: %s\n---\n\n" % (get_message('UNKNOWN_LOCATION', lang), location)
location = NOT_FOUND_LOCATION
continue
error(stdout + stderr)
break
dirname = os.path.dirname(filename)
if not os.path.exists(dirname):
os.makedirs(dirname)
if location_not_found:
stdout += get_message('NOT_FOUND_MESSAGE', lang).encode('utf-8')
stdout = NOT_FOUND_MESSAGE_HEADER.encode('utf-8') + stdout
if 'days' in query:
if query['days'] == '0':
stdout = "\n".join(stdout.splitlines()[:7]) + "\n"
if query['days'] == '1':
stdout = "\n".join(stdout.splitlines()[:17]) + "\n"
if query['days'] == '2':
stdout = "\n".join(stdout.splitlines()[:27]) + "\n"
first = stdout.splitlines()[0].decode('utf-8')
rest = stdout.splitlines()[1:]
if query.get('no-caption', False):
separator = None
if ':' in first:
separator = ':'
if u'' in first:
separator = u''
if separator:
first = first.split(separator,1)[1]
stdout = "\n".join([first.strip().encode('utf-8')] + rest) + "\n"
if query.get('no-terminal', False):
stdout = remove_ansi(stdout)
if query.get('no-city', False):
stdout = "\n".join(stdout.splitlines()[2:]) + "\n"
if full_address \
and query.get('format', 'txt') != 'png' \
and (not query.get('no-city')
and not query.get('no-caption')
and not query.get('days') == '0'):
line = "%s: %s [%s]\n" % (
get_message('LOCATION', lang).encode('utf-8'),
full_address.encode('utf-8'),
location.encode('utf-8'))
stdout += line
if query.get('padding', False):
lines = [x.rstrip() for x in stdout.splitlines()]
max_l = max(len(remove_ansi(x).decode('utf8')) for x in lines)
last_line = " "*max_l + " .\n"
stdout = " \n" + "\n".join(" %s " %x for x in lines) + "\n" + last_line
open(filename, 'w').write(stdout)
cmd = ["bash", ANSI2HTML, "--palette=solarized"]
if not query.get('inverted_colors'):
cmd += ["--bg=dark"]
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE )
stdout, stderr = p.communicate(stdout)
if p.returncode != 0:
error(stdout + stderr)
if query.get('inverted_colors'):
stdout = stdout.replace('<body class="">', '<body class="" style="background:white;color:#777777">')
title = "<title>%s</title>" % first.encode('utf-8')
opengraph = get_opengraph()
stdout = re.sub("<head>", "<head>" + title + opengraph, stdout)
open(filename+'.html', 'w').write(stdout)
filename = get_filename(location, lang=lang, query=query, location_name=location_name)
if not os.path.exists(filename):
save_weather_data(location, filename, lang=lang, query=query, location_name=location_name, full_address=full_address)
if html:
filename += '.html'
return open(filename).read()
def get_moon(location, html=False, lang=None, query=None):
if query is None:
query = {}
date = None
if '@' in location:
date = location[location.index('@')+1:]
location = location[:location.index('@')]
cmd = [PYPHOON]
if date:
try:
dateutil.parser.parse(date)
except Exception as e:
print("ERROR: %s" % e)
else:
cmd += [date]
env = os.environ.copy()
if lang:
env['LANG'] = lang
p = Popen(cmd, stdout=PIPE, stderr=PIPE, env=env)
stdout = p.communicate()[0]
if query.get('no-terminal', False):
stdout = remove_ansi(stdout)
if html:
p = Popen(["bash", ANSI2HTML, "--palette=solarized", "--bg=dark"], stdin=PIPE, stdout=PIPE, stderr=PIPE)
stdout, stderr = p.communicate(stdout)
if p.returncode != 0:
error(stdout + stderr)
return stdout

View file

@ -6,15 +6,18 @@ 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 wttrin_png
import fmt.png
import parse_query
from translations import get_message, FULL_TRANSLATION, PARTIAL_TRANSLATION, SUPPORTED_LANGS
from buttons import add_buttons
from globals import get_help_file, log, \
from globals import get_help_file, \
BASH_FUNCTION_FILE, TRANSLATION_FILE, LOG_FILE, \
NOT_FOUND_LOCATION, \
MALFORMED_RESPONSE_HTML_PAGE, \
@ -22,17 +25,20 @@ from globals import get_help_file, log, \
MY_EXTERNAL_IP, QUERY_LIMITS
from location import is_location_blocked, location_processing
from limits import Limits
from wttr import get_wetter, get_moon
from wttr_line import wttr_line
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.DEBUG, format='%(asctime)s %(message)s')
logging.basicConfig(filename=LOG_FILE, level=logging.INFO, format='%(asctime)s %(message)s')
LIMITS = Limits(whitelist=[MY_EXTERNAL_IP], limits=QUERY_LIMITS)
TASKS = ThreadPool(25)
def show_text_file(name, lang):
"""
show static file `name` for `lang`
@ -51,12 +57,10 @@ def show_text_file(name, lang):
text = text\
.replace('NUMBER_OF_LANGUAGES', str(len(SUPPORTED_LANGS)))\
.replace('SUPPORTED_LANGUAGES', ' '.join(SUPPORTED_LANGS))
return text.decode('utf-8')
return text
def client_ip_address(request):
"""
Return client ip address for `request`.
Flask related
def _client_ip_address(request):
"""Return client ip address for flask `request`.
"""
if request.headers.getlist("X-PNG-Query-For"):
@ -112,55 +116,56 @@ def _parse_language_header(header):
elif lang == 'en':
yield None, lang_tuple[1]
try:
return max(supported_langs(), key=lambda lang_tuple:lang_tuple[1])[0]
return max(supported_langs(), key=lambda lang_tuple: lang_tuple[1])[0]
except ValueError:
return None
return _find_supported_language(_parse_accept_language(header))
def get_answer_language_and_format(request):
def get_answer_language_and_view(request):
"""
Return preferred answer language based on
domain name, query arguments and headers
"""
lang = None
fmt = None
view_name = None
hostname = request.headers['Host']
if hostname != 'wttr.in' and hostname.endswith('.wttr.in'):
lang = hostname[:-8]
if lang == "v2":
fmt = "v2"
if lang.startswith("v2"):
view_name = lang
lang = None
if 'lang' in request.args:
lang = request.args.get('lang')
if lang.lower() == 'none':
lang = None
header_accept_language = request.headers.get('Accept-Language', '')
if lang is None and header_accept_language:
lang = _parse_language_header(header_accept_language)
return lang, fmt
return lang, view_name
def get_output_format(request, query):
def get_output_format(query, parsed_query):
"""
Return preferred output format: ansi, text, html or png
based on arguments and headers in `request`.
Return new location (can be rewritten)
"""
if 'format' in query:
return False
# FIXME
user_agent = request.headers.get('User-Agent', '').lower()
if query.get('force-ansi'):
if ('view' in query and not query["view"].startswith("v2")) \
or parsed_query.get("png_filename") \
or query.get('force-ansi'):
return False
user_agent = parsed_query.get("user_agent", "").lower()
html_output = not any(agent in user_agent for agent in PLAIN_TEXT_AGENTS)
return html_output
def cyclic_location_selection(locations, period):
"""
Return one of `locations` (: separated list)
def _cyclic_location_selection(locations, period):
"""Return one of `locations` (: separated list)
basing on the current time and query interval `period`
"""
@ -173,144 +178,202 @@ def cyclic_location_selection(locations, period):
except ValueError:
period = 1
index = int(time.time())/period % len(locations)
index = int(time.time()/period) % len(locations)
return locations[index]
def wttr(location, request):
def _response(parsed_query, query, fast_mode=False):
"""Create response text based on `parsed_query` and `query` data.
If `fast_mode` is True, process only requests that can
be handled very fast (cached and static files).
"""
Main rendering function, it processes incoming weather queries.
Depending on user agent it returns output in HTML or ANSI format.
answer = None
cache_signature = cache.get_signature(
parsed_query["user_agent"],
parsed_query["request_url"],
parsed_query["ip_addr"],
parsed_query["lang"])
answer = cache.get(cache_signature)
if parsed_query['orig_location'] in PLAIN_TEXT_PAGES:
answer = show_text_file(parsed_query['orig_location'], parsed_query['lang'])
if parsed_query['html_output']:
answer = render_template('index.html', body=answer)
if answer or fast_mode:
return answer
# at this point, we could not handle the query fast,
# so we handle it with all available logic
loc = (parsed_query['orig_location'] or "").lower()
if parsed_query.get("view"):
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'):
# originally it was just a usual function call,
# but it was a blocking call, so it was moved
# to separate threads:
#
# output = fmt.png.render_ansi(
# output, options=parsed_query)
result = TASKS.spawn(fmt.png.render_ansi, output, options=parsed_query)
output = result.get()
else:
if query.get('days', '3') != '0' \
and not query.get('no-follow-line') \
and ((parsed_query.get("view") or "v2")[:2] in ["v2"]):
if parsed_query['html_output']:
output = add_buttons(output)
else:
output += '\n' + get_message('FOLLOW_ME', parsed_query['lang']) + '\n'
return cache.store(cache_signature, output)
def parse_request(location, request, query, fast_mode=False):
"""Parse request and provided extended information for the query,
including location data, language, output format, view, etc.
Incoming data:
request.args
request.headers
request.remote_addr
request.referrer
request.query_string
`location` location name extracted from the query url
`request.args`
`request.headers`
`request.remote_addr`
`request.referrer`
`request.query_string`
`query` parsed command line arguments
Parameters priorities (from low to high):
* HTTP-header
* Domain name
* URL
* Filename
Return: dictionary with parsed parameters
"""
def _wrap_response(response_text, html_output):
response = make_response(response_text)
response.mimetype = 'text/html' if html_output else 'text/plain'
return response
if is_location_blocked(location):
return ""
ip_addr = client_ip_address(request)
try:
LIMITS.check_ip(ip_addr)
except RuntimeError as exception:
return str(exception)
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))
lang, fmt = get_answer_language_and_format(request)
query = parse_query.parse_query(request.args)
html_output = get_output_format(request, query)
user_agent = request.headers.get('User-Agent', '').lower()
parsed_query = {
'ip_addr': _client_ip_address(request),
'user_agent': request.headers.get('User-Agent', '').lower(),
'request_url': request.url,
}
# generating cache signature
cache_signature = cache.get_signature(user_agent, request.url, ip_addr, lang)
answer = cache.get(cache_signature)
if answer:
return _wrap_response(answer, html_output)
if png_filename:
parsed_query["png_filename"] = png_filename
parsed_query.update(parse_query.parse_wttrin_png_name(png_filename))
if location in PLAIN_TEXT_PAGES:
help_ = show_text_file(location, lang)
if html_output:
return _wrap_response(render_template('index.html', body=help_), html_output)
return _wrap_response(help_, html_output)
lang, _view = get_answer_language_and_view(request)
if location and ':' in location:
location = cyclic_location_selection(location, query.get('period', 1))
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)
orig_location = location
parsed_query["html_output"] = get_output_format(query, parsed_query)
if not png_filename:
if not fast_mode: # not png_filename and not fast_mode:
location, override_location_name, full_address, country, query_source_location = \
location_processing(location, ip_addr)
location_processing(parsed_query["location"], parsed_query["ip_addr"])
us_ip = query_source_location[1] == 'United States' and 'slack' not in user_agent
us_ip = query_source_location[1] == 'United States' \
and 'slack' not in parsed_query['user_agent']
query = parse_query.metric_or_imperial(query, lang, us_ip=us_ip)
# logging query
orig_location_utf8 = (orig_location or "").encode('utf-8')
location_utf8 = location.encode('utf-8')
use_imperial = query.get('use_imperial', False)
log(" ".join(map(str,
[ip_addr, user_agent, orig_location_utf8, location_utf8, use_imperial, lang])))
if country and location != NOT_FOUND_LOCATION:
location = "%s,%s" % (location, country)
# We are ready to return the answer
try:
if fmt or 'format' in query:
response_text = wttr_line(
location, override_location_name, full_address, query, lang, fmt)
fmt = fmt or query.get('format')
response_text = cache.store(cache_signature, response_text)
parsed_query.update({
'location': location,
'override_location_name': override_location_name,
'full_address': full_address,
'country': country,
'query_source_location': query_source_location})
return _wrap_response(
response_text,
html_output)
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, png_filename=None):
if not isinstance(response_text, str) and \
not isinstance(response_text, bytes):
return response_text
if png_filename:
options = {
'ip_addr': ip_addr,
'lang': lang,
'location': location}
options.update(query)
response = make_response(send_file(
io.BytesIO(response_text),
attachment_filename=png_filename,
mimetype='image/png'))
cached_png_file = wttrin_png.make_wttr_in_png(png_filename, options=options)
response = make_response(send_file(cached_png_file,
attachment_filename=png_filename,
mimetype='image/png'))
for key, value in {
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0',
}.items():
response.headers[key] = value
# Trying to disable github caching
return response
if orig_location and (orig_location.lower() == 'moon' or orig_location.lower().startswith('moon@')):
output = get_moon(orig_location, html=html_output, lang=lang, query=query)
else:
output = get_wetter(location, ip_addr,
html=html_output,
lang=lang,
query=query,
location_name=override_location_name,
full_address=full_address,
url=request.url,
)
response = make_response(response_text)
response.mimetype = 'text/html' if html_output else 'text/plain'
return response
if query.get('days', '3') != '0' and not query.get('no-follow-line'):
if html_output:
output = add_buttons(output)
else:
#output += '\n' + get_message('NEW_FEATURE', lang).encode('utf-8')
output += '\n' + get_message('FOLLOW_ME', lang).encode('utf-8') + '\n'
if is_location_blocked(location):
return ""
return _wrap_response(output, html_output)
try:
LIMITS.check_ip(_client_ip_address(request))
except RuntimeError as exception:
return str(exception)
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)
try:
if not response:
parsed_query = parse_request(location, request, query)
response = _response(parsed_query, query)
# pylint: disable=broad-except
except Exception as exception:
# if 'Malformed response' in str(exception) \
# or 'API key has reached calls per day allowed limit' in str(exception):
if html_output:
return _wrap_response(MALFORMED_RESPONSE_HTML_PAGE, html_output)
return _wrap_response(get_message('CAPACITY_LIMIT_REACHED', lang).encode('utf-8'), html_output)
# logging.error("Exception has occured", exc_info=1)
# return "ERROR"
logging.error("Exception has occured", exc_info=1)
if parsed_query['html_output']:
response = MALFORMED_RESPONSE_HTML_PAGE
else:
response = get_message('CAPACITY_LIMIT_REACHED', parsed_query['lang'])
# 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'],
png_filename=parsed_query.get('png_filename'))
if __name__ == "__main__":
import doctest

View file

@ -1,323 +0,0 @@
#!/usr/bin/python
#vim: encoding=utf-8
from __future__ import print_function
import sys
import os
import re
import time
"""
This module is used to generate png-files for wttr.in queries.
The only exported function is:
make_wttr_in_png(filename)
in filename (in the shortname) is coded the weather query.
The function saves the weather report in the file and returns None.
"""
import requests
from PIL import Image, ImageFont, ImageDraw
import pyte.screens
# downloaded from https://gist.github.com/2204527
# described/recommended here:
#
# http://stackoverflow.com/questions/9868792/find-out-the-unicode-script-of-a-character
#
import unicodedata2
MYDIR = os.path.abspath(os.path.dirname(os.path.dirname('__file__')))
sys.path.append("%s/lib/" % MYDIR)
import parse_query
from globals import PNG_CACHE, log
COLS = 180
ROWS = 100
CHAR_WIDTH = 7
CHAR_HEIGHT = 14
# How to find font for non-standard scripts:
#
# $ fc-list :lang=ja
FONT_SIZE = 12
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",
}
def color_mapping(color):
"""
Convert pyte color to PIL color
"""
if color == 'default':
return 'lightgray'
if color in ['green', 'black', 'cyan', 'blue', 'brown']:
return color
try:
return (int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16))
except:
# 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
"""
cat = unicodedata2.script_cat(char)[0]
if char == u'':
return 'Han'
if cat in ['Latin', 'Common']:
return 'default'
else:
return cat
def gen_term(filename, buf, options=None):
buf = strip_buf(buf)
cols = max(len(x) for x in buf)
rows = len(buf)
image = Image.new('RGB', (cols * CHAR_WIDTH, rows * CHAR_HEIGHT))
buf = buf[-ROWS:]
draw = ImageDraw.Draw(image)
font = {}
for cat in FONT_CAT:
font[cat] = ImageFont.truetype(FONT_CAT[cat], FONT_SIZE)
x_pos = 0
y_pos = 0
for line in buf:
x_pos = 0
for char in line:
current_color = color_mapping(char.fg)
if char.bg != 'default':
draw.rectangle(
((x_pos, y_pos),
(x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)),
fill=color_mapping(char.bg))
if char.data:
cat = script_category(char.data)
if cat not in font:
log("Unknown font category: %s" % cat)
draw.text(
(x_pos, y_pos),
char.data,
font=font.get(cat, font.get('default')),
fill=current_color)
#sys.stdout.write(c.data)
x_pos += CHAR_WIDTH
y_pos += CHAR_HEIGHT
#sys.stdout.write('\n')
if 'transparency' in options:
transparency = options.get('transparency', '255')
try:
transparency = int(transparency)
except:
transparceny = 255
if transparency < 0:
transparency = 0
if transparency > 255:
transparency = 255
image = image.convert("RGBA")
datas = image.getdata()
new_data = []
for item in datas:
new_item = tuple(list(item[:3]) + [transparency])
new_data.append(new_item)
image.putdata(new_data)
image.save(filename)
def typescript_to_one_frame(png_file, text, options=None):
"""
Render text (terminal sequence) in png_file
"""
# fixing some broken characters because of bug #... in pyte 6.0
text = text.replace('Н', 'H').replace('Ν', 'N')
screen = pyte.screens.Screen(COLS, ROWS)
#screen.define_charset("B", "(")
stream = pyte.streams.ByteStream()
stream.attach(screen)
stream.feed(text)
buf = sorted(screen.buffer.items(), key=lambda x: x[0])
buf = [[x[1] for x in sorted(line[1].items(), key=lambda x: x[0])] for line in buf]
gen_term(png_file, buf, options=options)
#
# wttr.in related functions
#
def parse_wttrin_png_name(name):
"""
Parse the PNG filename and return the result as a dictionary.
For example:
input = City_200x_lang=ru.png
output = {
"lang": "ru",
"width": "200",
"filetype": "png",
"location": "City"
}
"""
parsed = {}
to_be_parsed = {}
if name.lower()[-4:] == '.png':
parsed['filetype'] = 'png'
name = name[:-4]
parts = name.split('_')
parsed['location'] = parts[0]
for part in parts[1:]:
if re.match('(?:[0-9]+)x', part):
parsed['width'] = part[:-1]
elif re.match('x(?:[0-9]+)', part):
parsed['height'] = part[1:]
elif re.match(part, '(?:[0-9]+)x(?:[0-9]+)'):
parsed['width'], parsed['height'] = part.split('x', 1)
elif '=' in part:
arg, val = part.split('=', 1)
to_be_parsed[arg] = val
else:
to_be_parsed[part] = ''
parsed.update(parse_query.parse_query(to_be_parsed))
return parsed
def make_wttrin_query(parsed):
"""
Convert parsed data into query name
"""
for key in ['width', 'height', 'filetype']:
if key in parsed:
del parsed[key]
location = parsed['location']
del parsed['location']
args = []
if 'options' in parsed:
args = [parsed['options']]
del parsed['options']
else:
args = []
for key, val in parsed.items():
args.append('%s=%s' % (key, val))
args.append('filetype=png')
url = "http://wttr.in/%s" % location
if args != []:
url += "?%s" % ("&".join(args))
return url
def make_wttr_in_png(png_name, options=None):
"""
The function saves the weather report in the file and returns None.
The weather query is coded in filename (in the shortname).
"""
parsed = parse_wttrin_png_name(png_name)
# if location is MyLocation it should be overriden
# with autodetected location (from options)
if parsed.get('location', 'MyLocation') == 'MyLocation' or not parsed.get('location', ''):
del parsed['location']
if options is not None:
for key, val in options.items():
if key not in parsed:
parsed[key] = val
url = make_wttrin_query(parsed)
timestamp = time.strftime("%Y%m%d%H", time.localtime())
cached_basename = url[14:].replace('/','_')
cached_png_file = "%s/%s/%s.png" % (PNG_CACHE, timestamp, cached_basename)
dirname = os.path.dirname(cached_png_file)
if not os.path.exists(dirname):
os.makedirs(dirname)
if os.path.exists(cached_png_file):
return cached_png_file
headers = {'X-PNG-Query-For': options.get('ip_addr', '1.1.1.1')}
text = requests.get(url, headers=headers).text.replace('\n', '\r\n')
curl_output = text.encode('utf-8')
typescript_to_one_frame(cached_png_file, curl_output, options=parsed)
return cached_png_file

View file

@ -8,14 +8,17 @@ pylint
cyrtranslit
astral
timezonefinder==2.1.2
Pillow
pyte
python-dateutil
diagram
pyjq
scipy
numpy
pillow
babel
pylru
pysocks
supervisor
numba
emoji
grapheme

BIN
share/emoji/☀️.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
share/emoji/☁️.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
share/emoji/⛅️.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
share/emoji/⛈.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
share/emoji/✨.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
share/emoji/❄️.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
share/emoji/🌑.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
share/emoji/🌒.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
share/emoji/🌓.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
share/emoji/🌔.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
share/emoji/🌕.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
share/emoji/🌖.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
share/emoji/🌗.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
share/emoji/🌘.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
share/emoji/🌦.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
share/emoji/🌧.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
share/emoji/🌨.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
share/emoji/🌩.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
share/emoji/🌫.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -10,6 +10,7 @@ Translated/improved/corrected by:
Grigor Khachatryan @grigortw
* Azerbaijani: Dmytro Nikitiuk, Elsevar Abbasov,
Eldar Velibekov (@welibekov on github)
* Basque: Iker Sagasti (@isagasti on github)
* Belarusian: Igor Chubin, Anton Zhavoronkov @edogby (on github)
* Bosnian: Ismar Kunc @ismarkunc
* Bulgarian: Vladimir Vitkov @zeridon (on github)

View file

@ -303,6 +303,7 @@ var (
"da": "da_DK",
"de": "de_DE",
"el": "el_GR",
"eu": "eu_ES",
"eo": "eo",
"es": "es_ES",
"et": "et_EE",
@ -423,7 +424,7 @@ var (
daytimeTranslation = map[string][]string{
"af":{"Oggend","Middag", "Vroegaand", "Laatnag"},
"ar":{"صباح", "ظهر", "مساء", "ليل" },
"ar":{ "ﺎﻠﻠﻴﻟ", "ﺎﻠﻤﺳﺍﺀ", "ﺎﻠﻈﻫﺭ", "ﺎﻠﺼﺑﺎﺣ" },
"az":{"Səhər", "Gün", "Axşam", "Gecə" },
"be":{"Раніца", "Дзень", "Вечар", "Ноч" },
"bg":{"Сутрин", "Обяд", "Вечер", "Нощ" },
@ -915,6 +916,9 @@ func printDay(w weather) (ret []string) {
if config.Lang == "ko" {
dateName = lctime.Strftime("%b %d일 %a", d)
}
if config.Lang == "zh" || config.Lang == "zh-tw" || config.Lang == "zh-cn" {
dateName = lctime.Strftime("%b%d日%A", d)
}
}
// appendSide := 0
// // for utf8.RuneCountInString(dateName) <= dateWidth {

1
test/proxy-data/data1 Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"Content-Type": "application/json"}

71
test/query.sh Normal file
View file

@ -0,0 +1,71 @@
queries=(
/
/Kiev
/Kiev.png
/?T
/Киев
/Kiev?2
"/Kiev?format=1"
"/Kiev?format=2"
"/Kiev?format=3"
"/Kiev?format=4"
"/Kiev?format=v2"
"/Kiev?format=%s"
"/Kiev?format=%S"
"/Kiev?format=%D+%S+%z+%s+%d"
"/:help"
"/Kiev?T"
"/Kiev?p"
"/Kiev?q"
"/Kiev?Q"
"/Kiev_text=no_view=v2.png"
"/Kiev.png?1nqF"
"/Kiev_1nqF.png"
)
options=$(cat <<EOF
-A firefox
-H Accept-Language:ru
-H X-Forwarded-For:1.1.1.1
EOF
)
server="http://127.0.0.1:8002"
if [ "$1" = update ]; then
UPDATE=yes
fi
if [[ $UPDATE = yes ]]; then
true > test-data/signatures
fi
result_tmp=$(mktemp wttrin-test-XXXXX)
while read -r -a args
do
for q in "${queries[@]}"; do
signature=$(echo "${args[@]}" "$q" | sha1sum | awk '{print $1}')
curl -ks "${args[@]}" "$server$q" > "$result_tmp"
result=$(sha1sum "$result_tmp" | awk '{print $1}')
# this must be moved to the server
# but for the moment we just clean up
# the cache after each call
rm -rf "/wttr.in/cache"
if grep -Eq "(we are running out of queries|500 Internal Server Error)" "$result_tmp"; then
echo "$q"
fi
if [[ $UPDATE = yes ]]; then
printf "%s %s %s\\n" "$signature" "$result" "${args[*]} $q" >> test-data/signatures
elif ! grep -q "$signature $result" test-data/signatures; then
echo "FAILED: curl -ks ${args[*]} $server$q"
fi
done
done <<< "${options}"
rm "$result_tmp"

88
test/test-data/signatures Normal file
View file

@ -0,0 +1,88 @@
8f27084b6294ddbe28dbcbf98f798730e8a79289 4ee2dd9cf8f5818902647ff832ef40d690096bf1 /
ae537911bb7b0568f478073e661abee1cb4ff941 d123e570da22dee9798d353c4281cb5a2bdbaeac /Kiev
4dc586807c16020b9f4dbb705326c698bea41665 a186d89e95061a7887c005ffa8bd1e29362de2da /Kiev.png
3db1938bedc0ee0047bf3b043ddaf0aba1912f13 febab92af9526163bc9e502ecd7fa4225345e6f6 /?T
2cc0ba7a57a6342e72fd7142ca18dbb0eae69416 ce7fb7a88cab697f5280ddabf344f0d397888956 /Киев
928142e88da142ea8075cbfe09bfef349e72dbb1 0f86f59a45b4485fea1375ca945503d9abb9a96d /Kiev?2
4f6f0a16ff415fad1c102c8023c5d8365ef63402 de3b9821d587753149eded5411ec397e7a2000e2 /Kiev?format=1
c99903b86971ccccfcca4f13e6fca72776b4fbcf f64086e48d84ac6eb440ca080eff28de1470ec30 /Kiev?format=2
2a0d6cd8d30a84328580611ca6dd6bed1d805a04 da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 /Kiev?format=3
4e4e15eeb6a8b6ddb5d00591c4b5e9b74a13e6bc 6a61305ea631fbccb98090c62a96898e8ffa0d75 /Kiev?format=4
a27d3e4ad7f820124ef57c9299715bc61cb71387 fed12e63dd5fb5e348ee30d94c7231112eca71cd /Kiev?format=v2
cbe9fc56091b519e6aebcedd9a7541241f4c4cda 2d97c405f1557b822cc86b038aeea40c3eb79d7d /Kiev?format=%s
84b79ec29670254c3570901e4c5db017516e088d 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 /Kiev?format=%S
8a0111eb7a519adad1210661bbb49f960ba7f95f fe58cbd420cf36a910551a1037f5c4fa19b31074 /Kiev?format=%D+%S+%z+%s+%d
83cc0ef08c24ad7ecc81d1a6cbd693bb06214ece 0ea998c1b53e452a373699ab953ab00e8a2870f0 /:help
310b64f65fc9f66a5142bf6104f4f9b9d5eef0ea b0bd07f0c87aae9464c091ccb955f41ec6973098 /Kiev?T
9bd1b460d4927df24724f45f69bd3132f3de8e04 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 /Kiev?p
3ee1a25d436799804d7ebd8371d8022fa55a71d7 80f18be012d0471dce9fcd2b500f482bcd635347 /Kiev?q
e0e8e7eca16bfac88503ac6d19a7a6c8b469c0fd d5d070c98237f0dffc82b176039f90a15f03a667 /Kiev?Q
d08d1fa2546fee0717d1eb663cf63cd1505b8885 e380ef1a22f62a7fa1133d0e35d923c8587cb3ed /Kiev_text=no_view=v2.png
b14e89e2c183139495bad5404748b3b6173063d7 ff70b13244929fa3a934e94f99f019c66501ad43 /Kiev.png?1nqF
04e1a945bdde39d646e397e55c096ec46b53a92b ff70b13244929fa3a934e94f99f019c66501ad43 /Kiev_1nqF.png
3e1be80e942a2ea5450c60e1c0ebfb154aca3da1 6a5bdefe64689f4d05128bd62a8118f4f2f52043 -A firefox /
ee6bf0665c2719cda3ec1fbdb80413d821c99b8e 3f9c5091269ece259cce13fc842265019001ed54 -A firefox /Kiev
98ef11678b7fd33425f97eeee70e00cd96206539 a186d89e95061a7887c005ffa8bd1e29362de2da -A firefox /Kiev.png
ecbcf2cb9004a754c4559ce7e92fead68f71721a 2ea6d52a2108a481cbc0f44a881eb88642d68e80 -A firefox /?T
74206d869128383dba2d840b848b90eb376fd851 7c6ce53ff25d91a5f46baa30077b69e0f09f2571 -A firefox /Киев
91b89025b5acd56ca475924e0eb559a9734f3333 dbd49d93eff2b2cf82f7d266f90de950207a0561 -A firefox /Kiev?2
e6cb82dab95e05167ffbcb90a10d6cb03cd02ec1 de3b9821d587753149eded5411ec397e7a2000e2 -A firefox /Kiev?format=1
e65bc57e8d1df26c442a9ecf45afee390ff331a3 f64086e48d84ac6eb440ca080eff28de1470ec30 -A firefox /Kiev?format=2
d743b331d5f4c81bbc8b168ce84a99ab22dc70cf da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -A firefox /Kiev?format=3
bf359ee92690c3a3061542dc6e78cb42ca837412 6a61305ea631fbccb98090c62a96898e8ffa0d75 -A firefox /Kiev?format=4
cb875772a6610c991b95b3fbfa22fc7192e25843 5367fd6790d55639b1536ec71abf340e5c79ff45 -A firefox /Kiev?format=v2
91c0076d8e6665c06aab7c7b2326b29718bfeb80 2d97c405f1557b822cc86b038aeea40c3eb79d7d -A firefox /Kiev?format=%s
ae9d7b1ee27eb8201a0726a3e24fe195cf2ae9e4 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 -A firefox /Kiev?format=%S
e94371697b70bf956b6f9352fad913d716e774e7 fe58cbd420cf36a910551a1037f5c4fa19b31074 -A firefox /Kiev?format=%D+%S+%z+%s+%d
d520af45b491689d53024c696955db8b1e4eaa87 e916b140b1297cf5bea16d92a91260e9dc3e2bc9 -A firefox /:help
6b80492b79a4cc510cb4a9654cb6ff085cdc1943 801d4c6d6837c9604944168a3930dfe05b0e9f5d -A firefox /Kiev?T
e13d7449ce756e55ba3e84e4b7e601b36d0044b6 c5a87804710ab70b8798f56579d276f4ce1806bf -A firefox /Kiev?p
3f6c192a6da5b79ea59ef94e99b9cbf4b0e7ede2 372aca50f441920ad623d62ee8fcde46d609f6f9 -A firefox /Kiev?q
f28ca2a7a47f4859eac7d0307ec7ac67a40e0adf 1b4b66d58bd7e27abeeca45581e686f12fefe76a -A firefox /Kiev?Q
62c5029cf297b1434c57228dc8c8cdfb5e68285d e380ef1a22f62a7fa1133d0e35d923c8587cb3ed -A firefox /Kiev_text=no_view=v2.png
22aa5dc06076b086de776a3c601544b250fef07c ff70b13244929fa3a934e94f99f019c66501ad43 -A firefox /Kiev.png?1nqF
3a00ecab00da83f70c2abf30379c5e79c791e383 ff70b13244929fa3a934e94f99f019c66501ad43 -A firefox /Kiev_1nqF.png
ed573b89ca5522d6ab69dc1686b98b00391076bd 5ce7ea58bf02bff008baa3193b2db498268a244b -H Accept-Language:ru /
b879673f66235bbf1913ff9abc58aff2fb8962d1 00a96a5d83608c2dad7921862bb3f244775f6b19 -H Accept-Language:ru /Kiev
83d99896cf866ecbaa6d2c64c12bd31bc7b35068 92dc07acb93633974eaff19e8c1a99e590e140d9 -H Accept-Language:ru /Kiev.png
9cbb6aa3e0b46e78229a32688db1cced9a44271d b368cc8f39e7a7ced04e3f4e6506e1eb4551e904 -H Accept-Language:ru /?T
095d8d38c667923131801595b903e007b5f902f3 4ede3397f9def696adc7ecf3ffd46a59b8fb25cb -H Accept-Language:ru /Киев
4e6cdfc38c9d9f2436438b345776c42cb8cab8a5 1b00c96a05f9daea8248a8e063d990797be933ad -H Accept-Language:ru /Kiev?2
b7d8d0f0bb4c38aabac468c9a354bb4e2b401893 de3b9821d587753149eded5411ec397e7a2000e2 -H Accept-Language:ru /Kiev?format=1
8f3bbfc9be6418e82edacecc54a9f3e9f26b7fbf f64086e48d84ac6eb440ca080eff28de1470ec30 -H Accept-Language:ru /Kiev?format=2
f1d4178892fd3dc38e9f966112d317859acc9122 da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -H Accept-Language:ru /Kiev?format=3
cf44e154504d9bc2b9b6066bbb0f5d52fc12f13e 6a61305ea631fbccb98090c62a96898e8ffa0d75 -H Accept-Language:ru /Kiev?format=4
4955c849f67da53203b8c96b15a0bf0a4a471bc6 e2d3509ef7f6c3fc84151f55e1c5eb2f28dfd155 -H Accept-Language:ru /Kiev?format=v2
e23e33569bbe34de944dad3a647d2a7a525513b4 2d97c405f1557b822cc86b038aeea40c3eb79d7d -H Accept-Language:ru /Kiev?format=%s
d3c44cb57a1ba487b9fe7ec37368d00eee5b4601 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 -H Accept-Language:ru /Kiev?format=%S
db91cc89883050beedd2afac7c74276a4d2dcf42 fe58cbd420cf36a910551a1037f5c4fa19b31074 -H Accept-Language:ru /Kiev?format=%D+%S+%z+%s+%d
3f69f4a605ce88643b4e0d62a588c92625d41aea 73b4142cd3af43472897989c61408d5765c2a6ef -H Accept-Language:ru /:help
08553ca4bf71c738c4321fe7d84b4e6ff830956f 016fc03b18a8902f838719bbc171184603c08b60 -H Accept-Language:ru /Kiev?T
b70f8b3fc8aee126c04b27b0d3b4c503b4292cbf b60b68a9e77275884812f7e52b06f6012ba5682a -H Accept-Language:ru /Kiev?p
400efdba61125f8cb850d7c33caf4fc2739a960b 5ee4a043a91509ef57aec46a14a0c24f09e8ec47 -H Accept-Language:ru /Kiev?q
b9fd454e73343f262a6d99dd80487495bd647c6f a10718896a07baadb87adf2bf0026b1f00252213 -H Accept-Language:ru /Kiev?Q
8fed034e57624d0e0b33140673094e56e04087bc 83d236565782aff7416bf526b38636148d6ba15a -H Accept-Language:ru /Kiev_text=no_view=v2.png
cf618118ddeff08355620e83a693c40239990545 37a4dd271b5a66513da4a6dc9caeab0ae15814db -H Accept-Language:ru /Kiev.png?1nqF
0f106ea5605ecc852ab6f63eee348a9a954e137f 37a4dd271b5a66513da4a6dc9caeab0ae15814db -H Accept-Language:ru /Kiev_1nqF.png
3ce3dd46413f236244410f142a4b44356a0cedf9 1c2eea391b35c8bfed2541435e1788307aa06bc1 -H X-Forwarded-For:1.1.1.1 /
89be0a5787592298ce34f10b36da7ee87d1a1353 d123e570da22dee9798d353c4281cb5a2bdbaeac -H X-Forwarded-For:1.1.1.1 /Kiev
a9977eadc628b1ede5d4f91ee103dfb740caa2b1 a186d89e95061a7887c005ffa8bd1e29362de2da -H X-Forwarded-For:1.1.1.1 /Kiev.png
eec20c6be5e528967cddf6d0b72c84dbda553d43 9a39dbafa7e1550d374e38059c0f4b8f437e1739 -H X-Forwarded-For:1.1.1.1 /?T
e304153f0e1e9b41781bf4eb6fb6c4a5b7513aec ce7fb7a88cab697f5280ddabf344f0d397888956 -H X-Forwarded-For:1.1.1.1 /Киев
98f0b3a28863a861c6ac6d89ee5d49adb7f3f518 0f86f59a45b4485fea1375ca945503d9abb9a96d -H X-Forwarded-For:1.1.1.1 /Kiev?2
cf04d7fe2cf36eba8d7fb4fa6def1c9015036456 de3b9821d587753149eded5411ec397e7a2000e2 -H X-Forwarded-For:1.1.1.1 /Kiev?format=1
67fbe9168566709450eb35d36c60c27105335a7e f64086e48d84ac6eb440ca080eff28de1470ec30 -H X-Forwarded-For:1.1.1.1 /Kiev?format=2
b2604348bf39774c85b7c18ae7b51f63a2c9f31a da7b79cdff330edc1a7a78fcf2f9c8bf7e432d40 -H X-Forwarded-For:1.1.1.1 /Kiev?format=3
cf012400156c842e569b6a9f05b094e6b75348cd 6a61305ea631fbccb98090c62a96898e8ffa0d75 -H X-Forwarded-For:1.1.1.1 /Kiev?format=4
1f4981348cab19df9846cd3b3923ee7a972ff9fa fed12e63dd5fb5e348ee30d94c7231112eca71cd -H X-Forwarded-For:1.1.1.1 /Kiev?format=v2
e7d772042819bb62e6e259656a75bd7b0621d1da 2d97c405f1557b822cc86b038aeea40c3eb79d7d -H X-Forwarded-For:1.1.1.1 /Kiev?format=%s
7db5a1daac653383c18aec25ad8583f2e5296845 3f26bdbbd5faf7729f87a9cb964d407493e9a0d6 -H X-Forwarded-For:1.1.1.1 /Kiev?format=%S
a9331379fa4b5d61b5a87a8e4cd4412cdae970a1 fe58cbd420cf36a910551a1037f5c4fa19b31074 -H X-Forwarded-For:1.1.1.1 /Kiev?format=%D+%S+%z+%s+%d
767a7407c14049fd77a6a2fedd1d8b35f6e47e0d 0ea998c1b53e452a373699ab953ab00e8a2870f0 -H X-Forwarded-For:1.1.1.1 /:help
10631d55b42e7bc5ec15ffc5cddae712785eb354 b0bd07f0c87aae9464c091ccb955f41ec6973098 -H X-Forwarded-For:1.1.1.1 /Kiev?T
031478f562663eb9f577b04032993e2f098146f6 d001bf6ab36b6c14f98f02fc4500706d7a9f05a8 -H X-Forwarded-For:1.1.1.1 /Kiev?p
e106cd21a6b67196159c2baa023142e3a8859612 80f18be012d0471dce9fcd2b500f482bcd635347 -H X-Forwarded-For:1.1.1.1 /Kiev?q
1a16c9b52ba90cb7ad3dd8902bf41b31a287d49e d5d070c98237f0dffc82b176039f90a15f03a667 -H X-Forwarded-For:1.1.1.1 /Kiev?Q
83bd9cc6a646e44b75524474dd32f0fd1f5c5a39 e380ef1a22f62a7fa1133d0e35d923c8587cb3ed -H X-Forwarded-For:1.1.1.1 /Kiev_text=no_view=v2.png
ac8dabb4d30cf6a6fae626e6fc29a4f2d9df0164 ff70b13244929fa3a934e94f99f019c66501ad43 -H X-Forwarded-For:1.1.1.1 /Kiev.png?1nqF
f0b3c0851a03a9b182ab938fc7f14935bf7af1f0 ff70b13244929fa3a934e94f99f019c66501ad43 -H X-Forwarded-For:1.1.1.1 /Kiev_1nqF.png