diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b472a6a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+ve/
+share/static/fonts/
+*.pyc
+data/
+log/
+.idea/
+*.swp
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..2981838
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,19 @@
+group: travis_latest
+language: python
+cache: pip
+python:
+ - 2.7
+ - 3.6
+install:
+ - pip install -r requirements.txt
+ - pip install flake8 # pytest # add another testing frameworks later
+before_script:
+ # stop the build if there are Python syntax errors or undefined names
+ - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+ - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
+script:
+ - true # pytest --capture=sys # add other tests here
+notifications:
+ on_success: change
+ on_failure: change # `always` will be the setting once code changes slow down.
diff --git a/README.md b/README.md
index 3eee0ae..464540b 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,17 @@
-*wttr.in — the right way to check the weather.*
+*wttr.in — the right way to check the weather!*
+
+wttr.in is a console-oriented weather forecast service that supports various information
+representation methods like terminal-oriented ANSI-sequences for console HTTP clients
+(curl, httpie, or wget), HTML for web browsers, or PNG for graphical viewers.
-wttr.in is a console oriented weather forecast service, that supports various information representation methods
-like terminal oriented ANSI-sequences for console HTTP clients such as curl, httpie or wget;
-HTML for web browsers; or PNG for graphical viewers.
wttr.in uses [wego](http://github.com/schachmat/wego) for visualization
and various data sources for weather forecast information.
-You can check it at [wttr.in](http://wttr.in).
+You can see it running here: [wttr.in](http://wttr.in).
## Usage
-You can access the service from a shell or from a Web browser:
+You can access the service from a shell or from a Web browser like this:
$ curl wttr.in
Weather for City: Paris, France
@@ -22,42 +23,42 @@ You can access the service from a shell or from a Web browser:
/ \ 0.0 mm
-That is how the actual weather report for your location looks like (it is live!):
+Here is an actual weather report for your location (it's live!):

-(it's not your location actually, becasue GitHub's CDN hides your real IP address with its own IP address,
-but it is still a live weather report in your language).
+(It's not your actual location - GitHub's CDN hides your real IP address with its own IP address,
+but it's still a live weather report in your language.)
-You can specify the location that you want to get the weather information for.
-If you omit the location name, you will get the report for your current location,
-based on your IP address.
+Want to get the weather information for a specific location? You can add the desired location to the URL in your
+request like this:
$ curl wttr.in/London
$ curl wttr.in/Moscow
-You can use 3-letters airport codes if you want to get the weather information
-about some airports:
+If you omit the location name, you will get the report for your current location based on your IP address.
+
+Use 3-letter airport codes in order to get the weather information at a certain airport:
$ curl wttr.in/muc # Weather for IATA: muc, Munich International Airport, Germany
$ curl wttr.in/ham # Weather for IATA: ham, Hamburg Airport, Germany
-If you want to specify a location that is not a city/town's name, but a name of some geographical location
-(e.g. it can be a site in a city, a mountain name, a special location etc.) you should place `~` before its name.
-That means the location name should be looked up before:
+Let's say you'd like to get the weather for a geographical location other than a town or city - maybe an attraction
+in a city, a mountain name, or some special location. Add the character `~` before the name to look up that special
+location name before the weather is then retrieved:
$ curl wttr.in/~Vostok+Station
$ curl wttr.in/~Eiffel+Tower
$ curl wttr.in/~Kilimanjaro
-In this case there is a line below the weather forecast output describing the founded precise position:
+For these examples, you'll see a line below the weather forecast output that shows the geolocation
+results of looking up the location:
Location: Vostok Station, станция Восток, AAT, Antarctica [-78.4642714,106.8364678]
Location: Tour Eiffel, 5, Avenue Anatole France, Gros-Caillou, 7e, Paris, Île-de-France, 75007, France [48.8582602,2.29449905432]
Location: Kilimanjaro, Northern, Tanzania [-3.4762789,37.3872648]
-You can also use IP-addresses (direct) or domain names (prefixed with @)
-as a location specificator:
+You can also use IP-addresses (direct) or domain names (prefixed with `@`) to specify a location:
$ curl wttr.in/@github.com
$ curl wttr.in/@msu.ru
@@ -66,20 +67,20 @@ To get detailed information online, you can access the [/:help](http://wttr.in/:
$ curl wttr.in/:help
-## Additional options
+### Weather Units
-By default the USCS units are used for the queries from the USA and the
-metric system for the rest of the world.
-You can override this behavior with the following options:
+By default the USCS units are used for the queries from the USA and the metric system for the rest of the world.
+You can override this behavior by adding `?u` or `?m` to a URL like this:
$ curl wttr.in/Amsterdam?u
$ curl wttr.in/Amsterdam?m
-## Supported formats
+## Supported output formats
-wttr.in supports three output formats at the moment:
+wttr.in currently supports three output formats:
* ANSI for the terminal;
+* ANSI for the terminal, one-line mode;
* HTML for the browser;
* PNG for the graphical viewers.
@@ -93,44 +94,143 @@ to separate them with `_` instead of `?` and `&`:
$ wget wttr.in/Paris_0tqp_lang=fr.png
-Special useful options for the PNG format:
+Useful options for the PNG format:
* `t` for transparency (`transparency=150`);
* transparency=0..255 for a custom transparency level.
-Transparency is a useful feature when the weather PNGs are used to
-add weather data to the pictures:
+Transparency is a useful feature when weather PNGs are used to add weather data to pictures:
- $ convert 1.jpg <( curl wttr.in/Oymyakon_tqp0.png ) -geometry +50+50 -composite 2.jpg
+ $ convert source.jpg <( curl wttr.in/Oymyakon_tqp0.png ) -geometry +50+50 -composite target.jpg
-Here:
+In this example:
-* `1.jpg` - source file;
-* `2.jpg` - target file;
-* Oymyakon - name of the location;
-* tqp0 - options (recommended).
+* `source.jpg` - source file;
+* `target.jpg` - target file;
+* `Oymyakon` - name of the location;
+* `tqp0` - options (recommended).

-## Special pages
+## One-line output
-wttr.in can be used not only to check the weather, but also for some other purposes:
+For one-line output format, specify additional URL parameter `format`:
+
+```
+$ curl wttr.in/Nuremberg?format=3
+Nuremberg: 🌦 +11⁰C
+```
+
+Available preconfigured formats: 1, 2, 3, 4 and the custom format using the percent notation (see below).
+
+You can specify multiple locations separated with `:` (for repeating queries):
+
+```
+$ curl wttr.in/Nuremberg:Hamburg:Berlin?format=3
+Nuremberg: 🌦 +11⁰C
+```
+Or to process all this queries at once:
+
+```
+$ curl -s 'wttr.in/{Nuremberg,Hamburg,Berlin}?format=3'
+Nuremberg: 🌦 +11⁰C
+Hamburg: 🌦 +8⁰C
+Berlin: 🌦 +8⁰C
+```
+
+To specify your own custom output format, use the special `%`-notation:
+
+```
+ c Weather condition,
+ C Weather condition textual name,
+ h Humidity,
+ t Temperature,
+ w Wind,
+ l Location,
+ m Moonphase 🌑🌒🌓🌔🌕🌖🌗🌘,
+ M Moonday,
+ p precipitation (mm),
+ P pressure (hPa),
+```
+
+So, these two calls are the same:
+
+```
+ $ curl wttr.in/London?format=3
+ London: ⛅️ +7⁰C
+ $ curl wttr.in/London?format="%l:+%c+%t"
+ London: ⛅️ +7⁰C
+```
+Keep in mind, that when using in `tmux.conf`, you have to escape `%` with `%`, i.e. write there `%%` instead of `%`.
+
+In progams, that are querying the service automatically (such as tmux),it is better to use some reasonable update interval. In tmux, you can configure it with `status-interval`.
+
+If several, `:` separated locations, are specified in the query, specify update period
+as an additional query parameter `period=`:
+```
+set -g status-interval 60
+WEATHER='#(curl -s wttr.in/London:Stockholm:Moscow\?format\="%%l:+%%c%%20%%t%%60%%w&period=60")'
+set -g status-right "$WEATHER ..."
+```
+
+
+To see emojis in terminal, you need:
+
+1. Terminal support for emojis (was added to Cairo 1.15.8);
+2. Font with emojis support.
+
+For the Emoji font, we recommend *Noto Color Emoji*, and a good alternative option would be the *Emoji One* font;
+both of them support all necessary emoji glyphs.
+
+Font configuration:
+
+```
+$ cat ~/.config/fontconfig/fonts.conf
+
+
+
+
+ serif
+
+ Noto Color Emoji
+
+
+
+ sans-serif
+
+ Noto Color Emoji
+
+
+
+ monospace
+
+ Noto Color Emoji
+
+
+
+```
+
+(to apply the configuration, run `fc-cache -f -v`)
+
+## 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:
$ curl wttr.in/Moon
-To see the current Moon phase (uses [pyphoon](https://github.com/chubin/pyphoon) as its backend).
+Get the Moon phase for a particular date by adding `@YYYY-MM-DD`:
$ curl wttr.in/Moon@2016-12-25
-To see the Moon phase for the specified date (2016-12-25).
+The Moon phase information uses [pyphoon](https://github.com/chubin/pyphoon) as its backend.
## Internationalization and localization
-wttr.in supports multilingual locations names: they can be specified in any language in the world
-(it may be surprising, but many locations in the world do not have any English name at all).
+wttr.in supports multilingual locations names that can be specified in any language in the world
+(it may be surprising, but many locations in the world don't have an English name).
-The query string should be specified in Unicode (hex encoded or not). If it contains spaces
-they must be replaced with +:
+The query string should be specified in Unicode (hex-encoded or not). Spaces in the query string
+must be replaced with `+`:
$ curl wttr.in/станция+Восток
Weather report: станция Восток
@@ -145,48 +245,51 @@ The language used for the output (except the location name) does not depend on t
and it is either English (by default) or the preferred language of the browser (if the query
was issued from a browser) that is specified in the query headers (`Accept-Language`).
-It can be set explicitly when using console clients by means of the appropriate command line options
-(for example: `curl -H "Accept-Language: fr" wttr.in` or `http GET wttr.in Accept-Language:ru`).
+The language can be set explicitly when using console clients by using command-line options like this:
+
+ curl -H "Accept-Language: fr" wttr.in
+ http GET wttr.in Accept-Language:ru
The preferred language can be forced using the `lang` option:
$ curl wttr.in/Berlin?lang=de
-The third option is to choose the language using DNS name used in the query:
+The third option is to choose the language using the DNS name used in the query:
$ curl de.wttr.in/Berlin
-wttr.in is currently translated in more than 45 languages and the number of supported languages
-is constantly growing.
+wttr.in is currently translated into 54 languages, and the number of supported languages is constantly growing.
See [/:translation](http://wttr.in/:translation) to learn more about the translation process,
-to see the list of supported languages and contributors, or to know how you can help to translate wttr.in in your language.
+to see the list of supported languages and contributors, or to know how you can help to translate wttr.in
+in your language.

## Installation
-To install the program:
+To install the application:
1. Install external dependencies
-2. Install python dependencies used by the service
-3. Get WorldWeatherOnline API Key
-4. Configure wego
+2. Install Python dependencies used by the service
+3. Configure IP2Location (optional)
+4. Get a WorldWeatherOnline API and configure wego
5. Configure wttr.in
-6. Configure HTTP-frontend service
+6. Configure the HTTP-frontend service
### Install external dependencies
-External requirements:
+wttr.in has the following external dependencies:
+* [golang](https://golang.org/doc/install), wego dependency
* [wego](https://github.com/schachmat/wego), weather client for terminal
-To install `wego` you must have [golang](https://golang.org/doc/install) installed. After that:
+After you install [golang](https://golang.org/doc/install), install `wego`:
$ go get -u github.com/schachmat/wego
$ go install github.com/schachmat/wego
-### Install python dependencies
+### Install Python dependencies
Python requirements:
@@ -196,7 +299,7 @@ Python requirements:
* requests
* gevent
-If you want to get weather reports as PNG files, install also:
+If you want to get weather reports as PNG files, you'll also need to install:
* PIL
* pyte (>=0.6)
@@ -208,22 +311,33 @@ If `virtualenv` is used:
$ virtualenv ve
$ ve/bin/pip install -r requirements.txt
- $ ve/bin/pip bin/srv.py
-
-(pyte 0.6 is not yet released and it should be installed direct from the source code from its GitHub repository at the moment).
+ $ ve/bin/python 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/
+You can use a free database GeoLite2 that can be downloaded from (http://dev.maxmind.com/geoip/geoip2/geolite2/).
-### Get WorldWeatherOnline key
+### Configure IP2Location (optional)
-To get the WorldWeatherOnline API key, you must register here:
+If you want to use the IP2location service for IP-addresses that are not covered by GeoLite2,
+you have to obtain a API key of that service, and after that save into the `~/.ip2location.key` file:
+
+```
+$ echo 'YOUR_IP2LOCATION_KEY' > ~/.ip2location.key
+```
+
+If you don't have this file, the service will be silently skipped (it is not a big problem,
+because the MaxMind database is pretty good).
+
+### Get a WorldWeatherOnline key and configure wego
+
+To get a WorldWeatherOnline API key, you must register here:
https://developer.worldweatheronline.com/auth/register
-### Configure wego
+After you have a WorldWeatherOnline key, you can save it into the
+WWO key file: `~/.wwo.key`
-After you have the key, configure `wego`:
+Also, you have to specify the key in the `wego` configuration:
$ cat ~/.wegorc
{
@@ -238,8 +352,8 @@ The `City` parameter in `~/.wegorc` is ignored.
### Configure wttr.in
-Configure the following environment variables specifing the path to the local `wttr.in`
-installation, to the GeoLite database and to the `wego` installation. For example:
+Configure the following environment variables that define the path to the local `wttr.in`
+installation, to the GeoLite database, and to the `wego` installation. For example:
export WTTR_MYDIR="/home/igor/wttr.in"
export WTTR_GEOLITE="/home/igor/wttr.in/GeoLite2-City.mmdb"
@@ -248,10 +362,9 @@ installation, to the GeoLite database and to the `wego` installation. For exampl
export WTTR_LISTEN_PORT="8002"
-### Configure HTTP-frontend service
+### Configure the HTTP-frontend service
-Configure the web server, that will be used
-to access the service (if you want to use a web frontend; it's recommended):
+It's recommended that you also configure the web server that will be used to access the service:
server {
listen [::]:80;
@@ -281,5 +394,3 @@ to access the service (if you want to use a web frontend; it's recommended):
expires off;
}
}
-
-
diff --git a/bin/proxy.py b/bin/proxy.py
new file mode 100644
index 0000000..fda7131
--- /dev/null
+++ b/bin/proxy.py
@@ -0,0 +1,202 @@
+#vim: fileencoding=utf-8
+
+"""
+
+The proxy server acts as a backend for the wttr.in service.
+
+It caches the answers and handles various data sources transforming their
+answers into format supported by the wttr.in service.
+
+"""
+from __future__ import print_function
+
+from gevent.pywsgi import WSGIServer
+from gevent.monkey import patch_all
+patch_all()
+
+# pylint: disable=wrong-import-position,wrong-import-order
+import sys
+import os
+import time
+import json
+
+import requests
+import cyrtranslit
+
+from flask import Flask, request
+APP = Flask(__name__)
+
+
+MYDIR = os.path.abspath(
+ os.path.dirname(os.path.dirname('__file__')))
+sys.path.append("%s/lib/" % MYDIR)
+
+from globals import PROXY_CACHEDIR, PROXY_HOST, PROXY_PORT
+from translations import PROXY_LANGS
+# pylint: enable=wrong-import-position
+
+
+
+def load_translations():
+ """
+ load all translations
+ """
+ translations = {}
+
+ for f_name in PROXY_LANGS:
+ f_name = 'share/translations/%s.txt' % f_name
+ translation = {}
+ lang = f_name.split('/')[-1].split('.', 1)[0]
+ with open(f_name, "r") as f_file:
+ for line in f_file:
+ if ':' not in line:
+ continue
+ if line.count(':') == 3:
+ _, trans, orig, _ = line.strip().split(':', 4)
+ else:
+ _, trans, orig = line.strip().split(':', 3)
+ trans = trans.strip()
+ orig = orig.strip()
+
+ translation[orig] = trans
+ translations[lang] = translation
+ return translations
+TRANSLATIONS = load_translations()
+
+
+def _find_srv_for_query(path, query): # pylint: disable=unused-argument
+ return 'http://api.worldweatheronline.com/'
+
+def _load_content_and_headers(path, query):
+ timestamp = time.strftime("%Y%m%d%H", time.localtime())
+ cache_file = os.path.join(PROXY_CACHEDIR, timestamp, path, query)
+ try:
+ return (open(cache_file, 'r').read(),
+ json.loads(open(cache_file+".headers", 'r').read()))
+ except IOError:
+ return None, None
+
+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_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)
+
+def translate(text, lang):
+ """
+ Translate `text` into `lang`
+ """
+ translated = TRANSLATIONS.get(lang, {}).get(text, text)
+ if text.encode('utf-8') == translated:
+ print("%s: %s" % (lang, text))
+ return translated
+
+def cyr(to_translate):
+ """
+ Transliterate `to_translate` from latin into cyrillic
+ """
+ return cyrtranslit.to_cyrillic(to_translate)
+
+def _patch_greek(original):
+ return original.decode('utf-8').replace(u"Ηλιόλουστη/ο", u"Ηλιόλουστη").encode('utf-8')
+
+def add_translations(content, lang):
+ """
+ Add `lang` translation to `content` (JSON)
+ returned by the data source
+ """
+ languages_to_translate = TRANSLATIONS.keys()
+ try:
+ d = json.loads(content) # pylint: disable=invalid-name
+ except ValueError as exception:
+ print("---")
+ print(exception)
+ print("---")
+
+ try:
+ weather_condition = d['data']['current_condition'][0]['weatherDesc'][0]['value']
+ if lang in languages_to_translate:
+ d['data']['current_condition'][0]['lang_%s' % lang] = \
+ [{'value': translate(weather_condition, lang)}]
+ elif lang == 'sr':
+ d['data']['current_condition'][0]['lang_%s' % lang] = \
+ [{'value': cyr(
+ d['data']['current_condition'][0]['lang_%s' % lang][0]['value']\
+ .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
+ fixed_hourly = []
+ for h in w['hourly']: # pylint: disable=invalid-name
+ weather_condition = h['weatherDesc'][0]['value']
+ if lang in languages_to_translate:
+ h['lang_%s' % lang] = \
+ [{'value': translate(weather_condition, lang)}]
+ elif lang == 'sr':
+ h['lang_%s' % lang] = \
+ [{'value': cyr(h['lang_%s' % lang][0]['value'].encode('utf-8'))}]
+ elif lang == 'el':
+ h['lang_%s' % lang] = \
+ [{'value': _patch_greek(h['lang_%s' % lang][0]['value'].encode('utf-8'))}]
+ elif lang == 'sr-lat':
+ h['lang_%s' % lang] = \
+ [{'value': h['lang_sr'][0]['value'].encode('utf-8')}]
+ fixed_hourly.append(h)
+ w['hourly'] = fixed_hourly
+ fixed_weather.append(w)
+ d['data']['weather'] = fixed_weather
+
+ content = json.dumps(d)
+ except (IndexError, ValueError) as exception:
+ print(exception)
+ return content
+
+@APP.route("/")
+def proxy(path):
+ """
+ Main proxy function. Handles incoming HTTP queries.
+ """
+
+ lang = request.args.get('lang', 'en')
+ query_string = request.query_string
+ query_string = query_string.replace('sr-lat', 'sr')
+ content, headers = _load_content_and_headers(path, query_string)
+
+ if content is None:
+ srv = _find_srv_for_query(path, query_string)
+ url = '%s/%s?%s' % (srv, path, query_string)
+ print(url)
+
+ attempts = 5
+ while attempts:
+ response = requests.get(url, timeout=10)
+ try:
+ json.loads(response.content)
+ break
+ except ValueError:
+ attempts -= 1
+
+ headers = {}
+ headers['Content-Type'] = response.headers['content-type']
+ content = add_translations(response.content, lang)
+ _save_content_and_headers(path, query_string, content, headers)
+
+ return content, 200, headers
+
+if __name__ == "__main__":
+ #app.run(host='0.0.0.0', port=5001, debug=False)
+ #app.debug = True
+ SERVER = WSGIServer((PROXY_HOST, PROXY_PORT), APP)
+ SERVER.serve_forever()
diff --git a/bin/srv.py b/bin/srv.py
index dfb1995..8d4f447 100644
--- a/bin/srv.py
+++ b/bin/srv.py
@@ -1,278 +1,56 @@
-import logging
-import os
-import re
-import requests
-import socket
-import time
+#!/usr/bin/env python
+# vim: set encoding=utf-8
-import geoip2.database
-from geopy.geocoders import Nominatim
-import jinja2
-
-from gevent.wsgi import WSGIServer
+from gevent.pywsgi import WSGIServer
from gevent.monkey import patch_all
-from gevent.subprocess import Popen, PIPE
patch_all()
-import dns.resolver
-from dns.exception import DNSException
+# pylint: disable=wrong-import-position,wrong-import-order
+import sys
+import os
+import jinja2
-from flask import Flask, request, render_template, send_from_directory
-app = Flask(__name__)
+from flask import Flask, request, send_from_directory
+APP = Flask(__name__)
-MYDIR = os.environ.get('WTTR_MYDIR', os.path.abspath(os.path.dirname( os.path.dirname('__file__') )))
-GEOLITE = os.environ.get('WTTR_GEOLITE', os.path.join( MYDIR, "GeoLite2-City.mmdb" ))
-WEGO = os.environ.get('WTTR_WEGO', "/home/igor/go/bin/wego")
-LISTEN_HOST = os.environ.get('WTTR_LISTEN_HOST', "127.0.0.1")
-LISTEN_PORT = int(os.environ.get('WTTR_LISTEN_PORT', "8002"))
+MYDIR = os.path.abspath(
+ os.path.dirname(os.path.dirname('__file__')))
+sys.path.append("%s/lib/" % MYDIR)
-CACHEDIR = os.path.join( MYDIR, "cache" )
-IP2LCACHE = os.path.join( MYDIR, "cache/ip2l" )
-ALIASES = os.path.join( MYDIR, "share/aliases" )
-ANSI2HTML = os.path.join( MYDIR, "share/ansi2html.sh" )
-HELP_FILE = os.path.join( MYDIR, 'share/help.txt' )
-LOG_FILE = os.path.join( MYDIR, 'log/main.log' )
-TEMPLATES = os.path.join( MYDIR, 'share/templates' )
-STATIC = os.path.join( MYDIR, 'share/static' )
+import wttr_srv
+from globals import TEMPLATES, STATIC, LISTEN_HOST, LISTEN_PORT
+# pylint: enable=wrong-import-position,wrong-import-order
-NOT_FOUND_LOCATION = "NOT_FOUND"
-DEFAULT_LOCATION = "Oymyakon"
-NOT_FOUND_MESSAGE = """
-We were unable to find your location,
-so we have brought you to Oymyakon,
-one of the coldest permanently inhabited locales on the planet.
-"""
-PLAIN_TEXT_AGENTS = [
- "curl",
- "httpie",
- "lwp-request",
- "wget",
- "python-requests"
-]
-
-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)
-
-reader = geoip2.database.Reader(GEOLITE)
-geolocator = Nominatim()
-
-my_loader = jinja2.ChoiceLoader([
- app.jinja_loader,
+MY_LOADER = jinja2.ChoiceLoader([
+ APP.jinja_loader,
jinja2.FileSystemLoader(TEMPLATES),
])
-app.jinja_loader = my_loader
+APP.jinja_loader = MY_LOADER
-class Limits:
- def __init__( self ):
- self.intervals = [ 'min', 'hour', 'day' ]
- self.divisor = {
- 'min': 60,
- 'hour': 3600,
- 'day': 86400,
- }
- self.counter = {
- 'min': {},
- 'hour': {},
- 'day': {},
- }
- self.limit = {
- 'min': 10,
- 'hour': 20,
- 'day': 100,
- }
- self.last_update = {
- 'min': 0,
- 'hour': 0,
- 'day': 0,
- }
- self.clear_counters()
-
- def check_ip( self, ip ):
- self.clear_counters()
- for interval in self.intervals:
- if ip not in self.counter[interval]:
- self.counter[interval][ip] = 0
- self.counter[interval][ip] += 1
- if self.limit[interval] <= self.counter[interval][ip]:
- log("Too many queries: %s in %s for %s" % (self.limit[interval], interval, ip) )
- raise RuntimeError("Not so fast! Number of queries per %s is limited to %s" % (interval, self.limit[interval]))
- print self.counter
-
- def clear_counters( self ):
- t = int( time.time() )
- for interval in self.intervals:
- if t / self.divisor[interval] != self.last_update[interval]:
- self.counter[interval] = {}
- self.last_update[interval] = t / self.divisor[interval]
-
-
-limits = Limits()
-
-def error( text ):
- print text
- raise RuntimeError(text)
-
-def log( text ):
- print text.encode('utf-8')
- logging.info( text.encode('utf-8') )
-
-def is_ip( ip ):
- if re.match('\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}', ip) is None:
- return False
- try:
- socket.inet_aton(ip)
- return True
- except socket.error:
- return False
-
-def save_weather_data( location, filename ):
-
- if location == NOT_FOUND_LOCATION:
- location_not_found = True
- location = DEFAULT_LOCATION
- else:
- location_not_found = False
- p = Popen( [ WEGO, '-location=%s' % location ], stdout=PIPE, stderr=PIPE )
- stdout, stderr = p.communicate()
- if p.returncode != 0:
- error( stdout + stderr )
-
- dirname = os.path.dirname( filename )
- if not os.path.exists( dirname ):
- os.makedirs( dirname )
-
- if location_not_found:
- stdout += NOT_FOUND_MESSAGE
-
- open( filename, 'w' ).write( stdout )
-
- 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 )
-
- open( filename+'.html', 'w' ).write( stdout )
-
-def get_filename( location ):
- location = location.replace('/', '_')
- timestamp = time.strftime( "%Y%m%d%H", time.localtime() )
- return "%s/%s/%s" % ( CACHEDIR, location, timestamp )
-
-def get_wetter(location, ip, html=False):
- filename = get_filename( location )
- if not os.path.exists( filename ):
- limits.check_ip( ip )
- save_weather_data( location, filename )
- if html:
- filename += '.html'
- return open(filename).read()
-
-
-def ip2location( ip ):
- cached = os.path.join( IP2LCACHE, ip )
-
- if os.path.exists( cached ):
- return open( cached, 'r' ).read()
-
- try:
- t = requests.get( 'http://api.ip2location.com/?ip=%s&key=demo&package=WS10' % ip ).text
- if ';' in t:
- location = t.split(';')[3]
- if not os.path.exists( IP2LCACHE ):
- os.makedirs( IP2LCACHE )
- open( cached, 'w' ).write( location )
- return location
- except:
- pass
-
-def get_location( ip_addr ):
- response = reader.city( ip_addr )
- city = response.city.name
- if city is None and response.location:
- coord = "%s, %s" % (response.location.latitude, response.location.longitude)
- location = geolocator.reverse(coord, language='en')
- city = location.raw.get('address', {}).get('city')
- if city is None:
- print ip_addr
- city = ip2location( ip_addr )
- return city or NOT_FOUND_LOCATION
-
-def load_aliases( aliases_filename ):
- aliases_db = {}
- with open( aliases_filename, 'r' ) as f:
- for line in f.readlines():
- from_, to_ = line.split(':', 1)
- aliases_db[ from_.strip().lower() ] = to_.strip()
- return aliases_db
-
-location_alias = load_aliases( ALIASES )
-def location_canonical_name( location ):
- if location.lower() in location_alias:
- return location_alias[location.lower()]
- return location
-
-def show_help():
- return open(HELP_FILE, 'r').read()
-
-@app.route('/files/')
+@APP.route('/files/')
def send_static(path):
+ "Send any static file located in /files/"
return send_from_directory(STATIC, path)
-@app.route('/favicon.ico')
+@APP.route('/favicon.ico')
def send_favicon():
+ "Send static file favicon.ico"
return send_from_directory(STATIC, 'favicon.ico')
-@app.route("/")
-@app.route("/")
-def wttr(location = None):
+@APP.route('/malformed-response.html')
+def send_malformed():
+ "Send static file malformed-response.html"
+ return send_from_directory(STATIC, 'malformed-response.html')
- user_agent = request.headers.get('User-Agent').lower()
-
- if any(agent in user_agent for agent in PLAIN_TEXT_AGENTS):
- html_output = False
- else:
- html_output = True
-
-
- if location == ':help':
- help_ = show_help()
- if html_output:
- return render_template( 'index.html', body=help_ )
- else:
- return help_
-
- orig_location = location
-
- if request.headers.getlist("X-Forwarded-For"):
- ip = request.headers.getlist("X-Forwarded-For")[0]
- if ip.startswith('::ffff:'):
- ip = ip[7:]
- else:
- ip = request.remote_addr
-
- try:
- if location is None:
- location = get_location( ip )
-
- if is_ip( location ):
- location = get_location( location )
- if location.startswith('@'):
- try:
- loc = dns.resolver.query( location[1:], 'LOC' )
- location = str("%.7f,%.7f" % (loc[0].float_latitude, loc[0].float_longitude))
- except DNSException, e:
- location = get_location( socket.gethostbyname( location[1:] ) )
-
- location = location_canonical_name( location )
- log("%s %s %s %s" % (ip, user_agent, orig_location, location))
- return get_wetter( location, ip, html=html_output )
- except Exception, e:
- logging.error("Exception has occurred", exc_info=1)
- return str(e).rstrip()+"\n"
-
-server = WSGIServer((LISTEN_HOST, LISTEN_PORT), app)
-server.serve_forever()
+@APP.route("/")
+@APP.route("/")
+def wttr(location=None):
+ "Main function wrapper"
+ return wttr_srv.wttr(location, request)
+SERVER = WSGIServer(
+ (LISTEN_HOST, int(os.environ.get('WTTRIN_SRV_PORT', LISTEN_PORT))),
+ APP)
+SERVER.serve_forever()
diff --git a/lib/buttons.py b/lib/buttons.py
index 96180a8..c08e6b2 100644
--- a/lib/buttons.py
+++ b/lib/buttons.py
@@ -4,18 +4,17 @@ TWITTER_BUTTON = """
"""
GITHUB_BUTTON = """
-
-wttr.in
+wttr.in
"""
GITHUB_BUTTON_2 = """
-wego
+wego
"""
GITHUB_BUTTON_3 = """
-pyphoon
+pyphoon
"""
GITHUB_BUTTON_FOOTER = """
@@ -23,3 +22,14 @@ GITHUB_BUTTON_FOOTER = """
"""
+def add_buttons(output):
+ """
+ Add buttons to html output
+ """
+
+ return output.replace('
', '')
title = "%s" % first.encode('utf-8')
- stdout = re.sub("", "" + title, stdout)
+ opengraph = get_opengraph()
+ stdout = re.sub("", "" + title + opengraph, stdout)
open(filename+'.html', 'w').write(stdout)
filename = get_filename(location, lang=lang, query=query, location_name=location_name)
@@ -164,7 +208,10 @@ def get_wetter(location, ip, html=False, lang=None, query=None, location_name=No
return open(filename).read()
-def get_moon(location, html=False, lang=None):
+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:]
@@ -174,18 +221,20 @@ def get_moon(location, html=False, lang=None):
if date:
try:
dateutil.parser.parse(date)
- except:
- pass
+ except Exception as e:
+ print("ERROR: %s" % e)
else:
cmd += [date]
env = os.environ.copy()
if lang:
env['LANG'] = lang
- print cmd
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)
diff --git a/lib/wttr_line.py b/lib/wttr_line.py
new file mode 100644
index 0000000..e189087
--- /dev/null
+++ b/lib/wttr_line.py
@@ -0,0 +1,259 @@
+#vim: fileencoding=utf-8
+
+"""
+One-line output mode.
+
+Initial implementation of one-line output mode.
+
+[ ] forecast
+[ ] spark
+[ ] several locations
+[ ] location handling
+[ ] more preconfigured format lines
+[ ] add information about this mode to /:help
+"""
+
+import sys
+import re
+import datetime
+from astral import Astral, Location
+from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION
+from weather_data import get_weather_data
+
+PRECONFIGURED_FORMAT = {
+ '1': u'%c %t',
+ '2': u'%c 🌡️%t 🌬️%w',
+ '3': u'%l: %c %t',
+ '4': u'%l: %c 🌡️%t 🌬️%w',
+}
+
+MOON_PHASES = (
+ u"🌑", u"🌒", u"🌓", u"🌔", u"🌕", u"🌖", u"🌗", u"🌘"
+)
+
+def convert_to_fahrenheit(temp):
+ "Convert Celcius `temp` to Fahrenheit"
+
+ return (temp*9.0/5)+32
+
+def render_temperature(data, query):
+ """
+ temperature (t)
+ """
+
+ if query.get('use_imperial', False):
+ temperature = u'%s°F' % data['temp_F']
+ else:
+ temperature = u'%s°C' % data['temp_C']
+
+ if temperature[0] != '-':
+ temperature = '+' + temperature
+
+ return temperature
+
+def render_condition(data, query):
+ """
+ condition (c)
+ """
+
+ weather_condition = WEATHER_SYMBOL[WWO_CODE[data['weatherCode']]]
+ return weather_condition
+
+def render_condition_fullname(data, query):
+ """
+ condition_fullname (C)
+ """
+
+ found = None
+ for key, val in data.items():
+ if key.startswith('lang_'):
+ found = val
+ break
+ if not found:
+ found = data['weatherDesc']
+
+ try:
+ weather_condition = found[0]['value']
+ except KeyError:
+ weather_condition = ''
+
+ return weather_condition
+
+def render_humidity(data, query):
+ """
+ humidity (h)
+ """
+
+ humidity = data.get('humidity', '')
+ if humidity:
+ humidity += '%'
+ return humidity
+
+def render_precipitation(data, query):
+ """
+ precipitation (p)
+ """
+
+ answer = data.get('precipMM', '')
+ if answer:
+ answer += 'mm'
+ return answer
+
+def render_pressure(data, query):
+ """
+ pressure (P)
+ """
+
+ answer = data.get('pressure', '')
+ if answer:
+ answer += 'hPa'
+ return answer
+
+def render_wind(data, query):
+ """
+ wind (w)
+ """
+
+ try:
+ degree = data["winddirDegree"]
+ except KeyError:
+ degree = ""
+
+ try:
+ degree = int(degree)
+ except ValueError:
+ degree = ""
+
+ if degree:
+ wind_direction = WIND_DIRECTION[((degree+22)%360)/45]
+ else:
+ wind_direction = ""
+
+ if query.get('use_ms_for_wind', False):
+ unit = ' m/s'
+ wind = u'%s%.1f%s' % (wind_direction, float(data['windspeedKmph'])/36.0*10.0, unit)
+ elif query.get('use_imperial', False):
+ unit = ' mph'
+ wind = u'%s%s%s' % (wind_direction, data['windspeedMiles'], unit)
+ else:
+ unit = ' km/h'
+ wind = u'%s%s%s' % (wind_direction, data['windspeedKmph'], unit)
+
+ return wind
+
+def render_location(data, query):
+ """
+ location (l)
+ """
+
+ return (data['override_location'] or data['location']) # .title()
+
+def render_moonphase(_, query):
+ """
+ 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
+ )
+ return MOON_PHASES[moon_index]
+
+def render_moonday(_, query):
+ """
+ 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())))
+
+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)
+
+
+ return str(sun['sunset'])
+
+FORMAT_SYMBOL = {
+ 'c': render_condition,
+ 'C': render_condition_fullname,
+ 'h': render_humidity,
+ 't': render_temperature,
+ 'w': render_wind,
+ 'l': render_location,
+ 'm': render_moonphase,
+ 'M': render_moonday,
+ 's': render_sunset,
+ 'p': render_precipitation,
+ 'P': render_pressure,
+ }
+
+def render_line(line, data, query):
+ """
+ Render format `line` using `data`
+ """
+
+ def render_symbol(match):
+ """
+ Render one format symbol from re `match`
+ using `data` from external scope.
+ """
+
+ symbol_string = match.group(0)
+ symbol = symbol_string[-1]
+
+ if symbol not in FORMAT_SYMBOL:
+ return ''
+
+ render_function = FORMAT_SYMBOL[symbol]
+ return render_function(data, query)
+
+ return re.sub(r'%[^%]*[a-zA-Z]', render_symbol, line)
+
+def format_weather_data(format_line, location, override_location, data, query):
+ """
+ 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
+ current_condition = data['data']['current_condition'][0]
+ current_condition['location'] = location
+ current_condition['override_location'] = override_location
+ output = render_line(format_line, current_condition, query)
+ return output
+
+def wttr_line(location, override_location_name, query, lang):
+ """
+ Return 1line weather information for `location`
+ in format `line_format`
+ """
+
+ format_line = query.get('format', '')
+
+ if format_line in PRECONFIGURED_FORMAT:
+ format_line = PRECONFIGURED_FORMAT[format_line]
+
+ weather_data = get_weather_data(location, lang)
+
+ output = format_weather_data(format_line, location, override_location_name, weather_data, query)
+ output = output.rstrip("\n")+"\n"
+ return output
+
+def main():
+ """
+ Function for standalone module usage
+ """
+
+ location = sys.argv[1]
+ query = {
+ 'line': sys.argv[2],
+ }
+
+ sys.stdout.write(wttr_line(location, location, query, 'en'))
+
+if __name__ == '__main__':
+ main()
diff --git a/lib/wttr_srv.py b/lib/wttr_srv.py
new file mode 100644
index 0000000..0cf47bd
--- /dev/null
+++ b/lib/wttr_srv.py
@@ -0,0 +1,296 @@
+#!/usr/bin/env python
+# vim: set encoding=utf-8
+
+"""
+Main wttr.in rendering function implementation
+"""
+
+import logging
+import os
+import time
+from flask import render_template, send_file, make_response
+
+import wttrin_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, \
+ BASH_FUNCTION_FILE, TRANSLATION_FILE, LOG_FILE, \
+ NOT_FOUND_LOCATION, \
+ MALFORMED_RESPONSE_HTML_PAGE, \
+ PLAIN_TEXT_AGENTS, PLAIN_TEXT_PAGES, \
+ MY_EXTERNAL_IP, QUERY_LIMITS
+from location import is_location_blocked, location_processing
+from limits import Limits
+from wttr import get_wetter, get_moon
+from wttr_line import wttr_line
+
+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')
+
+LIMITS = Limits(whitelist=[MY_EXTERNAL_IP], limits=QUERY_LIMITS)
+
+def show_text_file(name, lang):
+ """
+ show static file `name` for `lang`
+ """
+ text = ""
+ if name == ":help":
+ text = open(get_help_file(lang), 'r').read()
+ text = text.replace('FULL_TRANSLATION', ' '.join(FULL_TRANSLATION))
+ text = text.replace('PARTIAL_TRANSLATION', ' '.join(PARTIAL_TRANSLATION))
+ elif name == ":bash.function":
+ text = open(BASH_FUNCTION_FILE, 'r').read()
+ elif name == ":translation":
+ text = open(TRANSLATION_FILE, 'r').read()
+ text = text\
+ .replace('NUMBER_OF_LANGUAGES', str(len(SUPPORTED_LANGS)))\
+ .replace('SUPPORTED_LANGUAGES', ' '.join(SUPPORTED_LANGS))
+ return text.decode('utf-8')
+
+def client_ip_address(request):
+ """
+ Return client ip address for `request`.
+ Flask related
+ """
+
+ if request.headers.getlist("X-PNG-Query-For"):
+ ip_addr = request.headers.getlist("X-PNG-Query-For")[0]
+ if ip_addr.startswith('::ffff:'):
+ ip_addr = ip_addr[7:]
+ elif request.headers.getlist("X-Forwarded-For"):
+ ip_addr = request.headers.getlist("X-Forwarded-For")[0]
+ if ip_addr.startswith('::ffff:'):
+ ip_addr = ip_addr[7:]
+ else:
+ ip_addr = request.remote_addr
+
+ return ip_addr
+
+def _parse_language_header(header):
+ """
+ >>> _parse_language_header("en-US,en;q=0.9")
+ >>> _parse_language_header("en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7")
+ >>> _parse_language_header("xx, fr-CA;q=0.8, da-DK;q=0.9")
+ 'da'
+ """
+
+ def _parse_accept_language(accept_language):
+ languages = accept_language.split(",")
+ locale_q_pairs = []
+
+ for language in languages:
+ try:
+ if language.split(";")[0] == language:
+ # no q => q = 1
+ locale_q_pairs.append((language.strip(), 1))
+ else:
+ locale = language.split(";")[0].strip()
+ weight = float(language.split(";")[1].split("=")[1])
+ locale_q_pairs.append((locale, weight))
+ except (IndexError, ValueError):
+ pass
+
+ return locale_q_pairs
+
+ def _find_supported_language(accepted_languages):
+
+ def supported_langs():
+ """Yields all pairs in the Accept-Language header
+ supported in SUPPORTED_LANGS or None if 'en' is the preferred"""
+ for lang_tuple in accepted_languages:
+ lang = lang_tuple[0]
+ if '-' in lang:
+ lang = lang.split('-', 1)[0]
+ if lang in SUPPORTED_LANGS:
+ yield lang, lang_tuple[1]
+ elif lang == 'en':
+ yield None, lang_tuple[1]
+ try:
+ return max(supported_langs(), key=lambda lang_tuple:lang_tuple[1])[0]
+ except ValueError:
+ return None
+
+ return _find_supported_language(_parse_accept_language(header))
+
+def get_answer_language(request):
+ """
+ Return preferred answer language based on
+ domain name, query arguments and headers
+ """
+
+ lang = None
+ hostname = request.headers['Host']
+ if hostname != 'wttr.in' and hostname.endswith('.wttr.in'):
+ lang = hostname[:-8]
+
+ if 'lang' in request.args:
+ lang = request.args.get('lang')
+
+ 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
+
+def get_output_format(request, 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'):
+ return False
+ html_output = not any(agent in user_agent for agent in PLAIN_TEXT_AGENTS)
+ return html_output
+
+def cyclic_location_selection(locations, period):
+ """
+ Return one of `locations` (: separated list)
+ basing on the current time and query interval `period`
+ """
+
+ locations = locations.split(':')
+ max_len = max(len(x) for x in locations)
+ locations = [x.rjust(max_len) for x in locations]
+
+ try:
+ period = int(period)
+ except ValueError:
+ period = 1
+
+ index = int(time.time())/period % len(locations)
+ return locations[index]
+
+
+def wttr(location, request):
+ """
+ Main rendering function, it processes incoming weather queries.
+ Depending on user agent it returns output in HTML or ANSI format.
+
+ Incoming data:
+ request.args
+ request.headers
+ request.remote_addr
+ request.referrer
+ request.query_string
+ """
+
+ 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)
+
+ png_filename = None
+ if location is not None and location.lower().endswith(".png"):
+ png_filename = location
+ location = location[:-4]
+
+ lang = get_answer_language(request)
+ query = parse_query.parse_query(request.args)
+ html_output = get_output_format(request, query)
+ user_agent = request.headers.get('User-Agent', '').lower()
+
+ if location in PLAIN_TEXT_PAGES:
+ help_ = show_text_file(location, lang)
+ if html_output:
+ return _wrap_response(render_template('index.html', body=help_), html_output)
+ return _wrap_response(help_, html_output)
+
+ if location and ':' in location:
+ location = cyclic_location_selection(location, query.get('period', 1))
+
+ orig_location = location
+
+ if not png_filename:
+ location, override_location_name, full_address, country, query_source_location = \
+ location_processing(location, ip_addr)
+
+ us_ip = query_source_location[1] == 'United States' and 'slack' not in user_agent
+ query = parse_query.metric_or_imperial(query, lang, us_ip=us_ip)
+
+ # logging query
+ orig_location_utf8 = (orig_location or "").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 'format' in query:
+ return _wrap_response(wttr_line(location, override_location_name, query, lang), html_output)
+
+ if png_filename:
+ options = {
+ 'ip_addr': ip_addr,
+ 'lang': lang,
+ 'location': location}
+ options.update(query)
+
+ 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 location.lower() == 'moon' or location.lower().startswith('moon@'):
+ output = get_moon(location, html=html_output, lang=lang, query=query)
+ else:
+ output = get_wetter(location, ip_addr,
+ html=html_output,
+ lang=lang,
+ query=query,
+ location_name=override_location_name,
+ full_address=full_address,
+ url=request.url,
+ )
+
+ if query.get('days', '3') != '0' and not query.get('no-follow-line'):
+ if html_output:
+ output = add_buttons(output)
+ else:
+ #output += '\n' + get_message('NEW_FEATURE', lang).encode('utf-8')
+ output += '\n' + get_message('FOLLOW_ME', lang).encode('utf-8') + '\n'
+
+ return _wrap_response(output, html_output)
+
+ except RuntimeError 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"
+
+if __name__ == "__main__":
+ import doctest
+ doctest.testmod()
diff --git a/lib/wttrin_png.py b/lib/wttrin_png.py
index 86e4b50..c54b2a8 100644
--- a/lib/wttrin_png.py
+++ b/lib/wttrin_png.py
@@ -1,6 +1,7 @@
#!/usr/bin/python
#vim: encoding=utf-8
+from __future__ import print_function
import sys
import os
import re
@@ -32,7 +33,7 @@ MYDIR = os.path.abspath(os.path.dirname(os.path.dirname('__file__')))
sys.path.append("%s/lib/" % MYDIR)
import parse_query
-PNG_CACHE = os.path.join(MYDIR, "cache/png")
+from globals import PNG_CACHE, log
COLS = 180
ROWS = 100
@@ -49,6 +50,8 @@ 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/motoya-l-cedar/MTLc3m.ttf",
'Hiragana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf",
'Katakana': "/usr/share/fonts/truetype/motoya-l-cedar/MTLc3m.ttf",
@@ -95,7 +98,8 @@ def strip_buf(buf):
break
number_of_lines += 1
- buf = buf[:-number_of_lines]
+ 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]
@@ -118,7 +122,7 @@ def script_category(char):
def gen_term(filename, buf, options=None):
buf = strip_buf(buf)
- cols = len(buf[0])
+ cols = max(len(x) for x in buf)
rows = len(buf)
image = Image.new('RGB', (cols * CHAR_WIDTH, rows * CHAR_HEIGHT))
@@ -142,14 +146,15 @@ def gen_term(filename, buf, options=None):
(x_pos+CHAR_WIDTH, y_pos+CHAR_HEIGHT)),
fill=color_mapping(char.bg))
- cat = script_category(char.data)
- if cat not in font:
- print "Unknown font category: %s" % cat
- draw.text(
- (x_pos, y_pos),
- char.data,
- font=font.get(cat, font.get('default')),
- fill=current_color)
+ 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
@@ -198,7 +203,10 @@ def typescript_to_one_frame(png_file, text, options=None):
stream.feed(text)
- gen_term(png_file, screen.buffer, options=options)
+ 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
@@ -266,6 +274,8 @@ def make_wttrin_query(parsed):
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))
@@ -280,13 +290,10 @@ def make_wttr_in_png(png_name, options=None):
"""
parsed = parse_wttrin_png_name(png_name)
- print "------"
- print parsed
- print "------"
# if location is MyLocation it should be overriden
# with autodetected location (from options)
- if parsed.get('location', 'MyLocation') == 'MyLocation':
+ if parsed.get('location', 'MyLocation') == 'MyLocation' or not parsed.get('location', ''):
del parsed['location']
if options is not None:
@@ -294,7 +301,6 @@ def make_wttr_in_png(png_name, options=None):
if key not in parsed:
parsed[key] = val
url = make_wttrin_query(parsed)
- print "URL = ", url
timestamp = time.strftime("%Y%m%d%H", time.localtime())
cached_basename = url[14:].replace('/','_')
@@ -305,12 +311,11 @@ def make_wttr_in_png(png_name, options=None):
if not os.path.exists(dirname):
os.makedirs(dirname)
- print "Cached file: %s" % cached_png_file
if os.path.exists(cached_png_file):
return cached_png_file
- print "Requesting URL: %s" % url
- text = requests.get(url).text.replace('\n', '\r\n')
+ 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)
diff --git a/requirements.txt b/requirements.txt
index e87b4b4..6ab23b3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,3 +4,10 @@ geopy
requests
gevent
dnspython
+pylint
+cyrtranslit
+astral
+timezonefinder
+Pillow
+pyte
+python-dateutil
diff --git a/share/aliases b/share/aliases
index f34bf21..f370c52 100644
--- a/share/aliases
+++ b/share/aliases
@@ -1,8 +1,9 @@
Msk : Moscow
Moskva : Moscow
Moskau : Moscow
-Kyiv : Kiev
-Kiew : Kiev
+Kyiv : Kiev,Ukraine
+Kiew : Kiev,Ukraine
+Kiev : Kiev,Ukraine
Kijev : Kiev
Kharkov : Kharkiv
spb : Saint Petersburg
@@ -35,8 +36,14 @@ tel-aviv : Tel Aviv
sao paulo : São Paulo
los-angeles : Los Angeles
Sevastopol : Sevastopol, Ukraine
+Simferopol : Simferopol, Ukraine
Beersheva : Beersheba
Be'ersheva : Beersheba
Be'er Sheva : Beersheba
Lugansk : Luhansk
Bjalistoko : Białystok
+Chicago : Chicago,IL
+Paris : Paris,France
+Giessen : Giessen, Germany
+Braga : Braga, Portugal
+Kashan : ~Kashan,Iran
diff --git a/share/ansi2html.sh b/share/ansi2html.sh
index 2221a1e..b4561d6 100644
--- a/share/ansi2html.sh
+++ b/share/ansi2html.sh
@@ -104,7 +104,6 @@ printf '%s' "
-