diff options
author | Sam Chudnick <sam@chudnick.com> | 2023-06-04 17:47:00 -0400 |
---|---|---|
committer | Sam Chudnick <sam@chudnick.com> | 2023-06-04 17:47:00 -0400 |
commit | 52cd654f581e7986de524a97e9ae61bf38324228 (patch) | |
tree | 7c7d589d39a3ca010278b9a0aaefa2cd5dcf3797 | |
parent | c023b5704d49679989ddae60851b82f051f4ac2f (diff) |
Started turning into flask app
-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> | ||