mirror of
https://github.com/chubin/wttr.in.git
synced 2024-10-05 01:42:07 +02:00
Merge branch 'master' into patch-1
This commit is contained in:
commit
3717e30837
94
Dockerfile
94
Dockerfile
@ -1,54 +1,70 @@
|
||||
FROM ubuntu:18.04
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y curl \
|
||||
git \
|
||||
python \
|
||||
python-pip \
|
||||
python-dev \
|
||||
autoconf \
|
||||
libtool \
|
||||
gawk
|
||||
RUN curl -O https://dl.google.com/go/go1.10.3.linux-amd64.tar.gz
|
||||
RUN tar xvf go1.10.3.linux-amd64.tar.gz
|
||||
# Build stage
|
||||
FROM golang:1-alpine as builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./share/we-lang/we-lang.go /app
|
||||
|
||||
RUN apk add --no-cache git
|
||||
|
||||
|
||||
RUN go get -u github.com/mattn/go-colorable && \
|
||||
go get -u github.com/klauspost/lctime && \
|
||||
go get -u github.com/mattn/go-runewidth && \
|
||||
CGO_ENABLED=0 go build /app/we-lang.go
|
||||
# Results in /app/we-lang
|
||||
|
||||
|
||||
FROM alpine:3
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./requirements.txt /app
|
||||
|
||||
ENV LLVM_CONFIG=/usr/bin/llvm9-config
|
||||
|
||||
RUN apk add --no-cache --virtual .build \
|
||||
autoconf \
|
||||
automake \
|
||||
g++ \
|
||||
gcc \
|
||||
jpeg-dev \
|
||||
llvm9-dev\
|
||||
make \
|
||||
zlib-dev \
|
||||
&& apk add --no-cache \
|
||||
python3 \
|
||||
py3-pip \
|
||||
py3-scipy \
|
||||
py3-wheel \
|
||||
py3-gevent \
|
||||
zlib \
|
||||
jpeg \
|
||||
llvm9 \
|
||||
libtool \
|
||||
supervisor \
|
||||
py3-numpy-dev \
|
||||
python3-dev && \
|
||||
mkdir -p /app/cache && \
|
||||
mkdir -p /var/log/supervisor && \
|
||||
mkdir -p /etc/supervisor/conf.d && \
|
||||
chmod -R o+rw /var/log/supervisor && \
|
||||
chmod -R o+rw /var/run && \
|
||||
pip install -r requirements.txt --no-cache-dir && \
|
||||
apk del --no-cache -r .build
|
||||
|
||||
COPY --from=builder /app/we-lang /app/bin/we-lang
|
||||
COPY ./bin /app/bin
|
||||
COPY ./lib /app/lib
|
||||
COPY ./share /app/share
|
||||
COPY ./requirements.txt /app
|
||||
COPY ./src/we-lang/we-lang.go /app
|
||||
|
||||
# There are several files that must be fetched/created manually
|
||||
# before building the image
|
||||
COPY ./.wegorc /root
|
||||
COPY ./.ip2location.key /root
|
||||
COPY ./airports.dat /app
|
||||
COPY ./GeoLite2-City.mmdb /app
|
||||
|
||||
RUN export PATH=$PATH:/go/bin && \
|
||||
go get -u github.com/mattn/go-colorable && \
|
||||
go get -u github.com/klauspost/lctime && \
|
||||
go get -u github.com/mattn/go-runewidth && \
|
||||
export GOBIN="/root/go/bin" && \
|
||||
go install /app/we-lang.go
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
RUN mkdir /app/cache
|
||||
RUN mkdir -p /var/log/supervisor && \
|
||||
mkdir -p /etc/supervisor/conf.d
|
||||
RUN chmod -R o+rw /var/log/supervisor && \
|
||||
chmod -R o+rw /var/run
|
||||
COPY share/docker/supervisord.conf /etc/supervisor/supervisord.conf
|
||||
|
||||
ENV WTTR_MYDIR="/app"
|
||||
ENV WTTR_GEOLITE="/app/GeoLite2-City.mmdb"
|
||||
ENV WTTR_WEGO="/root/go/bin/we-lang"
|
||||
ENV WTTR_WEGO="/app/bin/we-lang"
|
||||
ENV WTTR_LISTEN_HOST="0.0.0.0"
|
||||
ENV WTTR_LISTEN_PORT="8002"
|
||||
|
||||
EXPOSE 8002
|
||||
|
||||
CMD ["/usr/local/bin/supervisord"]
|
||||
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
|
223
README.md
223
README.md
@ -32,13 +32,16 @@ but it's still a live weather report in your language.)
|
||||
|
||||
Or in PowerShell:
|
||||
|
||||
(Invoke-WebRequest http://wttr.in).Content
|
||||
```PowerShell
|
||||
Invoke-RestMethod http://wttr.in
|
||||
```
|
||||
|
||||
Want to get the weather information for a specific location? You can add the desired location to the URL in your
|
||||
request like this:
|
||||
|
||||
$ curl wttr.in/London
|
||||
$ curl wttr.in/Moscow
|
||||
$ curl wttr.in/Salt+Lake+City
|
||||
|
||||
If you omit the location name, you will get the report for your current location based on your IP address.
|
||||
|
||||
@ -88,7 +91,8 @@ wttr.in currently supports five output formats:
|
||||
* Plain-text for the terminal and scripts;
|
||||
* HTML for the browser;
|
||||
* PNG for the graphical viewers;
|
||||
* JSON for scripts and APIs.
|
||||
* JSON for scripts and APIs;
|
||||
* Prometheus metrics for scripts and APIs.
|
||||
|
||||
The ANSI and HTML formats are selected basing on the User-Agent string.
|
||||
The PNG format can be forced by adding `.png` to the end of the query:
|
||||
@ -154,13 +158,13 @@ To specify your own custom output format, use the special `%`-notation:
|
||||
c Weather condition,
|
||||
C Weather condition textual name,
|
||||
h Humidity,
|
||||
t Temperature,
|
||||
t Temperature (Actual),
|
||||
f Temperature (Feels Like),
|
||||
w Wind,
|
||||
l Location,
|
||||
m Moonphase 🌑🌒🌓🌔🌕🌖🌗🌘,
|
||||
M Moonday,
|
||||
p precipitation (mm),
|
||||
o Probability of Precipitation,
|
||||
P pressure (hPa),
|
||||
|
||||
D Dawn*,
|
||||
@ -169,7 +173,7 @@ To specify your own custom output format, use the special `%`-notation:
|
||||
s Sunset*,
|
||||
d Dusk*.
|
||||
|
||||
(times are shown in the local timezone)
|
||||
(*times are shown in the local timezone)
|
||||
```
|
||||
|
||||
So, these two calls are the same:
|
||||
@ -177,7 +181,7 @@ So, these two calls are the same:
|
||||
```
|
||||
$ curl wttr.in/London?format=3
|
||||
London: ⛅️ +7⁰C
|
||||
$ curl wttr.in/London?format="%l:+%c+%t"
|
||||
$ curl wttr.in/London?format="%l:+%c+%t\n"
|
||||
London: ⛅️ +7⁰C
|
||||
```
|
||||
Keep in mind, that when using in `tmux.conf`, you have to escape `%` with `%`, i.e. write there `%%` instead of `%`.
|
||||
@ -203,7 +207,7 @@ both of them support all necessary emoji glyphs.
|
||||
|
||||
Font configuration:
|
||||
|
||||
```
|
||||
```xml
|
||||
$ cat ~/.config/fontconfig/fonts.conf
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
@ -253,6 +257,12 @@ or
|
||||
$ curl wttr.in/München?format=v2
|
||||
```
|
||||
|
||||
or, if you prefer Nerd Fonts instead of Emoji, `v2d` (day) or `v2n` (night):
|
||||
|
||||
```
|
||||
$ curl v2d.wttr.in/München
|
||||
```
|
||||
|
||||
|
||||
![data-reach output format](https://wttr.in/files/example-wttr-v2.png)
|
||||
|
||||
@ -265,7 +275,7 @@ Currently, you need some tweaks for some terminals, to get the best possible vis
|
||||
|
||||
### URXVT
|
||||
|
||||
Depending on your configuration you might be taking all steps, or only a few. URXVT currenly doesn't support emoji related fonts, but we can get almost the same effect using *Font-Symbola*. So add to your `.Xresources` file the following line:
|
||||
Depending on your configuration you might be taking all steps, or only a few. URXVT currently doesn't support emoji related fonts, but we can get almost the same effect using *Font-Symbola*. So add to your `.Xresources` file the following line:
|
||||
```
|
||||
xft:symbola:size=10:minspace=False
|
||||
```
|
||||
@ -277,9 +287,11 @@ The result, should look like:
|
||||
|
||||
![URXVT Emoji line](https://user-images.githubusercontent.com/24360204/63842949-1d36d480-c975-11e9-81dd-998d1329bd8a.png)
|
||||
|
||||
## JSON output
|
||||
## Different output formats
|
||||
|
||||
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.
|
||||
### 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:
|
||||
|
||||
@ -288,42 +300,74 @@ To fetch information in JSON format, use the following syntax:
|
||||
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"
|
||||
}
|
||||
],
|
||||
...
|
||||
```json
|
||||
{
|
||||
"current_condition": [
|
||||
{
|
||||
"FeelsLikeC": "25",
|
||||
"FeelsLikeF": "76",
|
||||
"cloudcover": "100",
|
||||
"humidity": "76",
|
||||
"observation_time": "04:08 PM",
|
||||
"precipMM": "0.2",
|
||||
"pressure": "1019",
|
||||
"temp_C": "22",
|
||||
"temp_F": "72",
|
||||
"uvIndex": 5,
|
||||
"visibility": "16",
|
||||
"weatherCode": "122",
|
||||
"weatherDesc": [
|
||||
{
|
||||
"value": "Overcast"
|
||||
}
|
||||
],
|
||||
"weatherIconUrl": [
|
||||
{
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"winddir16Point": "NNE",
|
||||
"winddirDegree": "20",
|
||||
"windspeedKmph": "7",
|
||||
"windspeedMiles": "4"
|
||||
}
|
||||
],
|
||||
...
|
||||
```
|
||||
|
||||
Most of these values are self-explanatory, aside from `weatherCode`. The `weatherCode` is an enumeration which you can find at either [the WorldWeatherOnline website](https://www.worldweatheronline.com/developer/api/docs/weather-icons.aspx) or [in the wttr.in source code](https://github.com/chubin/wttr.in/blob/master/lib/constants.py).
|
||||
|
||||
### Prometheus Metrics Output
|
||||
|
||||
The [Prometheus](https://github.com/prometheus/prometheus) Metrics format is a feature providing access to *wttr.in* data through an easy-to-parse format for monitoring systems, without requiring the user to create a complex script to reinterpret wttr.in's graphical output.
|
||||
|
||||
To fetch information in Prometheus format, use the following syntax:
|
||||
|
||||
$ curl wttr.in/Detroit?format=p1
|
||||
|
||||
This will fetch information on the Detroit region in Prometheus Metrics format. The `p1` format code is used to allow for the use of other layouts for the Prometheus Metrics output.
|
||||
|
||||
A possible configuration for Prometheus could look like this:
|
||||
|
||||
```yaml
|
||||
- job_name: 'wttr_in_detroit'
|
||||
static_configs:
|
||||
- targets: ['wttr.in']
|
||||
metrics_path: '/Detroit'
|
||||
params:
|
||||
format: ['p1']
|
||||
```
|
||||
|
||||
The result will look something like the following:
|
||||
|
||||
|
||||
# HELP temperature_feels_like_celsius Feels Like Temperature in Celsius
|
||||
temperature_feels_like_celsius{forecast="current"} 7
|
||||
# HELP temperature_feels_like_fahrenheit Feels Like Temperature in Fahrenheit
|
||||
temperature_feels_like_fahrenheit{forecast="current"} 45
|
||||
[truncated]
|
||||
...
|
||||
|
||||
|
||||
## Moon phases
|
||||
|
||||
@ -343,10 +387,10 @@ To get the moon phase information in the online mode, use `%m`:
|
||||
$ curl wttr.in/London?format=%m
|
||||
🌖
|
||||
|
||||
Keep in mid that the Unicode representation of moonphases suffers 2 caveats:
|
||||
Keep in mind that the Unicode representation of moonphases suffers 2 caveats:
|
||||
|
||||
- With some fonts, the representation `🌘` is ambiguous, for it either seem
|
||||
almost-shadowed or almost-lit, depedending on whether your terminal is in
|
||||
almost-shadowed or almost-lit, depending on whether your terminal is in
|
||||
light mode or dark mode. Relying on colored fonts like `noto-fonts` works
|
||||
around this problem.
|
||||
|
||||
@ -480,6 +524,19 @@ $ echo 'YOUR_IP2LOCATION_KEY' > ~/.ip2location.key
|
||||
If you don't have this file, the service will be silently skipped (it is not a big problem,
|
||||
because the MaxMind database is pretty good).
|
||||
|
||||
### Installation with Docker
|
||||
|
||||
* Install Docker
|
||||
* Build Docker Image
|
||||
* These files should be mounted by the user at runtime:
|
||||
|
||||
```
|
||||
/root/.wegorc
|
||||
/root/.ip2location.key (optional)
|
||||
/app/airports.dat
|
||||
/app/GeoLite2-City.mmdb
|
||||
```
|
||||
|
||||
### Get a WorldWeatherOnline key and configure wego
|
||||
|
||||
To get a WorldWeatherOnline API key, you must register here:
|
||||
@ -491,14 +548,16 @@ WWO key file: `~/.wwo.key`
|
||||
|
||||
Also, you have to specify the key in the `wego` configuration:
|
||||
|
||||
$ cat ~/.wegorc
|
||||
{
|
||||
"APIKey": "00XXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"City": "London",
|
||||
"Numdays": 3,
|
||||
"Imperial": false,
|
||||
"Lang": "en"
|
||||
}
|
||||
```json
|
||||
$ cat ~/.wegorc
|
||||
{
|
||||
"APIKey": "00XXXXXXXXXXXXXXXXXXXXXXXXXXX",
|
||||
"City": "London",
|
||||
"Numdays": 3,
|
||||
"Imperial": false,
|
||||
"Lang": "en"
|
||||
}
|
||||
```
|
||||
|
||||
The `City` parameter in `~/.wegorc` is ignored.
|
||||
|
||||
@ -507,42 +566,46 @@ The `City` parameter in `~/.wegorc` is ignored.
|
||||
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"
|
||||
export WTTR_WEGO="/home/igor/go/bin/wego"
|
||||
export WTTR_LISTEN_HOST="0.0.0.0"
|
||||
export WTTR_LISTEN_PORT="8002"
|
||||
```bash
|
||||
export WTTR_MYDIR="/home/igor/wttr.in"
|
||||
export WTTR_GEOLITE="/home/igor/wttr.in/GeoLite2-City.mmdb"
|
||||
export WTTR_WEGO="/home/igor/go/bin/wego"
|
||||
export WTTR_LISTEN_HOST="0.0.0.0"
|
||||
export WTTR_LISTEN_PORT="8002"
|
||||
```
|
||||
|
||||
|
||||
### Configure the HTTP-frontend service
|
||||
|
||||
It's recommended that you also configure the web server that will be used to access the service:
|
||||
|
||||
server {
|
||||
listen [::]:80;
|
||||
server_name wttr.in *.wttr.in;
|
||||
access_log /var/log/nginx/wttr.in-access.log main;
|
||||
error_log /var/log/nginx/wttr.in-error.log;
|
||||
```nginx
|
||||
server {
|
||||
listen [::]:80;
|
||||
server_name wttr.in *.wttr.in;
|
||||
access_log /var/log/nginx/wttr.in-access.log main;
|
||||
error_log /var/log/nginx/wttr.in-error.log;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8002;
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8002;
|
||||
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
|
||||
client_max_body_size 10m;
|
||||
client_body_buffer_size 128k;
|
||||
client_max_body_size 10m;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
proxy_connect_timeout 90;
|
||||
proxy_send_timeout 90;
|
||||
proxy_read_timeout 90;
|
||||
proxy_connect_timeout 90;
|
||||
proxy_send_timeout 90;
|
||||
proxy_read_timeout 90;
|
||||
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 4 32k;
|
||||
proxy_busy_buffers_size 64k;
|
||||
proxy_temp_file_write_size 64k;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 4 32k;
|
||||
proxy_busy_buffers_size 64k;
|
||||
proxy_temp_file_write_size 64k;
|
||||
|
||||
expires off;
|
||||
}
|
||||
}
|
||||
expires off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
75
bin/proxy.py
Normal file → Executable file
75
bin/proxy.py
Normal file → Executable file
@ -36,7 +36,8 @@ 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 globals import PROXY_CACHEDIR, PROXY_HOST, PROXY_PORT, USE_METNO, USER_AGENT
|
||||
from metno import create_standard_json_from_metno, metno_request
|
||||
from translations import PROXY_LANGS
|
||||
# pylint: enable=wrong-import-position
|
||||
|
||||
@ -71,7 +72,12 @@ def load_translations():
|
||||
return translations
|
||||
TRANSLATIONS = load_translations()
|
||||
|
||||
def _is_metno():
|
||||
return USE_METNO
|
||||
|
||||
def _find_srv_for_query(path, query): # pylint: disable=unused-argument
|
||||
if _is_metno():
|
||||
return 'https://api.met.no'
|
||||
return 'http://api.worldweatheronline.com'
|
||||
|
||||
def _cache_file(path, query):
|
||||
@ -84,7 +90,7 @@ def _cache_file(path, query):
|
||||
|
||||
digest = hashlib.sha1(("%s %s" % (path, query)).encode("utf-8")).hexdigest()
|
||||
digest_number = ord(digest[0].upper())
|
||||
expiry_interval = 60*(digest_number+10)
|
||||
expiry_interval = 60*(digest_number+40)
|
||||
|
||||
timestamp = "%010d" % (int(time.time())//expiry_interval*expiry_interval)
|
||||
filename = os.path.join(PROXY_CACHEDIR, timestamp, path, query)
|
||||
@ -142,7 +148,7 @@ def add_translations(content, lang):
|
||||
returned by the data source
|
||||
"""
|
||||
|
||||
if content is "{}":
|
||||
if content == "{}":
|
||||
return {}
|
||||
|
||||
languages_to_translate = TRANSLATIONS.keys()
|
||||
@ -155,7 +161,10 @@ def add_translations(content, lang):
|
||||
return {}
|
||||
|
||||
try:
|
||||
weather_condition = d['data']['current_condition'][0]['weatherDesc'][0]['value']
|
||||
weather_condition = d['data']['current_condition'
|
||||
][0]['weatherDesc'][0]['value'].capitalize()
|
||||
d['data']['current_condition'][0]['weatherDesc'][0]['value'] = \
|
||||
weather_condition
|
||||
if lang in languages_to_translate:
|
||||
d['data']['current_condition'][0]['lang_%s' % lang] = \
|
||||
[{'value': translate(weather_condition, lang)}]
|
||||
@ -201,18 +210,7 @@ def add_translations(content, lang):
|
||||
print(exception)
|
||||
return content
|
||||
|
||||
@APP.route("/<path:path>")
|
||||
def proxy(path):
|
||||
"""
|
||||
Main proxy function. Handles incoming HTTP queries.
|
||||
"""
|
||||
|
||||
lang = request.args.get('lang', 'en')
|
||||
query_string = request.query_string.decode("utf-8")
|
||||
query_string = query_string.replace('sr-lat', 'sr')
|
||||
query_string = query_string.replace('lang=None', 'lang=en')
|
||||
query_string += "&extra=localObsTime"
|
||||
query_string += "&includelocation=yes"
|
||||
def _fetch_content_and_headers(path, query_string, **kwargs):
|
||||
content, headers = _load_content_and_headers(path, query_string)
|
||||
|
||||
if content is None:
|
||||
@ -223,7 +221,7 @@ def proxy(path):
|
||||
response = None
|
||||
while attempts:
|
||||
try:
|
||||
response = requests.get(url, timeout=2)
|
||||
response = requests.get(url, timeout=2, **kwargs)
|
||||
except requests.ReadTimeout:
|
||||
attempts -= 1
|
||||
continue
|
||||
@ -243,6 +241,34 @@ def proxy(path):
|
||||
content = "{}"
|
||||
else:
|
||||
print("cache found")
|
||||
return content, headers
|
||||
|
||||
|
||||
@APP.route("/<path:path>")
|
||||
def proxy(path):
|
||||
"""
|
||||
Main proxy function. Handles incoming HTTP queries.
|
||||
"""
|
||||
|
||||
lang = request.args.get('lang', 'en')
|
||||
query_string = request.query_string.decode("utf-8")
|
||||
query_string = query_string.replace('sr-lat', 'sr')
|
||||
query_string = query_string.replace('lang=None', 'lang=en')
|
||||
content = ""
|
||||
headers = ""
|
||||
if _is_metno():
|
||||
path, query, days = metno_request(path, query_string)
|
||||
if USER_AGENT == '':
|
||||
raise ValueError('User agent must be set to adhere to metno ToS: https://api.met.no/doc/TermsOfService')
|
||||
content, headers = _fetch_content_and_headers(path, query, headers={
|
||||
'User-Agent': USER_AGENT
|
||||
})
|
||||
content = create_standard_json_from_metno(content, days)
|
||||
else:
|
||||
# WWO tweaks
|
||||
query_string += "&extra=localObsTime"
|
||||
query_string += "&includelocation=yes"
|
||||
content, headers = _fetch_content_and_headers(path, query)
|
||||
|
||||
content = add_translations(content, lang)
|
||||
|
||||
@ -251,6 +277,15 @@ def proxy(path):
|
||||
if __name__ == "__main__":
|
||||
#app.run(host='0.0.0.0', port=5001, debug=False)
|
||||
#app.debug = True
|
||||
bind_addr = "0.0.0.0"
|
||||
SERVER = WSGIServer((bind_addr, PROXY_PORT), APP)
|
||||
SERVER.serve_forever()
|
||||
if len(sys.argv) == 1:
|
||||
bind_addr = "0.0.0.0"
|
||||
SERVER = WSGIServer((bind_addr, PROXY_PORT), APP)
|
||||
SERVER.serve_forever()
|
||||
else:
|
||||
print('running single request from command line arg')
|
||||
APP.testing = True
|
||||
with APP.test_client() as c:
|
||||
resp = c.get(sys.argv[1])
|
||||
print('Status: ' + resp.status)
|
||||
# print('Headers: ' + dumps(resp.headers))
|
||||
print(resp.data.decode('utf-8'))
|
||||
|
79
cmd/peakHandling.go
Normal file
79
cmd/peakHandling.go
Normal file
@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/robfig/cron"
|
||||
)
|
||||
|
||||
var peakRequest30 sync.Map
|
||||
var peakRequest60 sync.Map
|
||||
|
||||
func initPeakHandling() {
|
||||
c := cron.New()
|
||||
// cronTime := fmt.Sprintf("%d,%d * * * *", 30-prefetchInterval/60, 60-prefetchInterval/60)
|
||||
c.AddFunc("24 * * * *", prefetchPeakRequests30)
|
||||
c.AddFunc("54 * * * *", prefetchPeakRequests60)
|
||||
c.Start()
|
||||
}
|
||||
|
||||
func savePeakRequest(cacheDigest string, r *http.Request) {
|
||||
_, min, _ := time.Now().Clock()
|
||||
if min == 30 {
|
||||
peakRequest30.Store(cacheDigest, *r)
|
||||
} else if min == 0 {
|
||||
peakRequest60.Store(cacheDigest, *r)
|
||||
}
|
||||
}
|
||||
|
||||
func prefetchRequest(r *http.Request) {
|
||||
processRequest(r)
|
||||
}
|
||||
|
||||
func syncMapLen(sm *sync.Map) int {
|
||||
count := 0
|
||||
|
||||
f := func(key, value interface{}) bool {
|
||||
|
||||
// Not really certain about this part, don't know for sure
|
||||
// if this is a good check for an entry's existence
|
||||
if key == "" {
|
||||
return false
|
||||
}
|
||||
count++
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
sm.Range(f)
|
||||
|
||||
return count
|
||||
}
|
||||
|
||||
func prefetchPeakRequests(peakRequestMap *sync.Map) {
|
||||
peakRequestLen := syncMapLen(peakRequestMap)
|
||||
log.Printf("PREFETCH: Prefetching %d requests\n", peakRequestLen)
|
||||
if peakRequestLen == 0 {
|
||||
return
|
||||
}
|
||||
sleepBetweenRequests := time.Duration(prefetchInterval*1000/peakRequestLen) * time.Millisecond
|
||||
peakRequestMap.Range(func(key interface{}, value interface{}) bool {
|
||||
go func(r http.Request) {
|
||||
prefetchRequest(&r)
|
||||
}(value.(http.Request))
|
||||
peakRequestMap.Delete(key)
|
||||
time.Sleep(sleepBetweenRequests)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func prefetchPeakRequests30() {
|
||||
prefetchPeakRequests(&peakRequest30)
|
||||
}
|
||||
|
||||
func prefetchPeakRequests60() {
|
||||
prefetchPeakRequests(&peakRequest60)
|
||||
}
|
147
cmd/processRequest.go
Normal file
147
cmd/processRequest.go
Normal file
@ -0,0 +1,147 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func processRequest(r *http.Request) responseWithHeader {
|
||||
var response responseWithHeader
|
||||
|
||||
if dontCache(r) {
|
||||
return get(r)
|
||||
}
|
||||
|
||||
cacheDigest := getCacheDigest(r)
|
||||
|
||||
foundInCache := false
|
||||
|
||||
savePeakRequest(cacheDigest, r)
|
||||
|
||||
cacheBody, ok := lruCache.Get(cacheDigest)
|
||||
if ok {
|
||||
cacheEntry := cacheBody.(responseWithHeader)
|
||||
|
||||
// if after all attempts we still have no answer,
|
||||
// we try to make the query on our own
|
||||
for attempts := 0; attempts < 300; attempts++ {
|
||||
if !ok || !cacheEntry.InProgress {
|
||||
break
|
||||
}
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
cacheBody, ok = lruCache.Get(cacheDigest)
|
||||
cacheEntry = cacheBody.(responseWithHeader)
|
||||
}
|
||||
if cacheEntry.InProgress {
|
||||
log.Printf("TIMEOUT: %s\n", cacheDigest)
|
||||
}
|
||||
if ok && !cacheEntry.InProgress && cacheEntry.Expires.After(time.Now()) {
|
||||
response = cacheEntry
|
||||
foundInCache = true
|
||||
}
|
||||
}
|
||||
|
||||
if !foundInCache {
|
||||
lruCache.Add(cacheDigest, responseWithHeader{InProgress: true})
|
||||
response = get(r)
|
||||
if response.StatusCode == 200 || response.StatusCode == 304 {
|
||||
lruCache.Add(cacheDigest, response)
|
||||
} else {
|
||||
log.Printf("REMOVE: %d response for %s from cache\n", response.StatusCode, cacheDigest)
|
||||
lruCache.Remove(cacheDigest)
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Printf("Request: %s\n", err)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
return responseWithHeader{
|
||||
InProgress: false,
|
||||
Expires: time.Now().Add(time.Duration(randInt(1000, 1500)) * time.Second),
|
||||
Body: body,
|
||||
Header: res.Header,
|
||||
StatusCode: res.StatusCode,
|
||||
}
|
||||
}
|
||||
|
||||
// implementation of the cache.get_signature of original wttr.in
|
||||
func getCacheDigest(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")
|
||||
|
||||
return fmt.Sprintf("%s:%s%s:%s:%s", userAgent, queryHost, queryString, clientIPAddress, lang)
|
||||
}
|
||||
|
||||
// return true if request should not be cached
|
||||
func dontCache(req *http.Request) bool {
|
||||
|
||||
// dont cache cyclic requests
|
||||
loc := strings.Split(req.RequestURI, "?")[0]
|
||||
if strings.Contains(loc, ":") {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
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 {
|
||||
log.Printf("ERROR: userip: %q is not IP:port\n", IPAddress)
|
||||
}
|
||||
}
|
||||
return IPAddress
|
||||
}
|
||||
|
||||
func randInt(min int, max int) int {
|
||||
return min + rand.Intn(max-min)
|
||||
}
|
116
cmd/srv.go
116
cmd/srv.go
@ -2,113 +2,48 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/golang-lru"
|
||||
lru "github.com/hashicorp/golang-lru"
|
||||
)
|
||||
|
||||
const uplinkSrvAddr = "127.0.0.1:9002"
|
||||
const uplinkTimeout = 30
|
||||
const prefetchInterval = 300
|
||||
const lruCacheSize = 12800
|
||||
|
||||
var lruCache *lru.Cache
|
||||
|
||||
type ResponseWithHeader struct {
|
||||
type responseWithHeader struct {
|
||||
InProgress bool // true if the request is being processed
|
||||
Expires time.Time // expiration time of the cache entry
|
||||
|
||||
Body []byte
|
||||
Header http.Header
|
||||
StatusCode int // e.g. 200
|
||||
|
||||
}
|
||||
|
||||
func init() {
|
||||
var err error
|
||||
lruCache, err = lru.New(12800)
|
||||
lruCache, err = lru.New(lruCacheSize)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
Timeout: uplinkTimeout * time.Second,
|
||||
KeepAlive: uplinkTimeout * 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)
|
||||
http.DefaultTransport.(*http.Transport).DialContext = func(ctx context.Context, network, _ string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, network, uplinkSrvAddr)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
initPeakHandling()
|
||||
}
|
||||
|
||||
func copyHeader(dst, src http.Header) {
|
||||
@ -120,26 +55,15 @@ func copyHeader(dst, src http.Header) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
var response ResponseWithHeader
|
||||
// printStat()
|
||||
response := processRequest(r)
|
||||
|
||||
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.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.WriteHeader(response.StatusCode)
|
||||
w.Write(response.Body)
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8081", nil))
|
||||
|
||||
log.Fatal(http.ListenAndServe(":8082", nil))
|
||||
}
|
||||
|
40
cmd/stat.go
Normal file
40
cmd/stat.go
Normal file
@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type safeCounter struct {
|
||||
v map[int]int
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
func (c *safeCounter) inc(key int) {
|
||||
c.mux.Lock()
|
||||
c.v[key]++
|
||||
c.mux.Unlock()
|
||||
}
|
||||
|
||||
// func (c *safeCounter) val(key int) int {
|
||||
// c.mux.Lock()
|
||||
// defer c.mux.Unlock()
|
||||
// return c.v[key]
|
||||
// }
|
||||
//
|
||||
// func (c *safeCounter) reset(key int) int {
|
||||
// c.mux.Lock()
|
||||
// defer c.mux.Unlock()
|
||||
// result := c.v[key]
|
||||
// c.v[key] = 0
|
||||
// return result
|
||||
// }
|
||||
|
||||
var queriesPerMinute safeCounter
|
||||
|
||||
func printStat() {
|
||||
_, min, _ := time.Now().Clock()
|
||||
queriesPerMinute.inc(min)
|
||||
log.Printf("Processed %d requests\n", min)
|
||||
}
|
17
lib/cache.py
17
lib/cache.py
@ -34,8 +34,20 @@ def get_signature(user_agent, query_string, client_ip_address, lang):
|
||||
"""
|
||||
Get cache signature based on `user_agent`, `url_string`,
|
||||
`lang`, and `client_ip_address`
|
||||
Return `None` if query should not be cached.
|
||||
"""
|
||||
|
||||
if "?" in query_string:
|
||||
location = query_string.split("?", 1)[0]
|
||||
else:
|
||||
location = query_string
|
||||
if location.startswith("http://"):
|
||||
location = location[7:]
|
||||
elif location.startswith("https://"):
|
||||
location = location[8:]
|
||||
if ":" in location:
|
||||
return None
|
||||
|
||||
signature = "%s:%s:%s:%s" % \
|
||||
(user_agent, query_string, client_ip_address, lang)
|
||||
print(signature)
|
||||
@ -48,6 +60,9 @@ def get(signature):
|
||||
the `_update_answer` function.
|
||||
"""
|
||||
|
||||
if not signature:
|
||||
return None
|
||||
|
||||
value_record = CACHE.get(signature)
|
||||
if not value_record:
|
||||
return None
|
||||
@ -69,6 +84,8 @@ def store(signature, value):
|
||||
"""
|
||||
Store in cache `value` for `signature`
|
||||
"""
|
||||
if not signature:
|
||||
return _update_answer(value)
|
||||
|
||||
if len(value) >= MIN_SIZE_FOR_FILECACHE:
|
||||
value_to_store = _store_in_file(signature, value)
|
||||
|
@ -103,6 +103,68 @@ MOON_PHASES = (
|
||||
"🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"
|
||||
)
|
||||
|
||||
WEATHER_SYMBOL_WI_DAY = {
|
||||
"Unknown": "",
|
||||
"Cloudy": "",
|
||||
"Fog": "",
|
||||
"HeavyRain": "",
|
||||
"HeavyShowers": "",
|
||||
"HeavySnow": "",
|
||||
"HeavySnowShowers": "",
|
||||
"LightRain": "",
|
||||
"LightShowers": "",
|
||||
"LightSleet": "",
|
||||
"LightSleetShowers": "",
|
||||
"LightSnow": "",
|
||||
"LightSnowShowers": "",
|
||||
"PartlyCloudy": "",
|
||||
"Sunny": "",
|
||||
"ThunderyHeavyRain": "",
|
||||
"ThunderyShowers": "",
|
||||
"ThunderySnowShowers": "",
|
||||
"VeryCloudy": "",
|
||||
}
|
||||
|
||||
WEATHER_SYMBOL_WI_NIGHT = {
|
||||
"Unknown": "",
|
||||
"Cloudy": "",
|
||||
"Fog": "",
|
||||
"HeavyRain": "",
|
||||
"HeavyShowers": "",
|
||||
"HeavySnow": "",
|
||||
"HeavySnowShowers": "",
|
||||
"LightRain": "",
|
||||
"LightShowers": "",
|
||||
"LightSleet": "",
|
||||
"LightSleetShowers": "",
|
||||
"LightSnow": "",
|
||||
"LightSnowShowers": "",
|
||||
"PartlyCloudy": "",
|
||||
"Sunny": "",
|
||||
"ThunderyHeavyRain": "",
|
||||
"ThunderyShowers": "",
|
||||
"ThunderySnowShowers": "",
|
||||
"VeryCloudy": "",
|
||||
}
|
||||
|
||||
WEATHER_SYMBOL_WIDTH_VTE_WI = {
|
||||
}
|
||||
|
||||
WIND_DIRECTION_WI = [
|
||||
"", "", "", "", "", "", "", "",
|
||||
]
|
||||
|
||||
WIND_SCALE_WI = [
|
||||
"", "", "", "", "", "", "", "", "", "", "", "", "",
|
||||
]
|
||||
|
||||
MOON_PHASES_WI = (
|
||||
"", "", "", "", "", "", "",
|
||||
"", "", "", "", "", "", "",
|
||||
"", "", "", "", "", "", "",
|
||||
"", "", "", "", "", "", "",
|
||||
)
|
||||
|
||||
WEATHER_SYMBOL_WEGO = {
|
||||
"Unknown": [
|
||||
" .-. ",
|
||||
@ -168,7 +230,7 @@ WEATHER_SYMBOL_WEGO = {
|
||||
"\033[38;5;226m _`/\"\"\033[38;5;250m.-. \033[0m",
|
||||
"\033[38;5;226m ,\\_\033[38;5;250m( ). \033[0m",
|
||||
"\033[38;5;226m /\033[38;5;250m(___(__) \033[0m",
|
||||
"\033[38;5;228;5m ⚡\033[38;5;111;25m‘‘\033[38;5;228;5m⚡\033[38;5;111;25m‘‘ \033[0m",
|
||||
"\033[38;5;228;5m ⚡\033[38;5;111;25m‘ ‘\033[38;5;228;5m⚡\033[38;5;111;25m‘ ‘ \033[0m",
|
||||
"\033[38;5;111m ‘ ‘ ‘ ‘ \033[0m"],
|
||||
"ThunderyHeavyRain": [
|
||||
"\033[38;5;240;1m .-. \033[0m",
|
||||
|
105
lib/fields.py
Normal file
105
lib/fields.py
Normal file
@ -0,0 +1,105 @@
|
||||
"""
|
||||
Human readable description of the available data fields
|
||||
describing current weather, weather forecast, and astronomical data
|
||||
"""
|
||||
|
||||
DESCRIPTION = {
|
||||
# current condition fields
|
||||
"FeelsLikeC": (
|
||||
"Feels Like Temperature in Celsius",
|
||||
"temperature_feels_like_celsius"),
|
||||
"FeelsLikeF": (
|
||||
"Feels Like Temperature in Fahrenheit",
|
||||
"temperature_feels_like_fahrenheit"),
|
||||
"cloudcover": (
|
||||
"Cloud Coverage in Percent",
|
||||
"cloudcover_percentage"),
|
||||
"humidity": (
|
||||
"Humidity in Percent",
|
||||
"humidity_percentage"),
|
||||
"precipMM": (
|
||||
"Precipitation (Rainfall) in mm",
|
||||
"precipitation_mm"),
|
||||
"pressure": (
|
||||
"Air pressure in hPa",
|
||||
"pressure_hpa"),
|
||||
"temp_C": (
|
||||
"Temperature in Celsius",
|
||||
"temperature_celsius"),
|
||||
"temp_F": (
|
||||
"Temperature in Fahrenheit",
|
||||
"temperature_fahrenheit"),
|
||||
"uvIndex": (
|
||||
"Ultaviolet Radiation Index",
|
||||
"uv_index"),
|
||||
"visibility": (
|
||||
"Visible Distance in Kilometres",
|
||||
"visibility"),
|
||||
"weatherCode": (
|
||||
"Code to describe Weather Condition",
|
||||
"weather_code"),
|
||||
"winddirDegree": (
|
||||
"Wind Direction in Degree",
|
||||
"winddir_degree"),
|
||||
"windspeedKmph": (
|
||||
"Wind Speed in Kilometres per Hour",
|
||||
"windspeed_kmph"),
|
||||
"windspeedMiles": (
|
||||
"Wind Speed in Miles per Hour",
|
||||
"windspeed_mph"),
|
||||
"observation_time": (
|
||||
"Minutes since start of the day the observation happened",
|
||||
"observation_time"),
|
||||
|
||||
# fields with `description`
|
||||
"weatherDesc": (
|
||||
"Weather Description",
|
||||
"weather_desc"),
|
||||
"winddir16Point": (
|
||||
"Wind Direction on a 16-wind compass rose",
|
||||
"winddir_16_point"),
|
||||
|
||||
# forecast fields
|
||||
"maxtempC": (
|
||||
"Maximum Temperature in Celsius",
|
||||
"temperature_celsius_maximum"),
|
||||
"maxtempF": (
|
||||
"Maximum Temperature in Fahrenheit",
|
||||
"temperature_fahrenheit_maximum"),
|
||||
"mintempC": (
|
||||
"Minimum Temperature in Celsius",
|
||||
"temperature_celsius_minimum"),
|
||||
"mintempF": (
|
||||
"Minimum Temperature in Fahrenheit",
|
||||
"temperature_fahrenheit_minimum"),
|
||||
"sunHour":(
|
||||
"Hours of sunlight",
|
||||
"sun_hour"),
|
||||
"totalSnow_cm":(
|
||||
"Total snowfall in cm",
|
||||
"snowfall_cm"),
|
||||
|
||||
# astronomy fields
|
||||
"moon_illumination": (
|
||||
"Percentage of the moon illuminated",
|
||||
"astronomy_moon_illumination"),
|
||||
|
||||
# astronomy fields with description
|
||||
"moon_phase": (
|
||||
"Phase of the moon",
|
||||
"astronomy_moon_phase"),
|
||||
|
||||
# astronomy fields with time
|
||||
"moonrise": (
|
||||
"Minutes since start of the day untill the moon appears above the horizon",
|
||||
"astronomy_moonrise_min"),
|
||||
"moonset": (
|
||||
"Minutes since start of the day untill the moon disappears below the horizon",
|
||||
"astronomy_moonset_min"),
|
||||
"sunrise": (
|
||||
"Minutes since start of the day untill the sun appears above the horizon",
|
||||
"astronomy_sunrise_min"),
|
||||
"sunset": (
|
||||
"Minutes since start of the day untill the moon disappears below the horizon",
|
||||
"astronomy_sunset_min"),
|
||||
}
|
@ -8,6 +8,7 @@ External environment variables:
|
||||
WTTR_WEGO
|
||||
WTTR_LISTEN_HOST
|
||||
WTTR_LISTEN_PORT
|
||||
WTTR_USER_AGENT
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
@ -24,7 +25,7 @@ else:
|
||||
GEOLITE = os.path.join(MYDIR, 'data', "GeoLite2-City.mmdb")
|
||||
|
||||
WEGO = os.environ.get("WTTR_WEGO", "/home/igor/go/bin/we-lang")
|
||||
PYPHOON = "/home/igor/pyphoon/bin/pyphoon-lolcat"
|
||||
PYPHOON = "pyphoon-lolcat"
|
||||
|
||||
_DATADIR = "/wttr.in"
|
||||
_LOGDIR = "/wttr.in/log"
|
||||
@ -83,15 +84,34 @@ PLAIN_TEXT_AGENTS = [
|
||||
|
||||
PLAIN_TEXT_PAGES = [':help', ':bash.function', ':translation', ':iterm2']
|
||||
|
||||
_IP2LOCATION_KEY_FILE = os.environ['HOME'] + '/.ip2location.key'
|
||||
_IPLOCATION_ORDER = os.environ.get(
|
||||
"WTTR_IPLOCATION_ORDER",
|
||||
'geoip,ip2location,ipinfo')
|
||||
IPLOCATION_ORDER = _IPLOCATION_ORDER.split(',')
|
||||
|
||||
_IP2LOCATION_KEY_FILE = os.environ.get(
|
||||
"WTTR_IP2LOCATION_KEY_FILE",
|
||||
os.environ['HOME'] + '/.ip2location.key')
|
||||
IP2LOCATION_KEY = None
|
||||
if os.path.exists(_IP2LOCATION_KEY_FILE):
|
||||
IP2LOCATION_KEY = open(_IP2LOCATION_KEY_FILE, 'r').read().strip()
|
||||
|
||||
_WWO_KEY_FILE = os.environ['HOME'] + '/.wwo.key'
|
||||
_IPINFO_KEY_FILE = os.environ.get(
|
||||
"WTTR_IPINFO_KEY_FILE",
|
||||
os.environ['HOME'] + '/.ipinfo.key')
|
||||
IPINFO_TOKEN = None
|
||||
if os.path.exists(_IPINFO_KEY_FILE):
|
||||
IPINFO_TOKEN = open(_IPINFO_KEY_FILE, 'r').read().strip()
|
||||
|
||||
_WWO_KEY_FILE = os.environ.get(
|
||||
"WTTR_WWO_KEY_FILE",
|
||||
os.environ['HOME'] + '/.wwo.key')
|
||||
WWO_KEY = "key-is-not-specified"
|
||||
USE_METNO = True
|
||||
USER_AGENT = os.environ.get("WTTR_USER_AGENT", "")
|
||||
if os.path.exists(_WWO_KEY_FILE):
|
||||
WWO_KEY = open(_WWO_KEY_FILE, 'r').read().strip()
|
||||
USE_METNO = False
|
||||
|
||||
def error(text):
|
||||
"log error `text` and raise a RuntimeError exception"
|
||||
|
134
lib/location.py
134
lib/location.py
@ -17,7 +17,7 @@ import requests
|
||||
import geoip2.database
|
||||
|
||||
from globals import GEOLITE, GEOLOCATOR_SERVICE, IP2LCACHE, IP2LOCATION_KEY, NOT_FOUND_LOCATION, \
|
||||
ALIASES, BLACKLIST, IATA_CODES_FILE
|
||||
ALIASES, BLACKLIST, IATA_CODES_FILE, IPLOCATION_ORDER, IPINFO_TOKEN
|
||||
|
||||
GEOIP_READER = geoip2.database.Reader(GEOLITE)
|
||||
|
||||
@ -88,9 +88,15 @@ def geolocator(location):
|
||||
|
||||
return None
|
||||
|
||||
def ip2location(ip_addr):
|
||||
"Convert IP address `ip_addr` to a location name"
|
||||
|
||||
def ipcachewrite(ip_addr, location):
|
||||
cached = os.path.join(IP2LCACHE, ip_addr)
|
||||
if not os.path.exists(IP2LCACHE):
|
||||
os.makedirs(IP2LCACHE)
|
||||
with open(cached, 'w') as file:
|
||||
file.write(location[0] + ';' + location[1])
|
||||
|
||||
def ipcache(ip_addr):
|
||||
cached = os.path.join(IP2LCACHE, ip_addr)
|
||||
if not os.path.exists(IP2LCACHE):
|
||||
os.makedirs(IP2LCACHE)
|
||||
@ -98,33 +104,57 @@ def ip2location(ip_addr):
|
||||
location = None
|
||||
|
||||
if os.path.exists(cached):
|
||||
location = open(cached, 'r').read()
|
||||
else:
|
||||
# if IP2LOCATION_KEY is not set, do not the query,
|
||||
# because the query wont be processed anyway
|
||||
if IP2LOCATION_KEY:
|
||||
try:
|
||||
ip2location_response = requests\
|
||||
.get('http://api.ip2location.com/?ip=%s&key=%s&package=WS3' \
|
||||
% (ip_addr, IP2LOCATION_KEY)).text
|
||||
if ';' in ip2location_response:
|
||||
open(cached, 'w').write(ip2location_response)
|
||||
location = ip2location_response
|
||||
except requests.exceptions.ConnectionError:
|
||||
pass
|
||||
location = open(cached, 'r').read().split(';')
|
||||
if len(location) > 3:
|
||||
return location[3], location[1]
|
||||
elif len(location) > 1:
|
||||
return location[0], location[1]
|
||||
else:
|
||||
return location[0], None
|
||||
|
||||
return None, None
|
||||
|
||||
def ip2location(ip_addr):
|
||||
"Convert IP address `ip_addr` to a location name"
|
||||
|
||||
location = ipcache(ip_addr)
|
||||
if location:
|
||||
return location
|
||||
|
||||
# if IP2LOCATION_KEY is not set, do not the query,
|
||||
# because the query wont be processed anyway
|
||||
if IP2LOCATION_KEY:
|
||||
try:
|
||||
location = requests\
|
||||
.get('http://api.ip2location.com/?ip=%s&key=%s&package=WS3' \
|
||||
% (ip_addr, IP2LOCATION_KEY)).text
|
||||
except requests.exceptions.ConnectionError:
|
||||
pass
|
||||
|
||||
if location and ';' in location:
|
||||
ipcachewrite(ip_addr, location)
|
||||
location = location.split(';')[3], location.split(';')[1]
|
||||
else:
|
||||
location = location, None
|
||||
|
||||
return location
|
||||
|
||||
def get_location(ip_addr):
|
||||
"""
|
||||
Return location pair (CITY, COUNTRY) for `ip_addr`
|
||||
"""
|
||||
|
||||
def ipinfo(ip_addr):
|
||||
location = ipcache(ip_addr)
|
||||
if location:
|
||||
return location
|
||||
if IPINFO_TOKEN:
|
||||
r = requests.get('https://ipinfo.io/%s/json?token=%s' %
|
||||
(ip_addr, IPINFO_TOKEN))
|
||||
if r.status_code == 200:
|
||||
location = r.json()["city"], r.json()["country"]
|
||||
if location:
|
||||
ipcachewrite(ip_addr, location)
|
||||
return location
|
||||
|
||||
|
||||
def geoip(ip_addr):
|
||||
try:
|
||||
response = GEOIP_READER.city(ip_addr)
|
||||
country = response.country.name
|
||||
@ -132,7 +162,34 @@ def get_location(ip_addr):
|
||||
except geoip2.errors.AddressNotFoundError:
|
||||
country = None
|
||||
city = None
|
||||
return city, country
|
||||
|
||||
def workaround(city, country):
|
||||
# workaround for the strange bug with the country name
|
||||
# maybe some other countries has this problem too
|
||||
#
|
||||
# Having these in a separate function will help if this gets to
|
||||
# be a problem
|
||||
if country == 'Russian Federation':
|
||||
country = 'Russia'
|
||||
return city, country
|
||||
|
||||
def get_location(ip_addr):
|
||||
"""
|
||||
Return location pair (CITY, COUNTRY) for `ip_addr`
|
||||
"""
|
||||
for method in IPLOCATION_ORDER:
|
||||
if method == 'geoip':
|
||||
city, country = geoip(ip_addr)
|
||||
elif method == 'ip2location':
|
||||
city, country = ip2location(ip_addr)
|
||||
elif method == 'ipinfo':
|
||||
city, country = ipinfo(ip_addr)
|
||||
else:
|
||||
print("ERROR: invalid iplocation method speficied: %s" % method)
|
||||
if city is not None:
|
||||
city, country = workaround(city, country)
|
||||
return city, country
|
||||
#
|
||||
# temporary disabled it because of geoip services capcacity
|
||||
#
|
||||
@ -143,18 +200,9 @@ def get_location(ip_addr):
|
||||
# city = location.raw.get('address', {}).get('city')
|
||||
# except:
|
||||
# pass
|
||||
if city is None:
|
||||
city, country = ip2location(ip_addr)
|
||||
|
||||
# workaround for the strange bug with the country name
|
||||
# maybe some other countries has this problem too
|
||||
if country == 'Russian Federation':
|
||||
country = 'Russia'
|
||||
|
||||
if city:
|
||||
return city, country
|
||||
else:
|
||||
return NOT_FOUND_LOCATION, None
|
||||
# No methods resulted in a location - return default
|
||||
return NOT_FOUND_LOCATION, None
|
||||
|
||||
|
||||
def location_canonical_name(location):
|
||||
@ -202,6 +250,19 @@ def is_location_blocked(location):
|
||||
return location is not None and location.lower() in LOCATION_BLACK_LIST
|
||||
|
||||
|
||||
def get_hemisphere(location):
|
||||
"""
|
||||
Return hemisphere of the location (True = North, False = South).
|
||||
Assume North and return True if location can't be found.
|
||||
"""
|
||||
location_string = location[0]
|
||||
if location[1] is not None:
|
||||
location_string += ",%s" % location[1]
|
||||
geolocation = geolocator(location_string)
|
||||
if geolocation is None:
|
||||
return True
|
||||
return geolocation["latitude"] > 0
|
||||
|
||||
def location_processing(location, ip_addr):
|
||||
"""
|
||||
"""
|
||||
@ -233,6 +294,12 @@ def location_processing(location, ip_addr):
|
||||
|
||||
query_source_location = get_location(ip_addr)
|
||||
|
||||
# For moon queries, hemisphere must be found
|
||||
# True for North, False for South
|
||||
hemisphere = False
|
||||
if location is not None and (location.lower()+"@").startswith("moon@"):
|
||||
hemisphere = get_hemisphere(query_source_location)
|
||||
|
||||
country = None
|
||||
if not location or location == 'MyLocation':
|
||||
location = ip_addr
|
||||
@ -283,4 +350,5 @@ def location_processing(location, ip_addr):
|
||||
override_location_name, \
|
||||
full_address, \
|
||||
country, \
|
||||
query_source_location
|
||||
query_source_location, \
|
||||
hemisphere
|
||||
|
462
lib/metno.py
Executable file
462
lib/metno.py
Executable file
@ -0,0 +1,462 @@
|
||||
#!/bin/env python
|
||||
# vim: fileencoding=utf-8
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import timezonefinder
|
||||
from pytz import timezone
|
||||
|
||||
from constants import WWO_CODE
|
||||
|
||||
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def metno_request(path, query_string):
|
||||
# We'll need to sanitize the inbound request - ideally the
|
||||
# premium/v1/weather.ashx portion would have always been here, though
|
||||
# it seems as though the proxy was built after the majority of the app
|
||||
# and not refactored. For WAPI we'll strip this and the API key out,
|
||||
# then manage it on our own.
|
||||
logger.debug('Original path: ' + path)
|
||||
logger.debug('Original query: ' + query_string)
|
||||
|
||||
path = path.replace('premium/v1/weather.ashx',
|
||||
'weatherapi/locationforecast/2.0/complete')
|
||||
query_string = re.sub(r'key=[^&]*&', '', query_string)
|
||||
query_string = re.sub(r'format=[^&]*&', '', query_string)
|
||||
days = int(re.search(r'num_of_days=([0-9]+)&', query_string).group(1))
|
||||
query_string = re.sub(r'num_of_days=[0-9]+&', '', query_string)
|
||||
# query_string = query_string.replace('key=', '?key=' + WAPI_KEY)
|
||||
# TP is for hourly forecasting, which isn't available in the free api.
|
||||
query_string = re.sub(r'tp=[0-9]*&', '', query_string)
|
||||
# This assumes lang=... is at the end. Also note that the API doesn't
|
||||
# localize, and we're not either. TODO: add language support
|
||||
query_string = re.sub(r'lang=[^&]*$', '', query_string)
|
||||
query_string = re.sub(r'&$', '', query_string)
|
||||
|
||||
logger.debug('qs: ' + query_string)
|
||||
# Deal with coordinates. Need to be rounded to 4 decimals for metno ToC
|
||||
# and in a different query string format
|
||||
coord_match = re.search(r'q=[^&]*', query_string)
|
||||
coords_str = coord_match.group(0)
|
||||
coords = re.findall(r'[-0-9.]+', coords_str)
|
||||
lat = str(round(float(coords[0]), 4))
|
||||
lng = str(round(float(coords[1]), 4))
|
||||
logger.debug('lat: ' + lat)
|
||||
logger.debug('lng: ' + lng)
|
||||
query_string = re.sub(r'q=[^&]*', 'lat=' + lat + '&lon=' + lng + '&',
|
||||
query_string)
|
||||
logger.debug('Return path: ' + path)
|
||||
logger.debug('Return query: ' + query_string)
|
||||
|
||||
return path, query_string, days
|
||||
|
||||
|
||||
def celsius_to_f(celsius):
|
||||
return round((1.8 * celsius) + 32, 1)
|
||||
|
||||
|
||||
def to_weather_code(symbol_code):
|
||||
logger.debug(symbol_code)
|
||||
code = re.sub(r'_.*', '', symbol_code)
|
||||
logger.debug(code)
|
||||
# symbol codes: https://api.met.no/weatherapi/weathericon/2.0/documentation
|
||||
# they also have _day, _night and _polartwilight variants
|
||||
# See json from https://api.met.no/weatherapi/weathericon/2.0/legends
|
||||
# WWO codes: https://github.com/chubin/wttr.in/blob/master/lib/constants.py
|
||||
# http://www.worldweatheronline.com/feed/wwoConditionCodes.txt
|
||||
weather_code_map = {
|
||||
"clearsky": 113,
|
||||
"cloudy": 119,
|
||||
"fair": 116,
|
||||
"fog": 143,
|
||||
"heavyrain": 302,
|
||||
"heavyrainandthunder": 389,
|
||||
"heavyrainshowers": 305,
|
||||
"heavyrainshowersandthunder": 386,
|
||||
"heavysleet": 314, # There's a ton of 'LightSleet' in WWO_CODE...
|
||||
"heavysleetandthunder": 377,
|
||||
"heavysleetshowers": 362,
|
||||
"heavysleetshowersandthunder": 374,
|
||||
"heavysnow": 230,
|
||||
"heavysnowandthunder": 392,
|
||||
"heavysnowshowers": 371,
|
||||
"heavysnowshowersandthunder": 392,
|
||||
"lightrain": 266,
|
||||
"lightrainandthunder": 200,
|
||||
"lightrainshowers": 176,
|
||||
"lightrainshowersandthunder": 386,
|
||||
"lightsleet": 281,
|
||||
"lightsleetandthunder": 377,
|
||||
"lightsleetshowers": 284,
|
||||
"lightsnow": 320,
|
||||
"lightsnowandthunder": 392,
|
||||
"lightsnowshowers": 368,
|
||||
"lightssleetshowersandthunder": 365,
|
||||
"lightssnowshowersandthunder": 392,
|
||||
"partlycloudy": 116,
|
||||
"rain": 293,
|
||||
"rainandthunder": 389,
|
||||
"rainshowers": 299,
|
||||
"rainshowersandthunder": 386,
|
||||
"sleet": 185,
|
||||
"sleetandthunder": 392,
|
||||
"sleetshowers": 263,
|
||||
"sleetshowersandthunder": 392,
|
||||
"snow": 329,
|
||||
"snowandthunder": 392,
|
||||
"snowshowers": 230,
|
||||
"snowshowersandthunder": 392,
|
||||
}
|
||||
if code not in weather_code_map:
|
||||
logger.debug('not found')
|
||||
return -1 # not found
|
||||
logger.debug(weather_code_map[code])
|
||||
return weather_code_map[code]
|
||||
|
||||
|
||||
def to_description(symbol_code):
|
||||
desc = WWO_CODE[str(to_weather_code(symbol_code))]
|
||||
logger.debug(desc)
|
||||
return desc
|
||||
|
||||
|
||||
def to_16_point(degrees):
|
||||
# 360 degrees / 16 = 22.5 degrees of arc or 11.25 degrees around the point
|
||||
if degrees > (360 - 11.25) or degrees <= 11.25:
|
||||
return 'N'
|
||||
if degrees > 11.25 and degrees <= (11.25 + 22.5):
|
||||
return 'NNE'
|
||||
if degrees > (11.25 + (22.5 * 1)) and degrees <= (11.25 + (22.5 * 2)):
|
||||
return 'NE'
|
||||
if degrees > (11.25 + (22.5 * 2)) and degrees <= (11.25 + (22.5 * 3)):
|
||||
return 'ENE'
|
||||
if degrees > (11.25 + (22.5 * 3)) and degrees <= (11.25 + (22.5 * 4)):
|
||||
return 'E'
|
||||
if degrees > (11.25 + (22.5 * 4)) and degrees <= (11.25 + (22.5 * 5)):
|
||||
return 'ESE'
|
||||
if degrees > (11.25 + (22.5 * 5)) and degrees <= (11.25 + (22.5 * 6)):
|
||||
return 'SE'
|
||||
if degrees > (11.25 + (22.5 * 6)) and degrees <= (11.25 + (22.5 * 7)):
|
||||
return 'SSE'
|
||||
if degrees > (11.25 + (22.5 * 7)) and degrees <= (11.25 + (22.5 * 8)):
|
||||
return 'S'
|
||||
if degrees > (11.25 + (22.5 * 8)) and degrees <= (11.25 + (22.5 * 9)):
|
||||
return 'SSW'
|
||||
if degrees > (11.25 + (22.5 * 9)) and degrees <= (11.25 + (22.5 * 10)):
|
||||
return 'SW'
|
||||
if degrees > (11.25 + (22.5 * 10)) and degrees <= (11.25 + (22.5 * 11)):
|
||||
return 'WSW'
|
||||
if degrees > (11.25 + (22.5 * 11)) and degrees <= (11.25 + (22.5 * 12)):
|
||||
return 'W'
|
||||
if degrees > (11.25 + (22.5 * 12)) and degrees <= (11.25 + (22.5 * 13)):
|
||||
return 'WNW'
|
||||
if degrees > (11.25 + (22.5 * 13)) and degrees <= (11.25 + (22.5 * 14)):
|
||||
return 'NW'
|
||||
if degrees > (11.25 + (22.5 * 14)) and degrees <= (11.25 + (22.5 * 15)):
|
||||
return 'NNW'
|
||||
|
||||
|
||||
def meters_to_miles(meters):
|
||||
return round(meters * 0.00062137, 2)
|
||||
|
||||
|
||||
def mm_to_inches(mm):
|
||||
return round(mm / 25.4, 2)
|
||||
|
||||
|
||||
def hpa_to_mb(hpa):
|
||||
return hpa
|
||||
|
||||
|
||||
def hpa_to_in(hpa):
|
||||
return round(hpa * 0.02953, 2)
|
||||
|
||||
|
||||
def group_hours_to_days(lat, lng, hourlies, days_to_return):
|
||||
tf = timezonefinder.TimezoneFinder()
|
||||
timezone_str = tf.certain_timezone_at(lat=lat, lng=lng)
|
||||
logger.debug('got TZ: ' + timezone_str)
|
||||
tz = timezone(timezone_str)
|
||||
start_day_gmt = datetime.fromisoformat(hourlies[0]['time']
|
||||
.replace('Z', '+00:00'))
|
||||
start_day_local = start_day_gmt.astimezone(tz)
|
||||
end_day_local = (start_day_local + timedelta(days=days_to_return - 1)).date()
|
||||
logger.debug('series starts at gmt time: ' + str(start_day_gmt))
|
||||
logger.debug('series starts at local time: ' + str(start_day_local))
|
||||
logger.debug('series ends on day: ' + str(end_day_local))
|
||||
days = {}
|
||||
|
||||
for hour in hourlies:
|
||||
current_day_gmt = datetime.fromisoformat(hour['time']
|
||||
.replace('Z', '+00:00'))
|
||||
current_local = current_day_gmt.astimezone(tz)
|
||||
current_day_local = current_local.date()
|
||||
if current_day_local > end_day_local:
|
||||
continue
|
||||
if current_day_local not in days:
|
||||
days[current_day_local] = {'hourly': []}
|
||||
hour['localtime'] = current_local.time()
|
||||
days[current_day_local]['hourly'].append(hour)
|
||||
|
||||
# Need a second pass to build the min/max/avg data
|
||||
for date, day in days.items():
|
||||
minTempC = -999
|
||||
maxTempC = 1000
|
||||
avgTempC = None
|
||||
n = 0
|
||||
maxUvIndex = 0
|
||||
for hour in day['hourly']:
|
||||
temp = hour['data']['instant']['details']['air_temperature']
|
||||
if temp > minTempC:
|
||||
minTempC = temp
|
||||
if temp < maxTempC:
|
||||
maxTempC = temp
|
||||
if avgTempC is None:
|
||||
avgTempC = temp
|
||||
n = 1
|
||||
else:
|
||||
avgTempC = ((avgTempC * n) + temp) / (n + 1)
|
||||
n = n + 1
|
||||
|
||||
uv = hour['data']['instant']['details']
|
||||
if 'ultraviolet_index_clear_sky' in uv:
|
||||
if uv['ultraviolet_index_clear_sky'] > maxUvIndex:
|
||||
maxUvIndex = uv['ultraviolet_index_clear_sky']
|
||||
day["maxtempC"] = str(maxTempC)
|
||||
day["maxtempF"] = str(celsius_to_f(maxTempC))
|
||||
day["mintempC"] = str(minTempC)
|
||||
day["mintempF"] = str(celsius_to_f(minTempC))
|
||||
day["avgtempC"] = str(round(avgTempC, 1))
|
||||
day["avgtempF"] = str(celsius_to_f(avgTempC))
|
||||
# day["totalSnow_cm": "not implemented",
|
||||
# day["sunHour": "12", # This would come from astonomy data
|
||||
day["uvIndex"] = str(maxUvIndex)
|
||||
|
||||
return days
|
||||
|
||||
def _convert_hour(hour):
|
||||
# Whatever is upstream is expecting data in the shape of WWO. This method will
|
||||
# morph from metno to hourly WWO response format.
|
||||
# Note that WWO is providing data every 3 hours. Metno provides every hour
|
||||
# {
|
||||
# "time": "0",
|
||||
# "tempC": "19",
|
||||
# "tempF": "66",
|
||||
# "windspeedMiles": "6",
|
||||
# "windspeedKmph": "9",
|
||||
# "winddirDegree": "276",
|
||||
# "winddir16Point": "W",
|
||||
# "weatherCode": "119",
|
||||
# "weatherIconUrl": [
|
||||
# {
|
||||
# "value": "http://cdn.worldweatheronline.com/images/wsymbols01_png_64/wsymbol_0003_white_cloud.png"
|
||||
# }
|
||||
# ],
|
||||
# "weatherDesc": [
|
||||
# {
|
||||
# "value": "Cloudy"
|
||||
# }
|
||||
# ],
|
||||
# "precipMM": "0.0",
|
||||
# "precipInches": "0.0",
|
||||
# "humidity": "62",
|
||||
# "visibility": "10",
|
||||
# "visibilityMiles": "6",
|
||||
# "pressure": "1017",
|
||||
# "pressureInches": "31",
|
||||
# "cloudcover": "66",
|
||||
# "HeatIndexC": "19",
|
||||
# "HeatIndexF": "66",
|
||||
# "DewPointC": "12",
|
||||
# "DewPointF": "53",
|
||||
# "WindChillC": "19",
|
||||
# "WindChillF": "66",
|
||||
# "WindGustMiles": "8",
|
||||
# "WindGustKmph": "13",
|
||||
# "FeelsLikeC": "19",
|
||||
# "FeelsLikeF": "66",
|
||||
# "chanceofrain": "0",
|
||||
# "chanceofremdry": "93",
|
||||
# "chanceofwindy": "0",
|
||||
# "chanceofovercast": "89",
|
||||
# "chanceofsunshine": "18",
|
||||
# "chanceoffrost": "0",
|
||||
# "chanceofhightemp": "0",
|
||||
# "chanceoffog": "0",
|
||||
# "chanceofsnow": "0",
|
||||
# "chanceofthunder": "0",
|
||||
# "uvIndex": "1"
|
||||
details = hour['data']['instant']['details']
|
||||
if 'next_1_hours' in hour['data']:
|
||||
next_hour = hour['data']['next_1_hours']
|
||||
elif 'next_6_hours' in hour['data']:
|
||||
next_hour = hour['data']['next_6_hours']
|
||||
elif 'next_12_hours' in hour['data']:
|
||||
next_hour = hour['data']['next_12_hours']
|
||||
else:
|
||||
next_hour = {}
|
||||
|
||||
# Need to dig out symbol_code and precipitation_amount
|
||||
symbol_code = 'clearsky_day' # Default to sunny
|
||||
if 'summary' in next_hour and 'symbol_code' in next_hour['summary']:
|
||||
symbol_code = next_hour['summary']['symbol_code']
|
||||
precipitation_amount = 0 # Default to no rain
|
||||
if 'details' in next_hour and 'precipitation_amount' in next_hour['details']:
|
||||
precipitation_amount = next_hour['details']['precipitation_amount']
|
||||
|
||||
uvIndex = 0 # default to 0 index
|
||||
if 'ultraviolet_index_clear_sky' in details:
|
||||
uvIndex = details['ultraviolet_index_clear_sky']
|
||||
localtime = ''
|
||||
if 'localtime' in hour:
|
||||
localtime = "{h:02.0f}".format(h=hour['localtime'].hour) + \
|
||||
"{m:02.0f}".format(m=hour['localtime'].minute)
|
||||
logger.debug(str(hour['localtime']))
|
||||
# time property is local time, 4 digit 24 hour, with no :, e.g. 2100
|
||||
return {
|
||||
'time': localtime,
|
||||
'observation_time': hour['time'], # Need to figure out WWO TZ
|
||||
# temp_C is used in we-lang.go calcs in such a way
|
||||
# as to expect a whole number
|
||||
'temp_C': str(int(round(details['air_temperature'], 0))),
|
||||
# temp_F can be more precise - not used in we-lang.go calcs
|
||||
'temp_F': str(celsius_to_f(details['air_temperature'])),
|
||||
'weatherCode': str(to_weather_code(symbol_code)),
|
||||
'weatherIconUrl': [{
|
||||
'value': 'not yet implemented',
|
||||
}],
|
||||
'weatherDesc': [{
|
||||
'value': to_description(symbol_code),
|
||||
}],
|
||||
# similiarly, windspeedMiles is not used by we-lang.go, but kmph is
|
||||
"windspeedMiles": str(meters_to_miles(details['wind_speed'])),
|
||||
"windspeedKmph": str(int(round(details['wind_speed'], 0))),
|
||||
"winddirDegree": str(details['wind_from_direction']),
|
||||
"winddir16Point": to_16_point(details['wind_from_direction']),
|
||||
"precipMM": str(precipitation_amount),
|
||||
"precipInches": str(mm_to_inches(precipitation_amount)),
|
||||
"humidity": str(details['relative_humidity']),
|
||||
"visibility": 'not yet implemented', # str(details['vis_km']),
|
||||
"visibilityMiles": 'not yet implemented', # str(details['vis_miles']),
|
||||
"pressure": str(hpa_to_mb(details['air_pressure_at_sea_level'])),
|
||||
"pressureInches": str(hpa_to_in(details['air_pressure_at_sea_level'])),
|
||||
"cloudcover": 'not yet implemented', # Convert from cloud_area_fraction?? str(details['cloud']),
|
||||
# metno doesn't have FeelsLikeC, but we-lang.go is using it in calcs,
|
||||
# so we shall set it to temp_C
|
||||
"FeelsLikeC": str(int(round(details['air_temperature'], 0))),
|
||||
"FeelsLikeF": 'not yet implemented', # str(details['feelslike_f']),
|
||||
"uvIndex": str(uvIndex),
|
||||
}
|
||||
|
||||
|
||||
def _convert_hourly(hours):
|
||||
converted_hours = []
|
||||
for hour in hours:
|
||||
converted_hours.append(_convert_hour(hour))
|
||||
return converted_hours
|
||||
|
||||
|
||||
# Whatever is upstream is expecting data in the shape of WWO. This method will
|
||||
# morph from metno to WWO response format.
|
||||
def create_standard_json_from_metno(content, days_to_return):
|
||||
try:
|
||||
forecast = json.loads(content) # pylint: disable=invalid-name
|
||||
except (ValueError, TypeError) as exception:
|
||||
logger.error("---")
|
||||
logger.error(exception)
|
||||
logger.error("---")
|
||||
return {}, ''
|
||||
hourlies = forecast['properties']['timeseries']
|
||||
current = hourlies[0]
|
||||
# We are assuming these units:
|
||||
# "units": {
|
||||
# "air_pressure_at_sea_level": "hPa",
|
||||
# "air_temperature": "celsius",
|
||||
# "air_temperature_max": "celsius",
|
||||
# "air_temperature_min": "celsius",
|
||||
# "cloud_area_fraction": "%",
|
||||
# "cloud_area_fraction_high": "%",
|
||||
# "cloud_area_fraction_low": "%",
|
||||
# "cloud_area_fraction_medium": "%",
|
||||
# "dew_point_temperature": "celsius",
|
||||
# "fog_area_fraction": "%",
|
||||
# "precipitation_amount": "mm",
|
||||
# "relative_humidity": "%",
|
||||
# "ultraviolet_index_clear_sky": "1",
|
||||
# "wind_from_direction": "degrees",
|
||||
# "wind_speed": "m/s"
|
||||
# }
|
||||
content = {
|
||||
'data': {
|
||||
'request': [{
|
||||
'type': 'feature',
|
||||
'query': str(forecast['geometry']['coordinates'][1]) + ',' +
|
||||
str(forecast['geometry']['coordinates'][0])
|
||||
}],
|
||||
'current_condition': [
|
||||
_convert_hour(current)
|
||||
],
|
||||
'weather': []
|
||||
}
|
||||
}
|
||||
|
||||
days = group_hours_to_days(forecast['geometry']['coordinates'][1],
|
||||
forecast['geometry']['coordinates'][0],
|
||||
hourlies, days_to_return)
|
||||
|
||||
# TODO: Astronomy needs to come from this:
|
||||
# https://api.met.no/weatherapi/sunrise/2.0/.json?lat=40.7127&lon=-74.0059&date=2020-10-07&offset=-05:00
|
||||
# and obviously can be cached for a while
|
||||
# https://api.met.no/weatherapi/sunrise/2.0/documentation
|
||||
# Note that full moon/new moon/first quarter/last quarter aren't returned
|
||||
# and the moonphase value should match these from WWO:
|
||||
# New Moon
|
||||
# Waxing Crescent
|
||||
# First Quarter
|
||||
# Waxing Gibbous
|
||||
# Full Moon
|
||||
# Waning Gibbous
|
||||
# Last Quarter
|
||||
# Waning Crescent
|
||||
|
||||
for date, day in days.items():
|
||||
content['data']['weather'].append({
|
||||
"date": str(date),
|
||||
"astronomy": [],
|
||||
"maxtempC": day['maxtempC'],
|
||||
"maxtempF": day['maxtempF'],
|
||||
"mintempC": day['mintempC'],
|
||||
"mintempF": day['mintempF'],
|
||||
"avgtempC": day['avgtempC'],
|
||||
"avgtempF": day['avgtempF'],
|
||||
"totalSnow_cm": "not implemented",
|
||||
"sunHour": "12", # This would come from astonomy data
|
||||
"uvIndex": day['uvIndex'],
|
||||
'hourly': _convert_hourly(day['hourly']),
|
||||
})
|
||||
|
||||
# for day in forecast.
|
||||
return json.dumps(content)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# if len(sys.argv) == 1:
|
||||
# for deg in range(0, 360):
|
||||
# print('deg: ' + str(deg) + '; 16point: ' + to_16_point(deg))
|
||||
if len(sys.argv) == 2:
|
||||
req = sys.argv[1].split('?')
|
||||
# to_description(sys.argv[1])
|
||||
metno_request(req[0], req[1])
|
||||
elif len(sys.argv) == 3:
|
||||
with open(sys.argv[1], 'r') as contentf:
|
||||
content = create_standard_json_from_metno(contentf.read(),
|
||||
int(sys.argv[2]))
|
||||
print(content)
|
||||
else:
|
||||
print('usage: metno <content file> <days>')
|
@ -6,8 +6,8 @@ Translation of almost everything.
|
||||
|
||||
FULL_TRANSLATION = [
|
||||
"ar", "af", "be", "ca", "da", "de", "el", "et",
|
||||
"fr", "fa", "hu", "ia", "id", "it",
|
||||
"nb", "nl", "pl", "pt-br", "ro",
|
||||
"fr", "fa", "hi", "hu", "ia", "id", "it",
|
||||
"nb", "nl", "oc", "pl", "pt-br", "ro",
|
||||
"ru", "tr", "th", "uk", "vi", "zh-cn", "zh-tw"
|
||||
]
|
||||
|
||||
@ -23,10 +23,10 @@ PARTIAL_TRANSLATION = [
|
||||
|
||||
PROXY_LANGS = [
|
||||
"af", "ar", "az", "be", "bs", "ca",
|
||||
"cy", "de", "el", "eo", "et", "fa", "fr",
|
||||
"fy", "he", "hr", "hu", "hy",
|
||||
"cy", "de", "el", "eo", "et", "eu", "fa", "fr",
|
||||
"fy", "ga", "he", "hr", "hu", "hy",
|
||||
"ia", "id", "is", "it", "ja", "kk",
|
||||
"lv", "mk", "nb", "nn", "ro",
|
||||
"lv", "mk", "nb", "nn", "oc", "ro",
|
||||
"ru", "sl", "th", "pt-br", "uk", "uz",
|
||||
"vi", "zh-cn", "zh-tw",
|
||||
]
|
||||
@ -120,6 +120,16 @@ Nous n'avons pas pu déterminer votre position,
|
||||
Nous vous avons donc amenés à Oïmiakon,
|
||||
l'un des endroits les plus froids habités en permanence sur la planète.
|
||||
Nous espérons qu'il fait meilleur chez vous !
|
||||
""",
|
||||
'ga': u"""
|
||||
Ní rabhamar ábalta do cheantar a aimsiú
|
||||
mar sin thugamar go dtí Oymyakon,
|
||||
tú ceann do na ceantair bhuanáitrithe is fuaire ar domhan.
|
||||
""",
|
||||
'hi': u"""
|
||||
हम आपका स्थान खोजने में असमर्थ है,
|
||||
इसलिए हम आपको ओयमयाकोन पर ले आए है,
|
||||
जो ग्रह के सबसे ठंडे स्थानों में से एक है|
|
||||
""",
|
||||
'hu': u"""
|
||||
Nem sikerült megtalálni a pozíciódat,
|
||||
@ -186,6 +196,12 @@ dus hebben we u naar Ojmjakon gebracht,
|
||||
Wy koenen jo lokaasje net fêststelle
|
||||
dus wy ha jo nei Ojmjakon brocht,
|
||||
ien fan de kâldste permanent bewenbere plakken op ierde.
|
||||
""",
|
||||
'oc': u"""
|
||||
Avèm pas pogut determinar vòstra posicion,
|
||||
Vos avèm doncas menat a Oïmiakon,
|
||||
un dels endreches mai freds abitat permanéncia del monde.
|
||||
Esperam que fa melhor en çò vòstre !
|
||||
""",
|
||||
'pt': u"""
|
||||
Não conseguimos encontrar a sua localização,
|
||||
@ -256,7 +272,7 @@ shuning uchun sizga sayyoramizning eng sovuq aholi punkti - Oymyakondagi ob-havo
|
||||
Umid qilamizki, sizda bugungi ob-havo bundan yaxshiroq!
|
||||
""",
|
||||
'zh': u"""
|
||||
我们无法找到您的位置,
|
||||
我们无法找到您的位置,
|
||||
当前显示奥伊米亚康(Oymyakon),这个星球上最冷的人类定居点。
|
||||
""",
|
||||
'da': u"""
|
||||
@ -296,9 +312,12 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
|
||||
'el': u'Άνγωστη τοποθεσία',
|
||||
'es': u'Ubicación desconocida',
|
||||
'et': u'Tundmatu asukoht',
|
||||
'eu': u'Kokapen ezezaguna',
|
||||
'fa': u'مکان نامعلوم',
|
||||
'fi': u'Tuntematon sijainti',
|
||||
'fr': u'Emplacement inconnu',
|
||||
'ga': u'Ceantar anaithnid',
|
||||
'hi': u'अज्ञात स्थान',
|
||||
'hu': u'Ismeretlen lokáció',
|
||||
'hy': u'Անհայտ գտնվելու վայր',
|
||||
'id': u'Lokasi tidak diketahui',
|
||||
@ -312,6 +331,7 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
|
||||
'mk': u'Непозната локација',
|
||||
'nb': u'Ukjent sted',
|
||||
'nl': u'Onbekende locatie',
|
||||
'oc': u'Emplaçament desconegut',
|
||||
'fy': u'Ûnbekende lokaasje',
|
||||
'pl': u'Nieznana lokalizacja',
|
||||
'pt': u'Localização desconhecida',
|
||||
@ -347,9 +367,12 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
|
||||
'el': u'Τοποθεσία',
|
||||
'es': u'Ubicación',
|
||||
'et': u'Asukoht',
|
||||
'eu': u'Kokaena',
|
||||
'fa': u'مکان',
|
||||
'fi': u'Tuntematon sijainti',
|
||||
'fr': u'Emplacement',
|
||||
'ga': u'Ceantar',
|
||||
'hi': u'स्थान',
|
||||
'hu': u'Lokáció',
|
||||
'hy': u'Դիրք',
|
||||
'ia': u'Location',
|
||||
@ -363,6 +386,7 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
|
||||
'mk': u'Локација',
|
||||
'nb': u'Sted',
|
||||
'nl': u'Locatie',
|
||||
'oc': u'Emplaçament',
|
||||
'fy': u'Lokaasje',
|
||||
'pl': u'Lokalizacja',
|
||||
'pt': u'Localização',
|
||||
@ -385,7 +409,7 @@ một trong những nơi lạnh nhất có người sinh sống trên trái đ
|
||||
'CAPACITY_LIMIT_REACHED': {
|
||||
'en': u"""
|
||||
Sorry, we are running out of queries to the weather service at the moment.
|
||||
Here is the weather report for the default city (just to show you, how it looks like).
|
||||
Here is the weather report for the default city (just to show you what it looks like).
|
||||
We will get new queries as soon as possible.
|
||||
You can follow https://twitter.com/igor_chubin for the updates.
|
||||
======================================================================================
|
||||
@ -465,6 +489,20 @@ Voici un bulletin météo de l'emplacement par défaut (pour vous donner un aper
|
||||
Nous serons très bientôt en mesure de faire de nouvelles requêtes.
|
||||
Vous pouvez suivre https://twitter.com/igor_chubin pour rester informé.
|
||||
======================================================================================
|
||||
""",
|
||||
'ga': u"""
|
||||
Tá brón orainn, níl mórán iarratas le fail chuig seirbhís na haimsire faoi láthair.
|
||||
Seo duit réamhaisnéis na haimsire don chathair réamhshocraithe (chun é a thaispeaint duit).
|
||||
Gheobhaimid iarratais nua chomh luath agus is feidir.
|
||||
Lean orainn ar https://twitter.com/igor_chubin don eolas is déanaí.
|
||||
======================================================================================
|
||||
""",
|
||||
'hi': u"""
|
||||
क्षमा करें, इस समय हम मौसम सेवा से संपर्क नहीं कर पा रहे है।
|
||||
यहा पूर्व निर्धारित शहर के लिए मौसम की जानकारी (आपको यह दिखाने के लिए कि यह कैसा दिखता है)।
|
||||
हम जल्द से जल्द मौसम सेवा से संपर्क करने नई कोशिश करेंगे।
|
||||
आप अपडेट के लिए https://twitter.com/igor_chubin का अनुसरण कर सकते हैं।
|
||||
======================================================================================
|
||||
""",
|
||||
'hu': u"""
|
||||
Sajnáljuk, kifogytunk az időjárási szolgáltatásra fordított erőforrásokból.
|
||||
@ -542,6 +580,13 @@ Hier is het weerbericht voor de standaard stad(zodat u weet hoe het er uitziet)
|
||||
Wij lossen dit probleem zo snel mogelijk op.
|
||||
voor updates kunt u ons op https://twitter.com/igor_chubin volgen.
|
||||
======================================================================================
|
||||
""",
|
||||
'oc': u"""
|
||||
O planhèm, avèm pas mai de requèstas cap al servici metèo.
|
||||
Vaquí las prevision metèo de l'emplaçament per defaut (per vos donar un apercebut).
|
||||
Poirem lèu ne far de novèlas.
|
||||
Podètz seguir https://twitter.com/igor_chubin per demorar informat.
|
||||
======================================================================================
|
||||
""",
|
||||
'fy': u"""
|
||||
Excuses, wy kinne op dit moment 't waarberjocht net sjin litte.
|
||||
@ -657,6 +702,7 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
|
||||
'fy': u'Nije funksje: twatalige lokaasje nammen \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) en lokaasje sykjen \033[92mwttr.in/~Kilimanjaro\033[0m (set er gewoan in ~ foar)',
|
||||
'cy': u'Nodwedd newydd: enwau lleoliadau amlieithog \033[92mwttr.in/станция+Восток\033[0m (yn UTF-8) a chwilio am leoliad \033[92mwttr.in/~Kilimanjaro\033[0m (ychwanegwch ~ yn gyntaf)',
|
||||
'de': u'Neue Funktion: mehrsprachige Ortsnamen \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) und Ortssuche \033[92mwttr.in/~Kilimanjaro\033[0m (fügen Sie ein ~ vor dem Ort ein)',
|
||||
'hi': u'नई सुविधा: बहुभाषी स्थान के नाम \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) और स्थान खोज \033[92mwttr.in/~Kilimanjaro\033[0m (बस ~ आगे लगाये)',
|
||||
'hu': u'Új funkcinalitás: többnyelvű helynevek \033[92mwttr.in/станция+Восток\033[0m (UTF-8-ban) és pozíció keresés \033[92mwttr.in/~Kilimanjaro\033[0m (csak adj egy ~ jelet elé)',
|
||||
'hy': u'Փորձարկեք: տեղամասերի անունները կամայական լեզվով \033[92mwttr.in/Դիլիջան\033[0m (в UTF-8) և տեղանքի որոնում \033[92mwttr.in/~Kilimanjaro\033[0m (հարկավոր է ~ ավելացնել դիմացից)',
|
||||
'ia': u'Nove functione: location nomine multilingue \033[92mwttr.in/станция+Восток\033[0m (a UTF-8) e recerca de location\033[92mwttr.in/~Kilimanjaro\033[0m (solo adde ~ ante)',
|
||||
@ -666,6 +712,7 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
|
||||
'kk': u'',
|
||||
'lv': u'Jaunums: Daudzvalodu atrašanās vietu nosaukumi \033[92mwttr.in/станция+Восток\033[0m (in UTF-8) un dabas objektu meklēšana \033[92mwttr.in/~Kilimanjaro\033[0m (tikai priekšā pievieno ~)',
|
||||
'mk': u'Нова функција: повеќе јазично локациски имиња \033[92mwttr.in/станция+Восток\033[0m (во UTF-8) и локациско пребарување \033[92mwttr.in/~Kilimanjaro\033[0m (just add ~ before)',
|
||||
'oc': u'Novèla foncionalitat : nom de lòc multilenga \033[92mwttr.in/станция+Восток\033[0m (en UTF-8) e recèrca de lòc \033[92mwttr.in/~Kilimanjaro\033[0m (solament ajustatz ~ abans)',
|
||||
'pl': u'Nowa funkcjonalność: wielojęzyczne nazwy lokalizacji \033[92mwttr.in/станция+Восток\033[0m (w UTF-8) i szukanie lokalizacji \033[92mwttr.in/~Kilimanjaro\033[0m (poprzedź zapytanie ~ - znakiem tyldy)',
|
||||
'pt': u'Nova funcionalidade: nomes de localidades em várias línguas \033[92mwttr.in/станция+Восток\033[0m (em UTF-8) e procura por localidades \033[92mwttr.in/~Kilimanjaro\033[0m (é só colocar ~ antes)',
|
||||
'pt-br': u'Nova funcionalidade: nomes de localidades em várias línguas \033[92mwttr.in/станция+Восток\033[0m (em UTF-8) e procura por localidades \033[92mwttr.in/~Kilimanjaro\033[0m (é só colocar ~ antes)',
|
||||
@ -691,10 +738,13 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
|
||||
'bs': u'XXXXXX \033[46m\033[30m@igor_chubin\033[0m XXXXXXXXXXXXXXXXXXX',
|
||||
'ca': u'Segueix \033[46m\033[30m@igor_chubin\033[0m per actualitzacions de wttr.in',
|
||||
'es': u'Sigue a \033[46m\033[30m@igor_chubin\033[0m para enterarte de las novedades de wttr.in',
|
||||
'eu': u'\033[46m\033[30m@igor_chubin\033[0m jarraitu wttr.in berriak jasotzeko',
|
||||
'cy': u'Dilyner \033[46m\033[30m@igor_Chubin\033[0m am diweddariadau wttr.in',
|
||||
'fa': u'برای دنبال کردن خبرهای wttr.in شناسه \033[46m\033[30m@igor_chubin\033[0m رو فالو کنید.',
|
||||
'fr': u'Suivez \033[46m\033[30m@igor_Chubin\033[0m pour rester informé sur wttr.in',
|
||||
'de': u'Folgen Sie \033[46m\033[30mhttps://twitter.com/igor_chubin\033[0m für wttr.in Updates',
|
||||
'ga': u'Lean \033[46m\033[30m@igor_chubin\033[0m don wttr.in eolas is deanaí',
|
||||
'hi': u'अपडेट के लिए फॉलो करें \033[46m\033[30m@igor_chubin\033[0m',
|
||||
'hu': u'Kövesd \033[46m\033[30m@igor_chubin\033[0m-t további wttr.in információkért',
|
||||
'hy': u'Նոր ֆիչռների համար հետևեք՝ \033[46m\033[30m@igor_chubin\033[0m',
|
||||
'ia': u'Seque \033[46m\033[30m@igor_chubin\033[0m por nove information de wttr.in',
|
||||
@ -706,6 +756,7 @@ Bạn có thể theo dõi https://twitter.com/igor_chubin để cập nhật th
|
||||
'mk': u'Следете \033[46m\033[30m@igor_chubin\033[0m за wttr.in новости',
|
||||
'nb': u'Følg \033[46m\033[30m@igor_chubin\033[0m for wttr.in oppdateringer',
|
||||
'nl': u'Volg \033[46m\033[30m@igor_chubin\033[0m voor wttr.in updates',
|
||||
'oc': u'Seguissètz \033[46m\033[30m@igor_Chubin\033[0m per demorar informat sus wttr.in',
|
||||
'fy': u'Folgje \033[46m\033[30m@igor_chubin\033[0m foar wttr.in updates',
|
||||
'pl': u'Śledź \033[46m\033[30m@igor_chubin\033[0m aby być na bieżąco z nowościami dotyczącymi wttr.in',
|
||||
'pt': u'Seguir \033[46m\033[30m@igor_chubin\033[0m para as novidades de wttr.in',
|
||||
@ -740,12 +791,14 @@ CAPTION = {
|
||||
"eo": u"Veterprognozo por:",
|
||||
"es": u"Pronóstico del tiempo en:",
|
||||
"et": u"Ilmaprognoos:",
|
||||
"eu": u"Eguraldiaren iragarpena:",
|
||||
"fa": u"گزارش آب و هئا برای شما:",
|
||||
"fi": u"Säätiedotus:",
|
||||
"fr": u"Prévisions météo pour:",
|
||||
"fy": u"Waarberjocht foar:",
|
||||
"ga": u"Réamhaisnéis na haimsire do:",
|
||||
"he": u":ריוואה גזמ תיזחת",
|
||||
"hi": u"मौसम की जानकारी",
|
||||
"hr": u"Vremenska prognoza za:",
|
||||
"hu": u"Időjárás előrejelzés:",
|
||||
"hy": u"Եղանակի տեսություն:",
|
||||
@ -766,6 +819,7 @@ CAPTION = {
|
||||
"nb": u"Værmelding for:",
|
||||
"nl": u"Weerbericht voor:",
|
||||
"nn": u"Vêrmelding for:",
|
||||
"oc": u"Previsions metèo per :",
|
||||
"pl": u"Pogoda w:",
|
||||
"pt": u"Previsão do tempo para:",
|
||||
"pt-br": u"Previsão do tempo para:",
|
||||
|
@ -22,24 +22,26 @@ V2_TRANSLATION = {
|
||||
"eo": ("Veterprognozo por:", "Vetero", "Horzono", "Nun", "Tagiĝo", "Sunleviĝo", "Zenito", "Sunsubiro", "Krepusko"),
|
||||
"es": ("El tiempo en:", "Clima", "Zona horaria", "Ahora", "Alborada", "Amanecer", "Cenit", "Atardecer", "Anochecer"),
|
||||
"et": ("Ilmaprognoos:", "Ilm", "Ajatsoon", "Hetkel", "Koit", "Päikesetõus", "Seniit", "Päikeseloojang", "Eha"),
|
||||
"eu": ("Eguraldia:", "Ordu-eremua", "Orain", "Egunsentia", "Eguzkia", "Zenit", "Ilunabarra", "Ilunabarra"),
|
||||
"fa": ("اوه و بآ تیعضو شرازگ", "", "", "", "", "", "", "", ""),
|
||||
"fi": ("Säätiedotus:", "", "", "", "", "", "", "", ""),
|
||||
"fr": ("Prévisions météo pour :", "Météo", "Fuseau Horaire", "Heure", "Aube", "Lever du Soleil", "Zénith", "Coucher du Soleil", "Crépuscule"),
|
||||
"fy": ("Waarberjocht foar:", "", "", "", "", "", "", "", ""),
|
||||
"ga": ("Réamhaisnéis na haimsire do:", "", "", "", "", "", "", "", ""),
|
||||
"he": (":ריוואה גזמ תיזחת", "", "", "", "", "", "", "", ""),
|
||||
"hi": ("मौसम की जानकारी", "मौसम", "समय मण्डल", "अभी", "उदय", "सूर्योदय", "चरम बिन्दु", "सूर्यास्त", "संध्याकाल"),
|
||||
"hr": ("Vremenska prognoza za:", "", "", "", "", "", "", "", ""),
|
||||
"hu": ("Időjárás előrejelzés:", "", "", "", "", "", "", "", ""),
|
||||
"hu": ("Időjárás előrejelzés:", "Időjárás", "időzóna", "aktuális", "hajnal", "napkelte", "dél", "naplemente", "szürkület"),
|
||||
"hy": ("Եղանակի տեսություն:", "", "", "", "", "", "", "", ""),
|
||||
"ia": ("Reporto tempore pro:", "Tempore", "Fuso Horari", "Alora", "Alba", "Aurora", "Zenit", "Poner del sol", "Crepusculo"),
|
||||
"id": ("Prakiraan cuaca:", "", "", "", "", "", "", "", ""),
|
||||
"it": ("Previsioni meteo:", "", "", "", "", "", "", "", ""),
|
||||
"it": ("Previsioni meteo:", "Tempo", "Fuso orario", "Ora", "Alba", "Sorgere del Sole", "Zenit", "Tramonto", "Crepuscolo"),
|
||||
"is": ("Veðurskýrsla fyrir:", "", "", "", "", "", "", "", ""),
|
||||
"ja": ("天気予報:", "天気", "タイムゾーン", "今", "夜明け", "日の出", "天頂", "日の入", "日暮れ"),
|
||||
"jv": ("Weather forecast for:", "", "", "", "", "", "", "", ""),
|
||||
"ka": ("ამინდის პროგნოზი:", "", "", "", "", "", "", "", ""),
|
||||
"kk": ("Ауа райы:", "", "", "", "", "", "", "", ""),
|
||||
"ko": ("일기 예보:", "", "", "", "", "", "", "", ""),
|
||||
"ko": ("일기 예보:", "날씨", "시간대", "현재", "새벽", "일출", "정오", "일몰", "황혼"),
|
||||
"ky": ("Аба ырайы:", "", "", "", "", "", "", "", ""),
|
||||
"lt": ("Orų prognozė:", "", "", "", "", "", "", "", ""),
|
||||
"lv": ("Laika ziņas:", "", "", "", "", "", "", "", ""),
|
||||
@ -48,6 +50,7 @@ V2_TRANSLATION = {
|
||||
"nb": ("Værmelding for:", "", "", "", "", "", "", "", ""),
|
||||
"nl": ("Weerbericht voor:", "Weer", "Tijdzone", "Nu", "Dageraad", "Zonsopkomst", "Zenit", "Zonsondergang", "Schemering"),
|
||||
"nn": ("Vêrmelding for:", "", "", "", "", "", "", "", ""),
|
||||
"oc": ("Previsions metèo per:" "Metèo", "Zòna orària", "Ara", "Auròra", "Alba", "Zenit", "A solelh colc", "Entrelutz"),
|
||||
"pl": ("Pogoda w:", "", "", "", "", "", "", "", ""),
|
||||
"pt": ("Previsão do tempo para:", "Tempo", "Fuso horário", "Agora", "Alvorada", "Nascer do sol", "Zénite", "Pôr do sol", "Crepúsculo"),
|
||||
"pt-br": ("Previsão do tempo para:", "Tempo", "Fuso horário", "Agora", "Alvorada", "Nascer do sol", "Zénite", "Pôr do sol", "Crepúsculo"),
|
||||
@ -69,5 +72,3 @@ V2_TRANSLATION = {
|
||||
"zh-tw": ("天氣預報:", "天氣", "時區", "目前", "黎明", "日出", "日正當中", "日落", "黃昏"),
|
||||
"zu": ("Isimo sezulu:", "", "", "", "", "", "", "", ""),
|
||||
}
|
||||
|
||||
|
||||
|
@ -28,12 +28,13 @@ import pytz
|
||||
from constants import WWO_CODE, WEATHER_SYMBOL, WIND_DIRECTION, WEATHER_SYMBOL_WIDTH_VTE
|
||||
from weather_data import get_weather_data
|
||||
from . import v2
|
||||
from . import prometheus
|
||||
|
||||
PRECONFIGURED_FORMAT = {
|
||||
'1': u'%c %t',
|
||||
'2': u'%c 🌡️%t 🌬️%w',
|
||||
'3': u'%l: %c %t',
|
||||
'4': u'%l: %c 🌡️%t 🌬️%w',
|
||||
'1': r'%c %t\n',
|
||||
'2': r'%c 🌡️%t 🌬️%w\n',
|
||||
'3': r'%l: %c %t\n',
|
||||
'4': r'%l: %c 🌡️%t 🌬️%w\n',
|
||||
}
|
||||
|
||||
MOON_PHASES = (
|
||||
@ -60,6 +61,21 @@ def render_temperature(data, query):
|
||||
|
||||
return temperature
|
||||
|
||||
def render_feel_like_temperature(data, query):
|
||||
"""
|
||||
feel like temperature (f)
|
||||
"""
|
||||
|
||||
if query.get('use_imperial', False):
|
||||
temperature = u'%s°F' % data['FeelsLikeF']
|
||||
else:
|
||||
temperature = u'%s°C' % data['FeelsLikeC']
|
||||
|
||||
if temperature[0] != '-':
|
||||
temperature = '+' + temperature
|
||||
|
||||
return temperature
|
||||
|
||||
def render_condition(data, query):
|
||||
"""Emoji encoded weather condition (c)
|
||||
"""
|
||||
@ -145,7 +161,7 @@ def render_wind(data, query):
|
||||
degree = ""
|
||||
|
||||
if degree:
|
||||
wind_direction = WIND_DIRECTION[((degree+22)%360)//45]
|
||||
wind_direction = WIND_DIRECTION[int(((degree+22.5)%360)/45.0)]
|
||||
else:
|
||||
wind_direction = ""
|
||||
|
||||
@ -224,6 +240,7 @@ FORMAT_SYMBOL = {
|
||||
'C': render_condition_fullname,
|
||||
'h': render_humidity,
|
||||
't': render_temperature,
|
||||
'f': render_feel_like_temperature,
|
||||
'w': render_wind,
|
||||
'l': render_location,
|
||||
'm': render_moonphase,
|
||||
@ -288,7 +305,7 @@ def render_line(line, data, query):
|
||||
|
||||
return ''
|
||||
|
||||
template_regexp = r'%[^%]*[a-zA-Z]'
|
||||
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()
|
||||
@ -297,7 +314,7 @@ def render_line(line, data, query):
|
||||
return re.sub(template_regexp, render_symbol, line)
|
||||
|
||||
def render_json(data):
|
||||
output = json.dumps(data, indent=4, sort_keys=True)
|
||||
output = json.dumps(data, indent=4, sort_keys=True, ensure_ascii=False)
|
||||
|
||||
output = "\n".join(
|
||||
re.sub('"[^"]*worldweatheronline[^"]*"', '""', line) if "worldweatheronline" in line else line
|
||||
@ -320,6 +337,8 @@ def format_weather_data(query, parsed_query, data):
|
||||
|
||||
if format_line == "j1":
|
||||
return render_json(data['data'])
|
||||
if format_line == "p1":
|
||||
return prometheus.render_prometheus(data['data'])
|
||||
if format_line[:2] == "v2":
|
||||
return v2.main(query, parsed_query, data)
|
||||
|
||||
@ -339,7 +358,7 @@ def wttr_line(query, parsed_query):
|
||||
|
||||
data = get_weather_data(location, lang)
|
||||
output = format_weather_data(query, parsed_query, data)
|
||||
return output.rstrip("\n")+"\n"
|
||||
return output.rstrip("\n").replace(r"\n", "\n")
|
||||
|
||||
def main():
|
||||
"""
|
||||
|
@ -15,6 +15,7 @@ def get_moon(parsed_query):
|
||||
location = parsed_query['orig_location']
|
||||
html = parsed_query['html_output']
|
||||
lang = parsed_query['lang']
|
||||
hemisphere = parsed_query['hemisphere']
|
||||
|
||||
date = None
|
||||
if '@' in location:
|
||||
@ -25,6 +26,9 @@ def get_moon(parsed_query):
|
||||
if lang:
|
||||
cmd += ["-l", lang]
|
||||
|
||||
if not hemisphere:
|
||||
cmd += ["-s", "south"]
|
||||
|
||||
if date:
|
||||
try:
|
||||
dateutil.parser.parse(date)
|
||||
|
69
lib/view/prometheus.py
Normal file
69
lib/view/prometheus.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
Rendering weather data in the Prometheus format.
|
||||
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from fields import DESCRIPTION
|
||||
|
||||
def _render_current(data, for_day="current", already_seen=[]):
|
||||
"Converts data into prometheus style format"
|
||||
|
||||
output = []
|
||||
|
||||
for field_name, val in DESCRIPTION.items():
|
||||
|
||||
help, name = val
|
||||
|
||||
try:
|
||||
value = data[field_name]
|
||||
if field_name == "weatherDesc":
|
||||
value = value[0]["value"]
|
||||
except (IndexError, KeyError):
|
||||
try:
|
||||
value = data["astronomy"][0][field_name]
|
||||
if value.endswith(" AM") or value.endswith(" PM"):
|
||||
value = _convert_time_to_minutes(value)
|
||||
except (IndexError, KeyError, ValueError):
|
||||
continue
|
||||
|
||||
try:
|
||||
if name == "observation_time":
|
||||
value = _convert_time_to_minutes(value)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
description = ""
|
||||
try:
|
||||
float(value)
|
||||
except ValueError:
|
||||
description = f", description=\"{value}\""
|
||||
value = "1"
|
||||
|
||||
if name not in already_seen:
|
||||
output.append(f"# HELP {name} {help}")
|
||||
already_seen.append(name)
|
||||
|
||||
output.append(f"{name}{{forecast=\"{for_day}\"{description}}} {value}")
|
||||
|
||||
return "\n".join(output)+"\n"
|
||||
|
||||
def _convert_time_to_minutes(time_str):
|
||||
"Convert time from midnight to minutes"
|
||||
return int((datetime.strptime(time_str, "%I:%M %p")
|
||||
- datetime.strptime("12:00 AM", "%I:%M %p")).total_seconds())//60
|
||||
|
||||
def render_prometheus(data):
|
||||
"""
|
||||
Convert `data` into Prometheus format
|
||||
and return it as string.
|
||||
"""
|
||||
|
||||
already_seen = []
|
||||
answer = _render_current(
|
||||
data["current_condition"][0], already_seen=already_seen)
|
||||
for i in range(3):
|
||||
answer += _render_current(
|
||||
data["weather"][i], for_day="%sd" % i, already_seen=already_seen)
|
||||
return answer
|
@ -38,7 +38,7 @@ from astral import moon, sun
|
||||
from scipy.interpolate import interp1d
|
||||
from babel.dates import format_datetime
|
||||
|
||||
from globals import WWO_KEY
|
||||
from globals import WWO_KEY, remove_ansi
|
||||
import constants
|
||||
import translations
|
||||
import parse_query
|
||||
@ -239,7 +239,7 @@ def draw_time(geo_data):
|
||||
|
||||
# }}}
|
||||
# draw_astronomical {{{
|
||||
def draw_astronomical(city_name, geo_data):
|
||||
def draw_astronomical(city_name, geo_data, config):
|
||||
datetime_day_start = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
|
||||
city = LocationInfo()
|
||||
@ -289,12 +289,18 @@ def draw_astronomical(city_name, geo_data):
|
||||
|
||||
answer += char
|
||||
|
||||
if config.get("view") in ["v2n", "v2d"]:
|
||||
moon_phases = constants.MOON_PHASES_WI
|
||||
moon_phases = [" %s" % x for x in moon_phases]
|
||||
else:
|
||||
moon_phases = constants.MOON_PHASES
|
||||
|
||||
# moon
|
||||
if time_interval in [0,23,47,69]: # time_interval % 3 == 0:
|
||||
moon_phase = moon.phase(
|
||||
date=datetime_day_start + datetime.timedelta(hours=time_interval))
|
||||
moon_phase_emoji = constants.MOON_PHASES[
|
||||
int(math.floor(moon_phase*1.0/28.0*8+0.5)) % len(constants.MOON_PHASES)]
|
||||
moon_phase_emoji = moon_phases[
|
||||
int(math.floor(moon_phase*1.0/28.0*8+0.5)) % len(moon_phases)]
|
||||
# if time_interval in [0, 24, 48, 69]:
|
||||
moon_line += moon_phase_emoji # + " "
|
||||
elif time_interval % 3 == 0:
|
||||
@ -309,19 +315,29 @@ def draw_astronomical(city_name, geo_data):
|
||||
return answer
|
||||
# }}}
|
||||
# draw_emoji {{{
|
||||
def draw_emoji(data):
|
||||
def draw_emoji(data, config):
|
||||
answer = ""
|
||||
if config.get("view") == "v2n":
|
||||
weather_symbol = constants.WEATHER_SYMBOL_WI_NIGHT
|
||||
weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE_WI
|
||||
elif config.get("view") == "v2d":
|
||||
weather_symbol = constants.WEATHER_SYMBOL_WI_NIGHT
|
||||
weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE_WI
|
||||
else:
|
||||
weather_symbol = constants.WEATHER_SYMBOL
|
||||
weather_symbol_width_vte = constants.WEATHER_SYMBOL_WIDTH_VTE
|
||||
|
||||
for i in data:
|
||||
emoji = constants.WEATHER_SYMBOL.get(
|
||||
emoji = weather_symbol.get(
|
||||
constants.WWO_CODE.get(
|
||||
str(int(i)), "Unknown"))
|
||||
space = " "*(3-constants.WEATHER_SYMBOL_WIDTH_VTE.get(emoji))
|
||||
answer += emoji + space
|
||||
space = " "*(3-weather_symbol_width_vte.get(emoji, 1))
|
||||
answer += space[:1] + emoji + space[1:]
|
||||
answer += "\n"
|
||||
return answer
|
||||
# }}}
|
||||
# draw_wind {{{
|
||||
def draw_wind(data, color_data):
|
||||
def draw_wind(data, color_data, config):
|
||||
|
||||
def _color_code_for_wind_speed(wind_speed):
|
||||
|
||||
@ -346,11 +362,16 @@ def draw_wind(data, color_data):
|
||||
answer = ""
|
||||
answer_line2 = ""
|
||||
|
||||
if config.get("view") in ["v2n", "v2d"]:
|
||||
wind_direction_list = constants.WIND_DIRECTION_WI
|
||||
else:
|
||||
wind_direction_list = constants.WIND_DIRECTION
|
||||
|
||||
for j, degree in enumerate(data):
|
||||
|
||||
degree = int(degree)
|
||||
if degree:
|
||||
wind_direction = constants.WIND_DIRECTION[((degree+22)%360)//45]
|
||||
wind_direction = wind_direction_list[int(((degree+22.5)%360)/45.0)]
|
||||
else:
|
||||
wind_direction = ""
|
||||
|
||||
@ -430,14 +451,14 @@ def generate_panel(data_parsed, geo_data, config):
|
||||
output += "\n"
|
||||
|
||||
data = jq_query(weather_code_query, data_parsed)
|
||||
output += draw_emoji(data)
|
||||
output += draw_emoji(data, config)
|
||||
|
||||
data = jq_query(wind_direction_query, data_parsed)
|
||||
color_data = jq_query(wind_speed_query, data_parsed)
|
||||
output += draw_wind(data, color_data)
|
||||
output += draw_wind(data, color_data, config)
|
||||
output += "\n"
|
||||
|
||||
output += draw_astronomical(config["location"], geo_data)
|
||||
output += draw_astronomical(config["location"], geo_data, config)
|
||||
output += "\n"
|
||||
|
||||
output = add_frame(output, max_width, config)
|
||||
@ -604,6 +625,8 @@ def main(query, parsed_query, data):
|
||||
output = generate_panel(data_parsed, geo_data, parsed_query)
|
||||
if query.get("text") != "no" and parsed_query.get("text") != "no":
|
||||
output += textual_information(data_parsed, geo_data, parsed_query)
|
||||
if parsed_query.get('no-terminal', False):
|
||||
output = remove_ansi(output)
|
||||
return output
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
@ -40,7 +40,10 @@ def get_wetter(parsed_query):
|
||||
location_not_found = True
|
||||
stdout += get_message('NOT_FOUND_MESSAGE', lang)
|
||||
|
||||
first_line, stdout = _wego_postprocessing(location, parsed_query, stdout)
|
||||
if "\n" in stdout:
|
||||
first_line, stdout = _wego_postprocessing(location, parsed_query, stdout)
|
||||
else:
|
||||
first_line = ""
|
||||
|
||||
|
||||
if html:
|
||||
|
@ -17,7 +17,7 @@ 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, \
|
||||
from globals import get_help_file, remove_ansi, \
|
||||
BASH_FUNCTION_FILE, TRANSLATION_FILE, LOG_FILE, \
|
||||
NOT_FOUND_LOCATION, \
|
||||
MALFORMED_RESPONSE_HTML_PAGE, \
|
||||
@ -221,7 +221,7 @@ def _response(parsed_query, query, fast_mode=False):
|
||||
#
|
||||
# output = fmt.png.render_ansi(
|
||||
# output, options=parsed_query)
|
||||
result = TASKS.spawn(fmt.png.render_ansi, output, options=parsed_query)
|
||||
result = TASKS.spawn(fmt.png.render_ansi, cache._update_answer(output), options=parsed_query)
|
||||
output = result.get()
|
||||
else:
|
||||
if query.get('days', '3') != '0' \
|
||||
@ -230,7 +230,10 @@ def _response(parsed_query, query, fast_mode=False):
|
||||
if parsed_query['html_output']:
|
||||
output = add_buttons(output)
|
||||
else:
|
||||
output += '\n' + get_message('FOLLOW_ME', parsed_query['lang']) + '\n'
|
||||
message = get_message('FOLLOW_ME', parsed_query['lang'])
|
||||
if parsed_query.get('no-terminal', False):
|
||||
message = remove_ansi(message)
|
||||
output += '\n' + message + '\n'
|
||||
|
||||
return cache.store(cache_signature, output)
|
||||
|
||||
@ -289,9 +292,10 @@ def parse_request(location, request, query, fast_mode=False):
|
||||
parsed_query["lang"] = parsed_query.get("lang", lang)
|
||||
|
||||
parsed_query["html_output"] = get_output_format(query, parsed_query)
|
||||
parsed_query["json_output"] = (parsed_query.get("view", "") or "").startswith("j")
|
||||
|
||||
if not fast_mode: # not png_filename and not fast_mode:
|
||||
location, override_location_name, full_address, country, query_source_location = \
|
||||
location, override_location_name, full_address, country, query_source_location, hemisphere = \
|
||||
location_processing(parsed_query["location"], parsed_query["ip_addr"])
|
||||
|
||||
us_ip = query_source_location[1] == 'United States' \
|
||||
@ -306,7 +310,8 @@ def parse_request(location, request, query, fast_mode=False):
|
||||
'override_location_name': override_location_name,
|
||||
'full_address': full_address,
|
||||
'country': country,
|
||||
'query_source_location': query_source_location})
|
||||
'query_source_location': query_source_location,
|
||||
'hemisphere': hemisphere})
|
||||
|
||||
parsed_query.update(query)
|
||||
return parsed_query
|
||||
@ -318,7 +323,7 @@ def wttr(location, request):
|
||||
it returns output in HTML, ANSI or other format.
|
||||
"""
|
||||
|
||||
def _wrap_response(response_text, html_output, png_filename=None):
|
||||
def _wrap_response(response_text, html_output, json_output, png_filename=None):
|
||||
if not isinstance(response_text, str) and \
|
||||
not isinstance(response_text, bytes):
|
||||
return response_text
|
||||
@ -337,16 +342,21 @@ def wttr(location, request):
|
||||
response.headers[key] = value
|
||||
else:
|
||||
response = make_response(response_text)
|
||||
response.mimetype = 'text/html' if html_output else 'text/plain'
|
||||
if html_output:
|
||||
response.mimetype = "text/html"
|
||||
elif json_output:
|
||||
response.mimetype = "application/json"
|
||||
else:
|
||||
response.mimetype = "text/plain"
|
||||
return response
|
||||
|
||||
if is_location_blocked(location):
|
||||
return ""
|
||||
return ("", 403) # Forbidden
|
||||
|
||||
try:
|
||||
LIMITS.check_ip(_client_ip_address(request))
|
||||
except RuntimeError as exception:
|
||||
return str(exception)
|
||||
return (str(exception), 429) # Too many requests
|
||||
|
||||
query = parse_query.parse_query(request.args)
|
||||
|
||||
@ -356,6 +366,8 @@ def wttr(location, request):
|
||||
# use the full track
|
||||
parsed_query = parse_request(location, request, query, fast_mode=True)
|
||||
response = _response(parsed_query, query, fast_mode=True)
|
||||
|
||||
http_code = 200
|
||||
try:
|
||||
if not response:
|
||||
parsed_query = parse_request(location, request, query)
|
||||
@ -365,15 +377,17 @@ def wttr(location, request):
|
||||
logging.error("Exception has occured", exc_info=1)
|
||||
if parsed_query['html_output']:
|
||||
response = MALFORMED_RESPONSE_HTML_PAGE
|
||||
http_code = 500 # Internal Server Error
|
||||
else:
|
||||
response = get_message('CAPACITY_LIMIT_REACHED', parsed_query['lang'])
|
||||
http_code = 503 # Service Unavailable
|
||||
|
||||
# if exception is occured, we return not a png file but text
|
||||
if "png_filename" in parsed_query:
|
||||
del parsed_query["png_filename"]
|
||||
return _wrap_response(
|
||||
response, parsed_query['html_output'],
|
||||
png_filename=parsed_query.get('png_filename'))
|
||||
return (_wrap_response(
|
||||
response, parsed_query['html_output'], parsed_query['json_output'],
|
||||
png_filename=parsed_query.get('png_filename')), http_code)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import doctest
|
||||
|
@ -8,6 +8,7 @@ pylint
|
||||
cyrtranslit
|
||||
astral
|
||||
timezonefinder==2.1.2
|
||||
pytz
|
||||
pyte
|
||||
python-dateutil
|
||||
diagram
|
||||
|
@ -29,7 +29,7 @@ nürnberg : Nuremberg
|
||||
norway : Oslo
|
||||
ville de bruxelles - stad brussel: Brussels
|
||||
frankfurt am main:Frankfurt, Hessen
|
||||
frankfurt :Frankfurt, Brandenburg
|
||||
frankfurt :Frankfurt, Hessen
|
||||
frankfurt oder : Frankfurt, Brandenburg
|
||||
frankfurt (oder): Frankfurt, Brandenburg
|
||||
tel-aviv : Tel Aviv
|
||||
|
@ -4,19 +4,19 @@ logfile=/var/log/supervisor/supervisord.log
|
||||
pidfile=/var/run/supervisord.pid
|
||||
|
||||
[program:srv]
|
||||
command=python /app/bin/srv.py
|
||||
command=python3 /app/bin/srv.py
|
||||
stderr_logfile=/var/log/supervisor/srv-stderr.log
|
||||
stdout_logfile=/var/log/supervisor/srv-stdout.log
|
||||
|
||||
[program:proxy]
|
||||
command=python /app/bin/proxy.py
|
||||
command=python3 /app/bin/proxy.py
|
||||
stderr_logfile=/var/log/supervisor/proxy-stderr.log
|
||||
stdout_logfile=/var/log/supervisor/proxy-stdout.log
|
||||
|
||||
[program:geoproxy]
|
||||
command=python /app/bin/geo-proxy.py
|
||||
command=python3 /app/bin/geo-proxy.py
|
||||
stderr_logfile=/var/log/supervisor/geoproxy-stderr.log
|
||||
stdout_logfile=/var/log/supervisor/geoproxy-stdout.log
|
||||
|
||||
[include]
|
||||
files=/etc/supervisor/conf.d/*.conf
|
||||
files=/etc/supervisor/conf.d/*.conf
|
||||
|
@ -6,7 +6,7 @@ Usage:
|
||||
Supported location types:
|
||||
|
||||
/paris # city name
|
||||
/~Eiffel+tower # any location
|
||||
/~Eiffel+tower # any location (+ for spaces)
|
||||
/Москва # Unicode name of any location in any language
|
||||
/muc # airport code (3 letters)
|
||||
/@stackoverflow.com # domain name
|
||||
|
@ -31,10 +31,12 @@ Translated/improved/corrected by:
|
||||
* German: Igor Chubin, @MAGICC (https://kthx.at)
|
||||
* Greek: Panayotis Vryonis and @Petewg (on github)
|
||||
* Hebrew: E.R.
|
||||
* Hindi: Aakash Gajjar @skyme5 (gh)
|
||||
* Hungarian: Mark Petruska
|
||||
* Icelandic: Óli G. @dvergur, Skúli Arnlaugsson @Arnlaugsson
|
||||
* Indonesian: Andria Arisal @andria009
|
||||
* Interlingua: Dustin Redmond @dustinkredmond (on github)
|
||||
* Irish: Robert Devereux @RobertDev (gh), Conor O'Callaghan @ivernus (gh)
|
||||
* Italian: Diego Maniacco
|
||||
* Japanese: @ryunix
|
||||
* Kazakh: Akku Tutkusheva, Oleg Tropinin
|
||||
@ -43,6 +45,7 @@ Translated/improved/corrected by:
|
||||
* Macedonian: Matej Plavevski @MatejMecka
|
||||
* Norwegian: Fredrik Fjeld @fredrikfjeld
|
||||
* Nynorsk: Kevin Brubeck Unhammer (https://unhammer.org/k/)
|
||||
* Occitan: Quentin PAGÈS @Quenty-tolosan (gh)
|
||||
* Persian: Javad @threadripper_
|
||||
* Polish: Wojtek Łukasiewicz @wojtuch (on github)
|
||||
* Portuguese: Fernando Bitti Loureiro @fbitti (on github)
|
||||
|
@ -1,14 +1,14 @@
|
||||
Argibideak:
|
||||
|
||||
$ curl wttr.in # eguraldia zure kokapenean
|
||||
$ curl wttr.in/muc # eguraldia Municheko aireportuan
|
||||
$ curl wttr.in/bio # eguraldia Bilboko aireportuan
|
||||
|
||||
Onartzen diren kokapen motak:
|
||||
|
||||
/paris # hiri baten izena
|
||||
/~Eiffel+tower # leku famatu baten izena
|
||||
/bilbao # hiri baten izena
|
||||
/~Bilbao+Guggenheim+museum # leku famatu baten izena
|
||||
/Москва # Edozein lekuko edozein hizkuntzako Unicode izena
|
||||
/muc # aeroportu baten kodea (3 letra)
|
||||
/bio # aeroportu baten kodea (3 letra)
|
||||
/@stackoverflow.com # web domeinu baten izena
|
||||
/94107 # area kode bat
|
||||
/-78.46,106.79 # GPS koordenadak
|
||||
|
81
share/translations/ga.txt
Normal file
81
share/translations/ga.txt
Normal file
@ -0,0 +1,81 @@
|
||||
113: Geal : Clear
|
||||
113: Grianmhar : Sunny
|
||||
116: Breacscamallach : Partly cloudy
|
||||
119: Scamallach : Cloudy
|
||||
122: Scamallach : Overcast
|
||||
143: Ceochán : Mist
|
||||
176: Ceathanna : Patchy rain possible
|
||||
179: Ceathanna sneachta : Patchy snow possible
|
||||
182: Ceathanna flichshneachta : Patchy sleet possible
|
||||
185: Ceathanna cloichshneachta : Patchy freezing drizzle possible
|
||||
200: Ceathanna toirní : Thundery outbreaks possible
|
||||
227: Stealladh sneachta : Blowing snow
|
||||
230: Síobadh sneachta : Blizzard
|
||||
248: Ceo : Fog
|
||||
260: Ceo reoite : Freezing fog
|
||||
263: Ceobhrán : Patchy light drizzle
|
||||
266: Ceobhrán éadrom : Light drizzle
|
||||
281: Ceobhrán reoite : Freezing drizzle
|
||||
284: Ceobhrán reoite trom : Heavy freezing drizzle
|
||||
293: Breachbháisteach éadrom : Patchy light rain
|
||||
296: Báisteach éadrom : Light rain
|
||||
299: Báisteach mheasartha uaireanta : Moderate rain at times
|
||||
302: Báisteach mheasartha : Moderate rain
|
||||
305: Báisteach throm uairanta : Heavy rain at times
|
||||
308: Báisteach throm : Heavy rain
|
||||
311: Báisteach reoite éadrom : Light freezing rain
|
||||
314: Báisteach mheasartha nó reoite throm : Moderate or heavy freezing rain
|
||||
317: Flichshneachta éadrom : Light sleet
|
||||
320: Flichshneachta measartha nó trom : Moderate or heavy sleet
|
||||
323: Breacsneachta éadrom : Patchy light snow
|
||||
326: Sneachta éadrom : Light snow
|
||||
329: Breacsneachta measartha : Patchy moderate snow
|
||||
332: Sneachta measartha : Moderate snow
|
||||
335: Sneachta trom uaireanta : Patchy heavy snow
|
||||
338: Sneachta trom : Heavy snow
|
||||
350: Chloichshneachta : Ice pellets
|
||||
353: Cith éadrom : Light rain shower
|
||||
356: Cith measartha no trom : Moderate or heavy rain shower
|
||||
359: Duartan : Torrential rain shower
|
||||
362: Ceathanna flichshneachta : Light sleet showers
|
||||
365: Ceathanna flichshneachta measartha nó troma : Moderate or heavy sleet showers
|
||||
368: Ceathanna sneachta éadroma : Light snow showers
|
||||
371: Ceathanna sneachta measartha nó troma : Moderate or heavy snow showers
|
||||
386: Ceathanna éadroma le toirneach : Patchy light rain with thunder
|
||||
389: Báisteach measartha nó throm le tóirneach : Moderate or heavy rain with thunder
|
||||
392: Sneachta éadrom le toirneach : Patchy light snow with thunder
|
||||
395: Sneachta measartha nó trom le toirneach : Moderate or heavy snow with thunder
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
: Kiša : Rain
|
||||
113 : Vedro : Clear
|
||||
113 : Sunčano : Sunny
|
||||
116 : Djelomično oblačno : Partly cloudy
|
||||
|
66
share/translations/oc-help.txt
Normal file
66
share/translations/oc-help.txt
Normal file
@ -0,0 +1,66 @@
|
||||
Usatge:
|
||||
|
||||
$ curl wttr.in # emplaçament actual
|
||||
$ curl wttr.in/cdg # metèo a l'aeropòrt de Paris - Charles de Gaulle
|
||||
|
||||
Types d'emplacements acceptés:
|
||||
|
||||
/toulouse # nom de la vila
|
||||
/~Eiffel+tower # emplaçament qual que siá
|
||||
/Москва # nom Unicode o emplaçament qual que siá dins quala que siá lenga
|
||||
/muc # còde aeropòrt (3 letras)
|
||||
/@stackoverflow.com # nom de domeni
|
||||
/94107 # còde postal (sonque pels Estats Units)
|
||||
/-78.46,106.79 # coordenadas GPS
|
||||
|
||||
Emplacements particuliers:
|
||||
|
||||
/moon # passas de la luna (ajustar ,+US o ,+France per accedir a las vilas del meteis nom)
|
||||
/moon@2016-10-25 # passas de la luna per aquesta data (@2016-10-25)
|
||||
|
||||
Unitats:
|
||||
|
||||
?m # sistèma metric (per defaut pertot levat als Estats Units d d'America)
|
||||
?u # USCS (per defaut pels Estats-Units d'America)
|
||||
?M # afichar la velocitat del vent en m/s
|
||||
|
||||
Opcion d'afichatge:
|
||||
|
||||
?0 # uèi solament
|
||||
?1 # uèi + deman
|
||||
?2 # uèi + 2 jorns
|
||||
?n # version corta (sonque pel jorn e la nuèch)
|
||||
?q # version silenciosa (cap d'entèsta "Previsions metèo per")
|
||||
?Q # version super-silencieuse (pas d'en-tête "Prévisions météo pour", pas de nom de la ville)
|
||||
?T # sequéncias d'ecapament pels terminals desactivadas (cap de colors)
|
||||
|
||||
Opcions PNG:
|
||||
|
||||
/paris.png # gnèra un fichièr PNG
|
||||
?p # ajuta un quadre altorn de la sortida
|
||||
?t # transparéncia 150 (transparence 150)
|
||||
transparency=... # transparéncia de 0 fins a 255 (255 = cap de transparéncia)
|
||||
|
||||
Combinar las opcions:
|
||||
|
||||
/Toulouse?0pq
|
||||
/Toulouse?0pq&lang=oc
|
||||
/Toulouse_0pq.png # dins lo mòde PNG las opcions son especificadas après _
|
||||
/Rome_0pq_lang=it.png # las opcions longas son separadas per de underscores _
|
||||
|
||||
Localizacion:
|
||||
|
||||
$ curl fr.wttr.in/Toulouse
|
||||
$ curl wttr.in/toulouse?lang=oc
|
||||
$ curl -H "Accept-Language: oc" wttr.in/toulouse
|
||||
|
||||
Lengas suportadas:
|
||||
|
||||
FULL_TRANSLATION (Support complèt)
|
||||
PARTIAL_TRANSLATION (Support incomplèt)
|
||||
|
||||
URLs particularas:
|
||||
|
||||
/:help # mostra aquesta pagina
|
||||
/:bash.function # foncion bash recomandada wttr()
|
||||
/:translation # mostra las informacions sus la traduccion de wttr.in
|
50
share/translations/oc.txt
Normal file
50
share/translations/oc.txt
Normal file
@ -0,0 +1,50 @@
|
||||
: Pluèja : Rain
|
||||
: Plugeta, Raissas : Light Rain, Rain Shower
|
||||
: Raissas : Rain Shower
|
||||
113 : Temps clar : Clear
|
||||
113 : Solelh : Sunny
|
||||
116 : Nivolós en partida : Partly cloudy
|
||||
119 : Nivolós : Cloudy
|
||||
122 : Ennivolat : Overcast
|
||||
143 : Brumós : Mist
|
||||
176 : Possiblas raissas esparpalhadas : Patchy rain possible
|
||||
179 : Possiblas nevadas esparpalhadas : Patchy snow possible
|
||||
182 : Possiblas nèus fondudas esparpalhadas : Patchy sleet possible
|
||||
185 : Possiblas plovinas gelibrantas esparpalhadas : Patchy freezing drizzle possible
|
||||
200 : Auratges possibles : Thundery outbreaks possible
|
||||
227 : Nèu e vent : Blowing snow
|
||||
230 : Blisard : Blizzard
|
||||
248 : Fums : Fog
|
||||
260 : Nèbla gelibranta : Freezing fog
|
||||
263 : Plovina leugièra esparpalhada : Patchy light drizzle
|
||||
266 : Plovina leugièra : Light drizzle
|
||||
281 : Plovina gelibranta : Freezing drizzle
|
||||
284 : Fòrta plovina gelibranta : Heavy freezing drizzle
|
||||
293 : Plugeta fina : Patchy light rain
|
||||
296 : Plovina : Light rain
|
||||
299 : Pluèja moderada intermitenta : Moderate rain at times
|
||||
302 : Pluèja moderada : Moderate rain
|
||||
305 : Forte pluie intermittente : Heavy rain at times
|
||||
308 : Raissa : Heavy rain
|
||||
311 : Pluie verglaçante légère : Light freezing rain
|
||||
314 : Pluie verglaçante modérée à forte : Moderate or heavy freezing rain
|
||||
317 : Aiganèu leugièra : Light sleet
|
||||
320 : Aiganèu moderada o fòrta : Moderate or heavy sleet
|
||||
323 : Nevada esparpalhada e leugièra : Patchy light snow
|
||||
326 : Nevada leugièra : Light snow
|
||||
329 : Nevada esparpalhada parciala : Patchy moderate snow
|
||||
332 : Nevada moderada : Moderate snow
|
||||
335 : Nevada fòrta parciala : Patchy heavy snow
|
||||
338 : Nevada fòrta : Heavy snow
|
||||
350 : Pèiras de glaça : Ice pellets
|
||||
353 : Pluèja leugièra : Light rain shower
|
||||
356 : Pluèja moderada a fòrta : Moderate or heavy rain shower
|
||||
359 : Pluèja torrenciala : Torrential rain shower
|
||||
362 : Granissa leugièra : Light sleet showers
|
||||
365 : Granissa moderada a fòrta : Moderate or heavy sleet showers
|
||||
368 : Nevadas leugièra : Light snow showers
|
||||
371 : Granissa moderada a violenta : Moderate or heavy snow showers
|
||||
386 : Pluèja auratjosa leugièra esparpalhada : Patchy light rain with thunder
|
||||
389 : Pluèja auratjosa moderada a violenta : Moderate or heavy rain with thunder
|
||||
392 : Nevada auratjosa esparpalhada e leugièra : Patchy light snow with thunder
|
||||
395 : Nevada auratjosa moderada a violenta : Moderate or heavy snow with thunder
|
@ -1,3 +1,4 @@
|
||||
: Проливной дождь : Rain shower :
|
||||
113: Ясно : Clear :
|
||||
113: Солнечно : Sunny :
|
||||
116: Переменная облачность : Partly cloudy :
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user