This repository has been archived on 2025-03-19. You can view files and clone it, but cannot push or open issues or pull requests.
rebble-weather/weather/__init__.py
2020-03-07 23:22:36 -05:00

135 lines
5.3 KiB
Python

import datetime
import os
import time
import beeline
from beeline.middleware.flask import HoneyMiddleware
from beeline.patch import requests
import requests
from flask import Flask, request, jsonify, abort
from werkzeug.exceptions import HTTPException
from werkzeug.routing import FloatConverter
app = Flask(__name__)
if 'HONEYCOMB_KEY' in os.environ:
beeline.init(writekey=os.environ['HONEYCOMB_KEY'], dataset='rws', service_name='auth')
HoneyMiddleware(app, db_events = True)
auth_internal = os.environ['REBBLE_AUTH_URL_INT']
ibm_root = os.environ['IBM_API_ROOT']
http_protocol = os.environ.get('HTTP_PROTOCOL', 'https')
# For some reason, the standard float converter rejects negative numbers
# (and also integers without a decimal point).
class SignedFloatConverter(FloatConverter):
regex = r'-?\d+(\.\d+)?'
app.url_map.converters['float'] = SignedFloatConverter
def format_date(date):
return date.strftime("%Y-%m-%dT%H:%M:%S")
class HTTPPaymentRequired(HTTPException):
def __init__(self, description=None, response=None):
self.code = 402
super().__init__(description, response)
@app.route('/heartbeat')
def heartbeat():
return jsonify({'alive': True})
@app.route('/api/v1/geocode/<float:latitude>/<float:longitude>/')
def geocode(latitude, longitude):
if not request.args.get('access_token'):
abort(401)
user_req = requests.get(f"{auth_internal}/api/v1/me",
headers={'Authorization': f"Bearer {request.args['access_token']}"})
user_req.raise_for_status()
if not user_req.json()['is_subscribed']:
raise HTTPPaymentRequired()
beeline.add_context_field("user", user_req.json()['id'])
units = request.args.get('units', 'h')
language = request.args.get('language', 'en-US')
beeline.add_context_field("weather.language", language)
beeline.add_context_field("weather.units", units)
forecast_req = requests.get(f"{ibm_root}/geocode/{latitude}/{longitude}/forecast/daily/7day.json?language={language}&units={units}")
forecast_req.raise_for_status()
forecast = forecast_req.json()
current_req = requests.get(f"{ibm_root}/geocode/{latitude}/{longitude}/observations.json?language={language}&units={units}")
current_req.raise_for_status()
current = current_req.json()
observation = current['observation']
old_style_conditions = {
'metadata': current['metadata'],
'observation': {
'class': observation['class'],
'expire_time_gmt': observation['expire_time_gmt'],
'obs_time': observation['valid_time_gmt'],
# 'obs_time_local': we don't know.
'wdir': observation['wdir'],
'icon_code': observation['wx_icon'],
'icon_extd': observation['icon_extd'],
# sunrise: we don't know these, but we could yank them out of the forecast for today.
# sunset
'day_ind': observation['day_ind'],
'uv_index': observation['uv_index'],
# uv_warning: I don't even know what this is. Apparently numeric.
# wxman: ???
'obs_qualifier_code': observation['qualifier'],
'ptend_code': observation['pressure_tend'],
'dow': datetime.datetime.utcfromtimestamp(observation['valid_time_gmt']).strftime('%A'),
'wdir_cardinal': observation['wdir_cardinal'], # sometimes this is "CALM", don't know if that's okay
'uv_desc': observation['uv_desc'],
# I'm just guessing at how the three phrases map.
'phrase_12char': observation['blunt_phrase'] or observation['wx_phrase'],
'phrase_22char': observation['terse_phrase'] or observation['wx_phrase'],
'phrase_32char': observation['wx_phrase'],
'ptend_desc': observation['pressure_desc'],
# sky_cover: we don't seem to get a description of this?
'clds': observation['clds'],
'obs_qualifier_severity': observation['qualifier_svrty'],
# vocal_key: we don't get one of these
{'e': 'imperial', 'm': 'metric', 'h': 'uk_hybrid'}[units]: {
'wspd': observation['wspd'],
'gust': observation['gust'],
'vis': observation['vis'],
# mslp: don't know what this is but it doesn't map to anything
'altimeter': observation['pressure'],
'temp': observation['temp'],
'dewpt': observation['dewPt'],
'rh': observation['rh'],
'wc': observation['wc'],
'hi': observation['heat_index'],
'feels_like': observation['feels_like'],
# temp_change_24hour, temp_max_24hour, temp_min_24hour, pchange: don't get any of these
# {snow,precip}_{{1,6,24}hour,mtd,season,{2,3,7}day}: don't get these either
# ceiling, obs_qualifier_{100,50,32}char: or these.
# these are all now in their own request that you can pay extra to retrieve.
},
}
}
return jsonify(
fcstdaily7={
'errors': False,
'data': forecast,
},
conditions={
'errors': False,
'data': old_style_conditions,
},
metadata={
'version': 2,
'transaction_id': str(int(time.time())),
},
)