diff options
| -rw-r--r-- | app.py | 45 | ||||
| -rw-r--r-- | forms.py | 8 | ||||
| -rw-r--r-- | library.py | 68 | ||||
| -rw-r--r-- | main.py | 53 | ||||
| -rw-r--r-- | static/style.css | 102 | ||||
| -rw-r--r-- | templates/base.html | 21 | ||||
| -rw-r--r-- | templates/daily.html | 13 | ||||
| -rw-r--r-- | templates/hourly.html | 18 | ||||
| -rw-r--r-- | templates/index.html | 26 |
9 files changed, 291 insertions, 63 deletions
| @@ -0,0 +1,45 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | import json, requests, datetime, argparse, pytz, flask | ||
| 3 | import library, forms | ||
| 4 | |||
| 5 | app = flask.Flask(__name__) | ||
| 6 | app.config['SECRET_KEY'] = "hunter2" | ||
| 7 | |||
| 8 | @app.route('/', methods=('GET','POST')) | ||
| 9 | def index(): | ||
| 10 | form = forms.WeatherForm() | ||
| 11 | if form.validate_on_submit(): | ||
| 12 | location = form.location.data | ||
| 13 | days = form.days.data | ||
| 14 | forecast_type = form.forecast_type.data | ||
| 15 | print(location) | ||
| 16 | print(days) | ||
| 17 | print(forecast_type) | ||
| 18 | if forecast_type == 'hourly': | ||
| 19 | return flask.redirect(flask.url_for('hourly', location=location, days=days)) | ||
| 20 | elif forecast_type == 'daily': | ||
| 21 | return flask.redirect(flask.url_for('daily', location=location, days=days)) | ||
| 22 | |||
| 23 | return flask.render_template("index.html", form=form) | ||
| 24 | |||
| 25 | @app.route('/hourly') | ||
| 26 | def hourly(): | ||
| 27 | location = flask.request.args.get('location', type=str) | ||
| 28 | days = flask.request.args.get('days', type=int) | ||
| 29 | latitude, longitude = library.get_lat_long(location) | ||
| 30 | grid_data = library.get_grid_data(latitude, longitude) | ||
| 31 | raw_data = library.get_raw_data(grid_data["grid_id"], grid_data["grid_x"], grid_data["grid_y"]) | ||
| 32 | data = library.hourly_forecast(raw_data, days) | ||
| 33 | return flask.render_template("hourly.html", data=data) | ||
| 34 | |||
| 35 | |||
| 36 | @app.route('/daily') | ||
| 37 | def daily(): | ||
| 38 | location = flask.request.args.get('location', type=str) | ||
| 39 | days = flask.request.args.get('days', type=int) | ||
| 40 | latitude, longitude = library.get_lat_long(location) | ||
| 41 | grid_data = library.get_grid_data(latitude, longitude) | ||
| 42 | raw_data = library.get_raw_data(grid_data["grid_id"], grid_data["grid_x"], grid_data["grid_y"]) | ||
| 43 | raw_forecast = library.get_raw_forecast(grid_data["grid_id"], grid_data["grid_x"], grid_data["grid_y"]) | ||
| 44 | data = library.daily_forecast(raw_data, raw_forecast, days) | ||
| 45 | 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 @@ | |||
| 1 | import flask_wtf, wtforms | ||
| 2 | |||
| 3 | class WeatherForm(flask_wtf.FlaskForm): | ||
| 4 | location = wtforms.StringField("Location", validators=[wtforms.validators.DataRequired()]) | ||
| 5 | days = wtforms.SelectField("Days", choices=[('1','1'),('2','2'),('3','3'),('4','4'),('5','5'),('6','6'),('7','7')]) | ||
| 6 | forecast_type = wtforms.RadioField("Type", choices=[('hourly', 'Hourly Forecast'),('daily','Daily Forecast')], default="hourly") | ||
| 7 | submit = wtforms.SubmitField("Submit") | ||
| 8 | |||
| @@ -23,12 +23,14 @@ def get_grid_data(latitude, longitude): | |||
| 23 | 23 | ||
| 24 | 24 | ||
| 25 | def get_raw_data(grid_id, grid_x, grid_y): | 25 | def get_raw_data(grid_id, grid_x, grid_y): |
| 26 | raw_data = json.loads(requests.get(f"https://api.weather.gov/gridpoints/{grid_id}/{grid_x},{grid_y}").text) | 26 | headers = {"User-Agent": "pywttr 0.1"} |
| 27 | raw_data = json.loads(requests.get(f"https://api.weather.gov/gridpoints/{grid_id}/{grid_x},{grid_y}", headers=headers).text) | ||
| 27 | return raw_data | 28 | return raw_data |
| 28 | 29 | ||
| 29 | 30 | ||
| 30 | def get_raw_forecast(grid_id, grid_x, grid_y): | 31 | def get_raw_forecast(grid_id, grid_x, grid_y): |
| 31 | 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) | 32 | headers = {"User-Agent": "pywttr 0.1"} |
| 33 | raw_data = json.loads(requests.get(f"https://api.weather.gov/gridpoints/{grid_id}/{grid_x},{grid_y}/forecast", headers=headers).text) | ||
| 32 | return raw_data | 34 | return raw_data |
| 33 | 35 | ||
| 34 | 36 | ||
| @@ -136,11 +138,22 @@ def get_daily_lows(raw_data): | |||
| 136 | time_str, duration_str = low["validTime"].split('/') | 138 | time_str, duration_str = low["validTime"].split('/') |
| 137 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | 139 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") |
| 138 | duration = parse_duration(duration_str) | 140 | duration = parse_duration(duration_str) |
| 139 | daily_lows.append({"time":time,"duration":duration,"low_celc":low_celc,"low_fahr":low_far}) | 141 | daily_lows.append({"time":time,"duration":duration,"low_celc":low_celc,"low_fahr":low_fahr}) |
| 140 | 142 | ||
| 141 | return daily_lows | 143 | return daily_lows |
| 142 | 144 | ||
| 143 | 145 | ||
| 146 | def get_daily_forecast(raw_data): | ||
| 147 | daily_forecast_raw = raw_data["properties"]["periods"] | ||
| 148 | |||
| 149 | daily_forecast = [] | ||
| 150 | for point in daily_forecast_raw: | ||
| 151 | time_str = point["startTime"] | ||
| 152 | time = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%S%z") | ||
| 153 | daily_forecast.append({"time":time, "short_forecast":point["shortForecast"], "detailed_forecast":point["detailedForecast"]}) | ||
| 154 | return make_current(set_timezone(daily_forecast)) | ||
| 155 | |||
| 156 | |||
| 144 | def get_temperature(raw_data): | 157 | def get_temperature(raw_data): |
| 145 | raw_values = raw_data["properties"]["temperature"]["values"] | 158 | raw_values = raw_data["properties"]["temperature"]["values"] |
| 146 | ret = [] | 159 | ret = [] |
| @@ -334,11 +347,9 @@ def get_hourly_data(raw_data, end_time): | |||
| 334 | precip_amount = get_precip_amount(raw_data) | 347 | precip_amount = get_precip_amount(raw_data) |
| 335 | wind_speed = get_wind_speed(raw_data) | 348 | wind_speed = get_wind_speed(raw_data) |
| 336 | wind_direction = get_wind_direction(raw_data) | 349 | wind_direction = get_wind_direction(raw_data) |
| 337 | 350 | ret_list = [] | |
| 338 | ret = [] | ||
| 339 | i = 0 | 351 | i = 0 |
| 340 | while i < len(temps) and temps[i]["time"] < end_time: | 352 | while i < len(temps) and temps[i]["time"] < end_time: |
| 341 | |||
| 342 | if i >= len(temps): | 353 | if i >= len(temps): |
| 343 | temps.append({"value":"N/A"}) | 354 | temps.append({"value":"N/A"}) |
| 344 | if i >= len(humidity): | 355 | if i >= len(humidity): |
| @@ -352,8 +363,6 @@ def get_hourly_data(raw_data, end_time): | |||
| 352 | if i >= len(wind_direction): | 363 | if i >= len(wind_direction): |
| 353 | wind_direction.append({"value":"N/A"}) | 364 | wind_direction.append({"value":"N/A"}) |
| 354 | 365 | ||
| 355 | |||
| 356 | |||
| 357 | val_dict = { "time": temps[i]["time"], | 366 | val_dict = { "time": temps[i]["time"], |
| 358 | "temp": temps[i]["value_fahr"], | 367 | "temp": temps[i]["value_fahr"], |
| 359 | "humidity": humidity[i]["value"], | 368 | "humidity": humidity[i]["value"], |
| @@ -361,7 +370,46 @@ def get_hourly_data(raw_data, end_time): | |||
| 361 | "precip_amount": precip_amount[i]["value"], | 370 | "precip_amount": precip_amount[i]["value"], |
| 362 | "wind_speed": wind_speed[i]["value"], | 371 | "wind_speed": wind_speed[i]["value"], |
| 363 | "wind_direction": wind_direction[i]["value"] } | 372 | "wind_direction": wind_direction[i]["value"] } |
| 364 | ret.append(val_dict) | 373 | ret_list.append(val_dict) |
| 365 | i+=1 | 374 | i+=1 |
| 366 | 375 | ||
| 367 | return ret | 376 | return ret_list |
| 377 | |||
| 378 | |||
| 379 | def get_daily_data(raw_data, raw_forecast, end_time): | ||
| 380 | daily_highs = get_daily_highs(raw_data) | ||
| 381 | daily_lows = get_daily_lows(raw_data) | ||
| 382 | daily_forecasts = get_daily_forecast(raw_forecast) | ||
| 383 | ret_list = [] | ||
| 384 | i = 0 | ||
| 385 | while i < len(daily_highs) and daily_highs[i]["time"] < end_time: | ||
| 386 | val_dict = { "time": daily_highs[i]["time"], | ||
| 387 | "high": daily_highs[i]["high_fahr"], | ||
| 388 | "low": daily_lows[i]["low_fahr"], | ||
| 389 | "short_forecast_am": daily_forecasts[i]["short_forecast"], | ||
| 390 | "detailed_forecast_am": daily_forecasts[i]["detailed_forecast"], | ||
| 391 | "short_forecast_pm": daily_forecasts[i+1]["short_forecast"], | ||
| 392 | "detailed_forecast_pm": daily_forecasts[i+1]["detailed_forecast"] } | ||
| 393 | ret_list.append(val_dict) | ||
| 394 | i+=1 | ||
| 395 | return ret_list | ||
| 396 | |||
| 397 | |||
| 398 | def hourly_forecast(raw_data, days): | ||
| 399 | init_time = get_current_rounded_time() | ||
| 400 | if days > 0: | ||
| 401 | delta = datetime.timedelta(days=days) | ||
| 402 | else: | ||
| 403 | delta = datetime.timedelta(hours=(24-init_time.hour)) | ||
| 404 | end_time = init_time + delta | ||
| 405 | return get_hourly_data(raw_data, end_time) | ||
| 406 | |||
| 407 | |||
| 408 | def daily_forecast(raw_data, raw_forecast, days): | ||
| 409 | init_time = get_current_rounded_time() | ||
| 410 | if days > 0: | ||
| 411 | delta = datetime.timedelta(days=days) | ||
| 412 | else: | ||
| 413 | delta = datetime.timedelta(days=5) | ||
| 414 | end_time = init_time + delta | ||
| 415 | 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 @@ | |||
| 1 | #!/usr/bin/env python3 | ||
| 2 | import json, requests, datetime, argparse, pytz | ||
| 3 | |||
| 4 | import library | ||
| 5 | |||
| 6 | def parse_args(): | ||
| 7 | parser = argparse.ArgumentParser() | ||
| 8 | parser.add_argument('-d', '--days', type=int) | ||
| 9 | parser.add_argument('-H', '--hours', type=int) | ||
| 10 | parser.add_argument('-l', '--location', type=str, required=True) | ||
| 11 | return parser.parse_args() | ||
| 12 | |||
| 13 | |||
| 14 | def hourly_forecast(args, raw_data): | ||
| 15 | |||
| 16 | init_time = library.get_current_rounded_time() | ||
| 17 | if args.hours is not None: | ||
| 18 | delta = datetime.timedelta(hours=args.hours) | ||
| 19 | elif args.days is not None: | ||
| 20 | delta = datetime.timedelta(days=args.days) | ||
| 21 | else: | ||
| 22 | delta = datetime.timedelta(hours=(24-init_time.hour)) | ||
| 23 | end_time = init_time + delta | ||
| 24 | |||
| 25 | hourly_data = library.get_hourly_data(raw_data, end_time) | ||
| 26 | |||
| 27 | for point in hourly_data: | ||
| 28 | print(point["time"].strftime("%a %x %I:%M %p")) | ||
| 29 | print(f'\t Temperature - { point["temp"] }°F') | ||
| 30 | print(f'\t Humidity - { point["humidity"] }%') | ||
| 31 | print(f'\t Chance of Precipitation - { point["precip_chance"] }%') | ||
| 32 | print(f'\t Precipitation Amount - { point["precip_amount"] } in') | ||
| 33 | print(f'\t Wind Speed - { point["wind_speed"] } MPH') | ||
| 34 | print(f'\t Wind Direction - { point["wind_direction"] }') | ||
| 35 | print('\n') | ||
| 36 | |||
| 37 | |||
| 38 | |||
| 39 | def daily_forecast(raw_data): | ||
| 40 | pass | ||
| 41 | |||
| 42 | |||
| 43 | def main(): | ||
| 44 | args = parse_args() | ||
| 45 | latitude, longitude = library.get_lat_long(args.location) | ||
| 46 | grid_data = library.get_grid_data(latitude, longitude) | ||
| 47 | raw_data = library.get_raw_data(grid_data["grid_id"], grid_data["grid_x"], grid_data["grid_y"]) | ||
| 48 | hourly_forecast(args, raw_data) | ||
| 49 | |||
| 50 | |||
| 51 | |||
| 52 | if __name__ == '__main__': | ||
| 53 | 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 @@ | |||
| 1 | @charset "UTF-8"; | ||
| 2 | |||
| 3 | :root { | ||
| 4 | /* Set sans-serif & mono fonts */ | ||
| 5 | --sans-font: Inter, Lato,Helvetica,"IBM Plex Sans","Roboto","Nimbus Sans L","Noto Sans", "Segoe UI",Arial,Helvetica,"Helvetica Neue",sans-serif; | ||
| 6 | --mono-font: "mononoki Nerd Font","IBM Plex Mono","Roboto Mono","Ubuntu Mono","Fira Code","Overpass Mono", Monaco,"Droid Sans Mono",monospace; | ||
| 7 | --bg: #242933; | ||
| 8 | --accent-bg: rgb(46, 52, 64); | ||
| 9 | --text: #eceff4; | ||
| 10 | --text-light: #d8dee9; | ||
| 11 | --border: #88c0d0; | ||
| 12 | --accent: #81a1c1; | ||
| 13 | --accent-light: #bf616a; | ||
| 14 | --code: #ebcb8b; | ||
| 15 | --alert: #a3be8c; | ||
| 16 | --alert-bg: #8fbcbb; | ||
| 17 | --code-bg: #2e3440; | ||
| 18 | } | ||
| 19 | |||
| 20 | |||
| 21 | html, body, footer { | ||
| 22 | background: var(--bg); | ||
| 23 | color: var(--text); | ||
| 24 | font-family: var(--sans-font); | ||
| 25 | justify-content: center; | ||
| 26 | align-items: center; | ||
| 27 | display: flex; | ||
| 28 | } | ||
| 29 | |||
| 30 | div.weather-box { | ||
| 31 | height: 100%; | ||
| 32 | align-items: center; | ||
| 33 | display: inline-flex; | ||
| 34 | } | ||
| 35 | |||
| 36 | div.daily-box { | ||
| 37 | text-align: center; | ||
| 38 | padding: 1em; | ||
| 39 | border: 0.5em solid var(--acent-bg); | ||
| 40 | border-radius: 5%; | ||
| 41 | box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.24); | ||
| 42 | box-sizing: border-box; | ||
| 43 | width: 20em; | ||
| 44 | height: 10em; | ||
| 45 | overflow-wrap: normal; | ||
| 46 | float: left; | ||
| 47 | } | ||
| 48 | |||
| 49 | div.hourly-box { | ||
| 50 | text-align: center; | ||
| 51 | padding: 1em; | ||
| 52 | border: 0.5em solid var(--acent-bg); | ||
| 53 | border-radius: 5%; | ||
| 54 | box-shadow: 0 1px 6px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.24); | ||
| 55 | box-sizing: border-box; | ||
| 56 | width: 20em; | ||
| 57 | height: 10em; | ||
| 58 | overflow-wrap: normal; | ||
| 59 | } | ||
| 60 | |||
| 61 | |||
| 62 | /*=== Links */ | ||
| 63 | a { | ||
| 64 | color: var(--accent); | ||
| 65 | } | ||
| 66 | |||
| 67 | a:hover { | ||
| 68 | color: var(--accent); | ||
| 69 | } | ||
| 70 | |||
| 71 | input, select, textarea { | ||
| 72 | margin: 5px; | ||
| 73 | padding: 5px; | ||
| 74 | color: var(--text); | ||
| 75 | border: 1px solid var(--border); | ||
| 76 | border-radius: 6px; | ||
| 77 | border-color: var(--border); | ||
| 78 | background-color: var(--bg); | ||
| 79 | min-height: 25px; | ||
| 80 | line-height: 25px; | ||
| 81 | vertical-align: middle; | ||
| 82 | } | ||
| 83 | |||
| 84 | input:disabled, select:disabled { | ||
| 85 | color: #aaa; | ||
| 86 | border-color: var(--border); | ||
| 87 | } | ||
| 88 | |||
| 89 | button { | ||
| 90 | font-family: var(--sans-font); | ||
| 91 | } | ||
| 92 | |||
| 93 | button.as-link, | ||
| 94 | button.as-link:hover, | ||
| 95 | button.as-link:active { | ||
| 96 | background: transparent; | ||
| 97 | /* background-color: var(--bg);A*/ | ||
| 98 | } | ||
| 99 | |||
| 100 | button.as-link[disabled] { | ||
| 101 | color: #ddd !important; | ||
| 102 | } | ||
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 @@ | |||
| 1 | <!DOCTYPE html> | ||
| 2 | <html> | ||
| 3 | <head> | ||
| 4 | <meta charset="utf-8"> | ||
| 5 | <title>{% block title %} {% endblock %}</title> | ||
| 6 | <link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}"> | ||
| 7 | <link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='style.css' ) }}"> | ||
| 8 | </head> | ||
| 9 | <body> | ||
| 10 | |||
| 11 | <div class="container"> | ||
| 12 | {% block content %} {% endblock %} | ||
| 13 | </div> | ||
| 14 | |||
| 15 | <footer> | ||
| 16 | {% block footer %} | ||
| 17 | {% endblock %} | ||
| 18 | </footer> | ||
| 19 | {% block scripts %}{% endblock %} | ||
| 20 | </body> | ||
| 21 | </html> | ||
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 @@ | |||
| 1 | {% extends 'base.html' %} | ||
| 2 | {% block content %} | ||
| 3 | <div class=weather-box> | ||
| 4 | {% for item in data %} | ||
| 5 | <div class=daily-box> | ||
| 6 | {{ item.time.strftime("%a %x") }} <br> | ||
| 7 | {{ item.high }}°F / {{ item.low }}°F <br> | ||
| 8 | {{ item.short_forecast_am }} <br><br> | ||
| 9 | </div> | ||
| 10 | {% endfor %} | ||
| 11 | </div> | ||
| 12 | {% endblock %} | ||
| 13 | |||
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 @@ | |||
| 1 | {% extends 'base.html' %} | ||
| 2 | |||
| 3 | {% block content %} | ||
| 4 | <div class=weather-box> | ||
| 5 | {% for item in data %} | ||
| 6 | <div class=daily-box> | ||
| 7 | {{ item.time.strftime("%a %x %I:%M %p") }} <br> | ||
| 8 | {{ item.temp }}°F <br> | ||
| 9 | {{ item.humidity }}%<br> | ||
| 10 | {{ item.precip_chance }}%<br> | ||
| 11 | {{ item.precip_amount }}in<br> | ||
| 12 | {{ item.wind_speed }}MPH<br> | ||
| 13 | {{ item.wind_direction }}<br><br> | ||
| 14 | </div> | ||
| 15 | {% endfor %} | ||
| 16 | </div> | ||
| 17 | {% endblock %} | ||
| 18 | |||
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 @@ | |||
| 1 | {% extends 'base.html' %} | ||
| 2 | |||
| 3 | {% block content %} | ||
| 4 | <h1>{% block title %} Enter a Location {% endblock %}</h1> | ||
| 5 | <form method="post"> | ||
| 6 | {{ form.csrf_token }} | ||
| 7 | <p> | ||
| 8 | {{ form.location.label }} <br> | ||
| 9 | {{ form.location }} | ||
| 10 | </p> | ||
| 11 | <p> | ||
| 12 | {{ form.days.label }} <br> | ||
| 13 | {{ form.days }} | ||
| 14 | </p> | ||
| 15 | <p> | ||
| 16 | {{ form.forecast_type.label }} <br> | ||
| 17 | {{ form.forecast_type }} | ||
| 18 | </p> | ||
| 19 | <p>{{ form.submit() }}</p> | ||
| 20 | </form> | ||
| 21 | {% endblock %} | ||
| 22 | |||
| 23 | <form method="POST" action="/"> | ||
| 24 | {{ form.name.label }} {{ form.name(size=20) }} | ||
| 25 | <input type="submit" value="Go"> | ||
| 26 | </form> | ||
