From 52cd654f581e7986de524a97e9ae61bf38324228 Mon Sep 17 00:00:00 2001 From: Sam Chudnick Date: Sun, 4 Jun 2023 17:47:00 -0400 Subject: Started turning into flask app --- app.py | 45 ++++++++++++++++++++++ forms.py | 8 ++++ library.py | 68 ++++++++++++++++++++++++++++----- main.py | 53 -------------------------- static/style.css | 102 ++++++++++++++++++++++++++++++++++++++++++++++++++ templates/base.html | 21 +++++++++++ templates/daily.html | 13 +++++++ templates/hourly.html | 18 +++++++++ templates/index.html | 26 +++++++++++++ 9 files changed, 291 insertions(+), 63 deletions(-) create mode 100644 app.py create mode 100644 forms.py delete mode 100644 main.py create mode 100644 static/style.css create mode 100644 templates/base.html create mode 100644 templates/daily.html create mode 100644 templates/hourly.html create mode 100644 templates/index.html diff --git a/app.py b/app.py new file mode 100644 index 0000000..e929032 --- /dev/null +++ b/app.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python3 +import json, requests, datetime, argparse, pytz, flask +import library, forms + +app = flask.Flask(__name__) +app.config['SECRET_KEY'] = "hunter2" + +@app.route('/', methods=('GET','POST')) +def index(): + form = forms.WeatherForm() + if form.validate_on_submit(): + location = form.location.data + days = form.days.data + forecast_type = form.forecast_type.data + print(location) + print(days) + print(forecast_type) + if forecast_type == 'hourly': + return flask.redirect(flask.url_for('hourly', location=location, days=days)) + elif forecast_type == 'daily': + return flask.redirect(flask.url_for('daily', location=location, days=days)) + + return flask.render_template("index.html", form=form) + +@app.route('/hourly') +def hourly(): + location = flask.request.args.get('location', type=str) + days = flask.request.args.get('days', type=int) + latitude, longitude = library.get_lat_long(location) + grid_data = library.get_grid_data(latitude, longitude) + raw_data = library.get_raw_data(grid_data["grid_id"], grid_data["grid_x"], grid_data["grid_y"]) + data = library.hourly_forecast(raw_data, days) + return flask.render_template("hourly.html", data=data) + + +@app.route('/daily') +def daily(): + location = flask.request.args.get('location', type=str) + days = flask.request.args.get('days', type=int) + latitude, longitude = library.get_lat_long(location) + grid_data = library.get_grid_data(latitude, longitude) + raw_data = library.get_raw_data(grid_data["grid_id"], grid_data["grid_x"], grid_data["grid_y"]) + raw_forecast = library.get_raw_forecast(grid_data["grid_id"], grid_data["grid_x"], grid_data["grid_y"]) + data = library.daily_forecast(raw_data, raw_forecast, days) + return flask.render_template("daily.html", data=data) diff --git a/forms.py b/forms.py new file mode 100644 index 0000000..804a0f2 --- /dev/null +++ b/forms.py @@ -0,0 +1,8 @@ +import flask_wtf, wtforms + +class WeatherForm(flask_wtf.FlaskForm): + location = wtforms.StringField("Location", validators=[wtforms.validators.DataRequired()]) + days = wtforms.SelectField("Days", choices=[('1','1'),('2','2'),('3','3'),('4','4'),('5','5'),('6','6'),('7','7')]) + forecast_type = wtforms.RadioField("Type", choices=[('hourly', 'Hourly Forecast'),('daily','Daily Forecast')], default="hourly") + submit = wtforms.SubmitField("Submit") + diff --git a/library.py b/library.py index dcb4630..0d490a1 100644 --- a/library.py +++ b/library.py @@ -23,12 +23,14 @@ def get_grid_data(latitude, longitude): def get_raw_data(grid_id, grid_x, grid_y): - raw_data = json.loads(requests.get(f"https://api.weather.gov/gridpoints/{grid_id}/{grid_x},{grid_y}").text) + headers = {"User-Agent": "pywttr 0.1"} + raw_data = json.loads(requests.get(f"https://api.weather.gov/gridpoints/{grid_id}/{grid_x},{grid_y}", headers=headers).text) return raw_data def get_raw_forecast(grid_id, grid_x, grid_y): - raw_data = json.loads(requests.get(f"https://api.weather.gov/gridpoints/{grid_id}/{grid_x},{grid_y}/forecast", user_agent="my-test-app").text) + headers = {"User-Agent": "pywttr 0.1"} + raw_data = json.loads(requests.get(f"https://api.weather.gov/gridpoints/{grid_id}/{grid_x},{grid_y}/forecast", headers=headers).text) return raw_data @@ -136,11 +138,22 @@ def get_daily_lows(raw_data): time_str, duration_str = low["validTime"].split('/') time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") duration = parse_duration(duration_str) - daily_lows.append({"time":time,"duration":duration,"low_celc":low_celc,"low_fahr":low_far}) + daily_lows.append({"time":time,"duration":duration,"low_celc":low_celc,"low_fahr":low_fahr}) return daily_lows +def get_daily_forecast(raw_data): + daily_forecast_raw = raw_data["properties"]["periods"] + + daily_forecast = [] + for point in daily_forecast_raw: + time_str = point["startTime"] + time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") + daily_forecast.append({"time":time, "short_forecast":point["shortForecast"], "detailed_forecast":point["detailedForecast"]}) + return make_current(set_timezone(daily_forecast)) + + def get_temperature(raw_data): raw_values = raw_data["properties"]["temperature"]["values"] ret = [] @@ -334,11 +347,9 @@ def get_hourly_data(raw_data, end_time): precip_amount = get_precip_amount(raw_data) wind_speed = get_wind_speed(raw_data) wind_direction = get_wind_direction(raw_data) - - ret = [] + ret_list = [] i = 0 while i < len(temps) and temps[i]["time"] < end_time: - if i >= len(temps): temps.append({"value":"N/A"}) if i >= len(humidity): @@ -352,8 +363,6 @@ def get_hourly_data(raw_data, end_time): if i >= len(wind_direction): wind_direction.append({"value":"N/A"}) - - val_dict = { "time": temps[i]["time"], "temp": temps[i]["value_fahr"], "humidity": humidity[i]["value"], @@ -361,7 +370,46 @@ def get_hourly_data(raw_data, end_time): "precip_amount": precip_amount[i]["value"], "wind_speed": wind_speed[i]["value"], "wind_direction": wind_direction[i]["value"] } - ret.append(val_dict) + ret_list.append(val_dict) i+=1 - return ret + return ret_list + + +def get_daily_data(raw_data, raw_forecast, end_time): + daily_highs = get_daily_highs(raw_data) + daily_lows = get_daily_lows(raw_data) + daily_forecasts = get_daily_forecast(raw_forecast) + ret_list = [] + i = 0 + while i < len(daily_highs) and daily_highs[i]["time"] < end_time: + val_dict = { "time": daily_highs[i]["time"], + "high": daily_highs[i]["high_fahr"], + "low": daily_lows[i]["low_fahr"], + "short_forecast_am": daily_forecasts[i]["short_forecast"], + "detailed_forecast_am": daily_forecasts[i]["detailed_forecast"], + "short_forecast_pm": daily_forecasts[i+1]["short_forecast"], + "detailed_forecast_pm": daily_forecasts[i+1]["detailed_forecast"] } + ret_list.append(val_dict) + i+=1 + return ret_list + + +def hourly_forecast(raw_data, days): + init_time = get_current_rounded_time() + if days > 0: + delta = datetime.timedelta(days=days) + else: + delta = datetime.timedelta(hours=(24-init_time.hour)) + end_time = init_time + delta + return get_hourly_data(raw_data, end_time) + + +def daily_forecast(raw_data, raw_forecast, days): + init_time = get_current_rounded_time() + if days > 0: + delta = datetime.timedelta(days=days) + else: + delta = datetime.timedelta(days=5) + end_time = init_time + delta + return get_daily_data(raw_data, raw_forecast, end_time) diff --git a/main.py b/main.py deleted file mode 100644 index 3fbf3e7..0000000 --- a/main.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 -import json, requests, datetime, argparse, pytz - -import library - -def parse_args(): - parser = argparse.ArgumentParser() - parser.add_argument('-d', '--days', type=int) - parser.add_argument('-H', '--hours', type=int) - parser.add_argument('-l', '--location', type=str, required=True) - return parser.parse_args() - - -def hourly_forecast(args, raw_data): - - init_time = library.get_current_rounded_time() - if args.hours is not None: - delta = datetime.timedelta(hours=args.hours) - elif args.days is not None: - delta = datetime.timedelta(days=args.days) - else: - delta = datetime.timedelta(hours=(24-init_time.hour)) - end_time = init_time + delta - - hourly_data = library.get_hourly_data(raw_data, end_time) - - for point in hourly_data: - print(point["time"].strftime("%a %x %I:%M %p")) - print(f'\t Temperature - { point["temp"] }°F') - print(f'\t Humidity - { point["humidity"] }%') - print(f'\t Chance of Precipitation - { point["precip_chance"] }%') - print(f'\t Precipitation Amount - { point["precip_amount"] } in') - print(f'\t Wind Speed - { point["wind_speed"] } MPH') - print(f'\t Wind Direction - { point["wind_direction"] }') - print('\n') - - - -def daily_forecast(raw_data): - pass - - -def main(): - args = parse_args() - latitude, longitude = library.get_lat_long(args.location) - grid_data = library.get_grid_data(latitude, longitude) - raw_data = library.get_raw_data(grid_data["grid_id"], grid_data["grid_x"], grid_data["grid_y"]) - hourly_forecast(args, raw_data) - - - -if __name__ == '__main__': - main() diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..693eee9 --- /dev/null +++ b/static/style.css @@ -0,0 +1,102 @@ +@charset "UTF-8"; + +:root { + /* Set sans-serif & mono fonts */ + --sans-font: Inter, Lato,Helvetica,"IBM Plex Sans","Roboto","Nimbus Sans L","Noto Sans", "Segoe UI",Arial,Helvetica,"Helvetica Neue",sans-serif; + --mono-font: "mononoki Nerd Font","IBM Plex Mono","Roboto Mono","Ubuntu Mono","Fira Code","Overpass Mono", Monaco,"Droid Sans Mono",monospace; + --bg: #242933; + --accent-bg: rgb(46, 52, 64); + --text: #eceff4; + --text-light: #d8dee9; + --border: #88c0d0; + --accent: #81a1c1; + --accent-light: #bf616a; + --code: #ebcb8b; + --alert: #a3be8c; + --alert-bg: #8fbcbb; + --code-bg: #2e3440; +} + + +html, body, footer { + background: var(--bg); + color: var(--text); + font-family: var(--sans-font); + justify-content: center; + align-items: center; + display: flex; +} + +div.weather-box { + height: 100%; + align-items: center; + display: inline-flex; +} + +div.daily-box { + text-align: center; + padding: 1em; + border: 0.5em solid var(--acent-bg); + border-radius: 5%; + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.24); + box-sizing: border-box; + width: 20em; + height: 10em; + overflow-wrap: normal; + float: left; +} + +div.hourly-box { + text-align: center; + padding: 1em; + border: 0.5em solid var(--acent-bg); + border-radius: 5%; + box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.24); + box-sizing: border-box; + width: 20em; + height: 10em; + overflow-wrap: normal; +} + + +/*=== Links */ +a { + color: var(--accent); +} + +a:hover { + color: var(--accent); +} + +input, select, textarea { + margin: 5px; + padding: 5px; + color: var(--text); + border: 1px solid var(--border); + border-radius: 6px; + border-color: var(--border); + background-color: var(--bg); + min-height: 25px; + line-height: 25px; + vertical-align: middle; +} + +input:disabled, select:disabled { + color: #aaa; + border-color: var(--border); +} + +button { + font-family: var(--sans-font); +} + +button.as-link, +button.as-link:hover, +button.as-link:active { + background: transparent; + /* background-color: var(--bg);A*/ +} + +button.as-link[disabled] { + color: #ddd !important; +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..d464a5a --- /dev/null +++ b/templates/base.html @@ -0,0 +1,21 @@ + + + + + {% block title %} {% endblock %} + + + + + +
+ {% block content %} {% endblock %} +
+ + +{% block scripts %}{% endblock %} + + diff --git a/templates/daily.html b/templates/daily.html new file mode 100644 index 0000000..5eb765e --- /dev/null +++ b/templates/daily.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% block content %} +
+{% for item in data %} +
+ {{ item.time.strftime("%a %x") }}
+ {{ item.high }}°F / {{ item.low }}°F
+ {{ item.short_forecast_am }}

+
+{% endfor %} +
+{% endblock %} + diff --git a/templates/hourly.html b/templates/hourly.html new file mode 100644 index 0000000..56f9d9b --- /dev/null +++ b/templates/hourly.html @@ -0,0 +1,18 @@ +{% extends 'base.html' %} + +{% block content %} +
+{% for item in data %} +
+ {{ item.time.strftime("%a %x %I:%M %p") }}
+ {{ item.temp }}°F
+ {{ item.humidity }}%
+ {{ item.precip_chance }}%
+ {{ item.precip_amount }}in
+ {{ item.wind_speed }}MPH
+ {{ item.wind_direction }}

+
+{% endfor %} +
+{% endblock %} + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..2d97379 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,26 @@ +{% extends 'base.html' %} + +{% block content %} +

{% block title %} Enter a Location {% endblock %}

+
+ {{ form.csrf_token }} +

+ {{ form.location.label }}
+ {{ form.location }} +

+

+ {{ form.days.label }}
+ {{ form.days }} +

+

+ {{ form.forecast_type.label }}
+ {{ form.forecast_type }} +

+

{{ form.submit() }}

+
+{% endblock %} + +
+ {{ form.name.label }} {{ form.name(size=20) }} + +
-- cgit v1.2.3