Compare commits
16 Commits
196873aac9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
a5af30df6f
|
|||
|
e291f232ba
|
|||
|
3b3d7c5247
|
|||
|
1dc30200fd
|
|||
|
02d7245212
|
|||
|
7277a5da9d
|
|||
|
bd31c5dbac
|
|||
|
1640d30551
|
|||
|
123ccb68b6
|
|||
|
c6aa52772d
|
|||
|
331d2757bf
|
|||
|
f9bcdf6ef9
|
|||
|
8fb0ce7e08
|
|||
|
93e6485ddf
|
|||
|
7b5a49979e
|
|||
|
9dbd72f4ee
|
@@ -1,3 +1,5 @@
|
|||||||
# Weather (Python)
|
# Weather (Python)
|
||||||
|
|
||||||
Recreating a weather reporting app from Laravel to Flask
|
Recreating a weather reporting app from Laravel to Flask
|
||||||
|
|
||||||
|
<img src="./screenshot.png" width="200" />
|
||||||
|
|||||||
5008
data/2026-01-10/daily.json
Normal file
4265
data/2026-01-10/hourly.json
Normal file
319
data/2026-01-10/weekly.json
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://geojson.org/geojson-ld/geojson-context.jsonld",
|
||||||
|
{
|
||||||
|
"@version": "1.1",
|
||||||
|
"wx": "https://api.weather.gov/ontology#",
|
||||||
|
"geo": "http://www.opengis.net/ont/geosparql#",
|
||||||
|
"unit": "http://codes.wmo.int/common/unit/",
|
||||||
|
"@vocab": "https://api.weather.gov/ontology#"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "Feature",
|
||||||
|
"geometry": {
|
||||||
|
"type": "Polygon",
|
||||||
|
"coordinates": [
|
||||||
|
[
|
||||||
|
[
|
||||||
|
-111.6091,
|
||||||
|
40.1254
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-111.6126,
|
||||||
|
40.1473
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-111.6411,
|
||||||
|
40.1446
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-111.6376,
|
||||||
|
40.1227
|
||||||
|
],
|
||||||
|
[
|
||||||
|
-111.6091,
|
||||||
|
40.1254
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"units": "us",
|
||||||
|
"forecastGenerator": "BaselineForecastGenerator",
|
||||||
|
"generatedAt": "2026-01-11T01:15:56+00:00",
|
||||||
|
"updateTime": "2026-01-10T21:04:44+00:00",
|
||||||
|
"validTimes": "2026-01-10T15:00:00+00:00/P7DT10H",
|
||||||
|
"elevation": {
|
||||||
|
"unitCode": "wmoUnit:m",
|
||||||
|
"value": 1399.9464
|
||||||
|
},
|
||||||
|
"periods": [
|
||||||
|
{
|
||||||
|
"number": 1,
|
||||||
|
"name": "Tonight",
|
||||||
|
"startTime": "2026-01-10T18:00:00-07:00",
|
||||||
|
"endTime": "2026-01-11T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 21,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "SE",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/few?size=medium",
|
||||||
|
"shortForecast": "Mostly Clear",
|
||||||
|
"detailedForecast": "Mostly clear, with a low around 21. Southeast wind around 2 mph."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 2,
|
||||||
|
"name": "Sunday",
|
||||||
|
"startTime": "2026-01-11T06:00:00-07:00",
|
||||||
|
"endTime": "2026-01-11T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 43,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "S",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/sct?size=medium",
|
||||||
|
"shortForecast": "Mostly Sunny",
|
||||||
|
"detailedForecast": "Mostly sunny. High near 43, with temperatures falling to around 39 in the afternoon. South wind around 2 mph."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 3,
|
||||||
|
"name": "Sunday Night",
|
||||||
|
"startTime": "2026-01-11T18:00:00-07:00",
|
||||||
|
"endTime": "2026-01-12T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 22,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "E",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/few/haze?size=medium",
|
||||||
|
"shortForecast": "Mostly Clear then Haze",
|
||||||
|
"detailedForecast": "Haze after 5am. Mostly clear. Low around 22, with temperatures rising to around 24 overnight. East wind around 2 mph."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 4,
|
||||||
|
"name": "Monday",
|
||||||
|
"startTime": "2026-01-12T06:00:00-07:00",
|
||||||
|
"endTime": "2026-01-12T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 45,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "NW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/haze?size=medium",
|
||||||
|
"shortForecast": "Haze",
|
||||||
|
"detailedForecast": "Haze. Sunny, with a high near 45. Northwest wind around 2 mph."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 5,
|
||||||
|
"name": "Monday Night",
|
||||||
|
"startTime": "2026-01-12T18:00:00-07:00",
|
||||||
|
"endTime": "2026-01-13T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 24,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "ENE",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/haze?size=medium",
|
||||||
|
"shortForecast": "Haze",
|
||||||
|
"detailedForecast": "Haze. Mostly clear, with a low around 24. East northeast wind around 2 mph."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 6,
|
||||||
|
"name": "Tuesday",
|
||||||
|
"startTime": "2026-01-13T06:00:00-07:00",
|
||||||
|
"endTime": "2026-01-13T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 40,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "WNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/haze?size=medium",
|
||||||
|
"shortForecast": "Haze",
|
||||||
|
"detailedForecast": "Haze. Mostly sunny, with a high near 40."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 7,
|
||||||
|
"name": "Tuesday Night",
|
||||||
|
"startTime": "2026-01-13T18:00:00-07:00",
|
||||||
|
"endTime": "2026-01-14T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 22,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "E",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/haze?size=medium",
|
||||||
|
"shortForecast": "Haze",
|
||||||
|
"detailedForecast": "Haze. Partly cloudy, with a low around 22."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 8,
|
||||||
|
"name": "Wednesday",
|
||||||
|
"startTime": "2026-01-14T06:00:00-07:00",
|
||||||
|
"endTime": "2026-01-14T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 42,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "WNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/haze?size=medium",
|
||||||
|
"shortForecast": "Haze",
|
||||||
|
"detailedForecast": "Haze. Sunny, with a high near 42."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 9,
|
||||||
|
"name": "Wednesday Night",
|
||||||
|
"startTime": "2026-01-14T18:00:00-07:00",
|
||||||
|
"endTime": "2026-01-15T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 24,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "ESE",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/haze?size=medium",
|
||||||
|
"shortForecast": "Haze",
|
||||||
|
"detailedForecast": "Haze. Clear, with a low around 24."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 10,
|
||||||
|
"name": "Thursday",
|
||||||
|
"startTime": "2026-01-15T06:00:00-07:00",
|
||||||
|
"endTime": "2026-01-15T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 40,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "WNW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/haze?size=medium",
|
||||||
|
"shortForecast": "Haze",
|
||||||
|
"detailedForecast": "Haze. Sunny, with a high near 40."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 11,
|
||||||
|
"name": "Thursday Night",
|
||||||
|
"startTime": "2026-01-15T18:00:00-07:00",
|
||||||
|
"endTime": "2026-01-16T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 18,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "E",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/haze?size=medium",
|
||||||
|
"shortForecast": "Haze",
|
||||||
|
"detailedForecast": "Haze. Clear, with a low around 18."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 12,
|
||||||
|
"name": "Friday",
|
||||||
|
"startTime": "2026-01-16T06:00:00-07:00",
|
||||||
|
"endTime": "2026-01-16T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 40,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "N",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/haze?size=medium",
|
||||||
|
"shortForecast": "Haze",
|
||||||
|
"detailedForecast": "Haze. Sunny, with a high near 40."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 13,
|
||||||
|
"name": "Friday Night",
|
||||||
|
"startTime": "2026-01-16T18:00:00-07:00",
|
||||||
|
"endTime": "2026-01-17T06:00:00-07:00",
|
||||||
|
"isDaytime": false,
|
||||||
|
"temperature": 22,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "ENE",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/night/haze/few?size=medium",
|
||||||
|
"shortForecast": "Haze then Mostly Clear",
|
||||||
|
"detailedForecast": "Haze before 11pm. Mostly clear, with a low around 22."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"number": 14,
|
||||||
|
"name": "Saturday",
|
||||||
|
"startTime": "2026-01-17T06:00:00-07:00",
|
||||||
|
"endTime": "2026-01-17T18:00:00-07:00",
|
||||||
|
"isDaytime": true,
|
||||||
|
"temperature": 45,
|
||||||
|
"temperatureUnit": "F",
|
||||||
|
"temperatureTrend": null,
|
||||||
|
"probabilityOfPrecipitation": {
|
||||||
|
"unitCode": "wmoUnit:percent",
|
||||||
|
"value": 0
|
||||||
|
},
|
||||||
|
"windSpeed": "2 mph",
|
||||||
|
"windDirection": "SSW",
|
||||||
|
"icon": "https://api.weather.gov/icons/land/day/few?size=medium",
|
||||||
|
"shortForecast": "Sunny",
|
||||||
|
"detailedForecast": "Sunny, with a high near 45."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
12
pyproject.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
[project]
|
||||||
|
name = "weather"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "A little weather app"
|
||||||
|
dependencies = [
|
||||||
|
"flask",
|
||||||
|
"requests",
|
||||||
|
]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["flit_core<4"]
|
||||||
|
build-backend = "flit_core.buildapi"
|
||||||
BIN
screenshot.png
Normal file
|
After Width: | Height: | Size: 888 KiB |
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
def create_app(test_config=None):
|
def create_app(test_config=None):
|
||||||
app = Flask(__name__, instance_relative_config=True)
|
app = Flask(__name__, instance_relative_config=True)
|
||||||
@@ -19,9 +20,9 @@ def create_app(test_config=None):
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@app.route('/hello')
|
@app.template_filter('format_datetime')
|
||||||
def hello():
|
def format_datetime(value, format):
|
||||||
return "'Ello, Wurld!"
|
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S").strftime(format)
|
||||||
|
|
||||||
from . import db
|
from . import db
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
@@ -30,4 +31,8 @@ def create_app(test_config=None):
|
|||||||
app.register_blueprint(weather.bp)
|
app.register_blueprint(weather.bp)
|
||||||
app.add_url_rule('/', endpoint='index')
|
app.add_url_rule('/', endpoint='index')
|
||||||
|
|
||||||
|
from . import api
|
||||||
|
app.register_blueprint(api.bp)
|
||||||
|
app.add_url_rule('/api', endpoint='api')
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
18
weather/api.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
from flask import (
|
||||||
|
Blueprint
|
||||||
|
)
|
||||||
|
from werkzeug.exceptions import abort
|
||||||
|
|
||||||
|
from weather.db import get_db
|
||||||
|
|
||||||
|
bp = Blueprint('api', __name__)
|
||||||
|
|
||||||
|
@bp.route('/api')
|
||||||
|
def api():
|
||||||
|
db = get_db()
|
||||||
|
current_conditions = dict(db.execute(
|
||||||
|
"SELECT * FROM `current_forecasts` WHERE DATETIME('now', 'localtime') BETWEEN `start_time` AND `end_time` LIMIT 1"
|
||||||
|
).fetchone())
|
||||||
|
|
||||||
|
return current_conditions
|
||||||
|
|
||||||
122
weather/ingest.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from weather.db import (
|
||||||
|
get_db, close_db
|
||||||
|
)
|
||||||
|
|
||||||
|
def fetchHourlyForecasts():
|
||||||
|
try:
|
||||||
|
remote_url = 'https://api.weather.gov/gridpoints/SLC/106,146/forecast/hourly'
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Brian's Python API test (captbrogers@gmail.com)"
|
||||||
|
}
|
||||||
|
response = requests.get(remote_url, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
current_date = datetime.now()
|
||||||
|
if not os.path.exists(f"data/{current_date.strftime('%Y-%m-%d')}"):
|
||||||
|
os.makedirs(f"data/{current_date.strftime('%Y-%m-%d')}", exist_ok=True)
|
||||||
|
|
||||||
|
hourly_filename = f"data/{current_date.strftime('%Y-%m-%d')}/hourly_{current_date.strftime('%H')}.json"
|
||||||
|
with open(hourly_filename, 'w') as json_file:
|
||||||
|
json.dump(response.json(), json_file, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
cursor = db.cursor()
|
||||||
|
insert_sql = """
|
||||||
|
INSERT INTO `current_forecasts`
|
||||||
|
(
|
||||||
|
`start_time`,
|
||||||
|
`end_time`,
|
||||||
|
`is_daytime`,
|
||||||
|
`temperature`,
|
||||||
|
`precipitation_probability`,
|
||||||
|
`relative_humidity`,
|
||||||
|
`wind_speed`,
|
||||||
|
`wind_direction`,
|
||||||
|
`icon_url`,
|
||||||
|
`short_forecast`,
|
||||||
|
`detailed_forecast`,
|
||||||
|
`created_at`
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
for forecast in response.json()['properties']['periods']:
|
||||||
|
# TODO: this should be a transaction with rollback, pretty hacky right now
|
||||||
|
cursor.execute(insert_sql, (
|
||||||
|
datetime.strptime(forecast['startTime'], "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
datetime.strptime(forecast['endTime'], "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
forecast['isDaytime'],
|
||||||
|
forecast['temperature'],
|
||||||
|
forecast['probabilityOfPrecipitation']['value'],
|
||||||
|
forecast['relativeHumidity']['value'],
|
||||||
|
forecast['windSpeed'],
|
||||||
|
forecast['windDirection'],
|
||||||
|
forecast['icon'],
|
||||||
|
forecast['shortForecast'],
|
||||||
|
forecast['detailedForecast'],
|
||||||
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
close_db()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return str(e)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def fetchDailyForecasts():
|
||||||
|
try:
|
||||||
|
remote_url = 'https://api.weather.gov/gridpoints/SLC/106,146/forecast'
|
||||||
|
headers = {
|
||||||
|
"User-Agent": "Brian's Python API test (captbrogers@gmail.com)"
|
||||||
|
}
|
||||||
|
response = requests.get(remote_url, headers=headers)
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
current_date = datetime.now()
|
||||||
|
if not os.path.exists(f"data/{current_date.strftime('%Y-%m-%d')}"):
|
||||||
|
os.makedirs(f"data/{current_date.strftime('%Y-%m-%d')}", exist_ok=True)
|
||||||
|
|
||||||
|
daily_filename = f"data/{current_date.strftime('%Y-%m-%d')}/daily_{current_date.strftime('%H')}.json"
|
||||||
|
with open(daily_filename, 'w') as json_file:
|
||||||
|
json.dump(response.json(), json_file, indent=4)
|
||||||
|
|
||||||
|
db = get_db()
|
||||||
|
cursor = db.cursor()
|
||||||
|
insert_sql = """
|
||||||
|
INSERT INTO `daily_forecasts`
|
||||||
|
(
|
||||||
|
`forecasted_date`,
|
||||||
|
`temperature_high`,
|
||||||
|
`precipitation_probability`,
|
||||||
|
`icon_url`,
|
||||||
|
`short_forecast`,
|
||||||
|
`created_at`
|
||||||
|
) VALUES (
|
||||||
|
?, ?, ?, ?, ?, ?
|
||||||
|
)
|
||||||
|
"""
|
||||||
|
|
||||||
|
for forecast in response.json()['properties']['periods']:
|
||||||
|
if forecast['isDaytime']:
|
||||||
|
# TODO: this should be a transaction with rollback
|
||||||
|
cursor.execute(insert_sql, (
|
||||||
|
datetime.strptime(forecast['startTime'], "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
forecast['temperature'],
|
||||||
|
forecast['probabilityOfPrecipitation']['value'],
|
||||||
|
forecast['icon'],
|
||||||
|
forecast['shortForecast'],
|
||||||
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
close_db()
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
return str(e)
|
||||||
@@ -1,18 +1,13 @@
|
|||||||
DROP TABLE IF EXISTS periods;
|
DROP TABLE IF EXISTS current_forecasts;
|
||||||
DROP TABLE IF EXISTS reports;
|
DROP TABLE IF EXISTS daily_forecasts;
|
||||||
|
|
||||||
CREATE TABLE "periods" (
|
CREATE TABLE "current_forecasts" (
|
||||||
"id" Integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
"id" Integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
"report_id" Integer NOT NULL,
|
|
||||||
"period_number" Integer NOT NULL,
|
|
||||||
"name" Text,
|
|
||||||
"start_time" DateTime NOT NULL,
|
"start_time" DateTime NOT NULL,
|
||||||
"end_time" DateTime NOT NULL,
|
"end_time" DateTime NOT NULL,
|
||||||
"is_daytime" Integer NOT NULL,
|
"is_daytime" Integer NOT NULL,
|
||||||
"temperature" Integer NOT NULL,
|
"temperature" Integer NOT NULL,
|
||||||
"temperature_unit" Text NOT NULL DEFAULT 'F',
|
|
||||||
"precipitation_probability" Integer,
|
"precipitation_probability" Integer,
|
||||||
"dewpoint_celsius" Numeric,
|
|
||||||
"relative_humidity" Integer,
|
"relative_humidity" Integer,
|
||||||
"wind_speed" Text,
|
"wind_speed" Text,
|
||||||
"wind_direction" Text,
|
"wind_direction" Text,
|
||||||
@@ -20,24 +15,17 @@ CREATE TABLE "periods" (
|
|||||||
"short_forecast" Text,
|
"short_forecast" Text,
|
||||||
"detailed_forecast" Text,
|
"detailed_forecast" Text,
|
||||||
"created_at" DateTime,
|
"created_at" DateTime,
|
||||||
"updated_at" DateTime,
|
"updated_at" DateTime
|
||||||
CONSTRAINT "periods_reports_CASCADE_NO ACTION_report_id_id_0" FOREIGN KEY ( "report_id" ) REFERENCES "reports"( "id" )
|
);
|
||||||
ON DELETE Cascade
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX "periods_report_id_start_time_index" ON "periods"( "report_id", "start_time" );
|
CREATE TABLE "daily_forecasts" (
|
||||||
|
|
||||||
CREATE TABLE "reports" (
|
|
||||||
"id" Integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
"id" Integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||||
"type" Text NOT NULL,
|
"forecasted_date" DateTime NOT NULL,
|
||||||
"reported_at" DateTime NOT NULL,
|
"temperature_high" Integer NOT NULL,
|
||||||
"generated_at" DateTime NOT NULL,
|
"precipitation_probability" Integer,
|
||||||
"latitude" Numeric NOT NULL,
|
"icon_url" Text,
|
||||||
"longitude" Numeric NOT NULL,
|
"short_forecast" Text,
|
||||||
"elevation_meters" Numeric,
|
|
||||||
"created_at" DateTime,
|
"created_at" DateTime,
|
||||||
"updated_at" DateTime,
|
"updated_at" DateTime
|
||||||
CONSTRAINT "check ""type"" in ('hourly', 'weekly')" CHECK ("type" in ('hourly', 'weekly')) );
|
);
|
||||||
|
|
||||||
CREATE INDEX "reports_type_reported_at_index" ON "reports"( "type", "reported_at" );
|
|
||||||
CREATE UNIQUE INDEX "reports_type_reported_at_unique" ON "reports"( "type", "reported_at" );
|
|
||||||
|
|||||||
BIN
weather/static/images/day_clear.jpg
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
1
weather/static/images/day_clear_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/couleur-1195798/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=5459972">Couleur</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=5459972">Pixabay</a>
|
||||||
BIN
weather/static/images/day_cloudy.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
1
weather/static/images/day_cloudy_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/kapa65-61253/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1494656">Karsten Paulick</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1494656">Pixabay</a>
|
||||||
BIN
weather/static/images/day_hazy.jpg
Normal file
|
After Width: | Height: | Size: 4.0 MiB |
1
weather/static/images/day_hazy_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/enrico-b-18984570/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7541217">enrico bernardis</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7541217">Pixabay</a>
|
||||||
BIN
weather/static/images/day_rainy.jpg
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
1
weather/static/images/day_rainy_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/diego_torres-1118992/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=815271">Roman Grac</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=815271">Pixabay</a>
|
||||||
BIN
weather/static/images/day_snowy.jpg
Normal file
|
After Width: | Height: | Size: 2.4 MiB |
1
weather/static/images/day_snowy_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/juncala-5111596/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7661769">Juncala</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=7661769">Pixabay</a>
|
||||||
BIN
weather/static/images/day_stormy.jpg
Normal file
|
After Width: | Height: | Size: 627 KiB |
1
weather/static/images/day_stormy_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/sethink-348886/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=399853">sethink</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=399853">Pixabay</a>
|
||||||
BIN
weather/static/images/night_clear.jpg
Normal file
|
After Width: | Height: | Size: 2.5 MiB |
1
weather/static/images/night_clear_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/medienservice-1888061/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=6558014">Nicole Pankalla</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=6558014">Pixabay</a>
|
||||||
BIN
weather/static/images/night_cloudy.jpg
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
1
weather/static/images/night_cloudy_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/pexels-2286921/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1869753">Pexels</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1869753">Pixabay</a>
|
||||||
BIN
weather/static/images/night_hazy.jpg
Normal file
|
After Width: | Height: | Size: 2.6 MiB |
1
weather/static/images/night_hazy_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/realakp-490876/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2045453">RealAKP</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=2045453">Pixabay</a>
|
||||||
BIN
weather/static/images/night_rainy.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
1
weather/static/images/night_rainy_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/photos/night-lighting-road-light-rain-777882/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1869753">Pexels</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1869753">Pixabay</a>
|
||||||
BIN
weather/static/images/night_snowy.jpg
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
1
weather/static/images/night_snowy_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/jerzygórecki-2233926/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=3373432">Jerzy</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=3373432">Pixabay</a>
|
||||||
BIN
weather/static/images/night_stormy.jpg
Normal file
|
After Width: | Height: | Size: 548 KiB |
1
weather/static/images/night_stormy_license.html
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Image by <a href="https://pixabay.com/users/oimheidi-1660138/?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1455285">Claudia Hinz</a> from <a href="https://pixabay.com//?utm_source=link-attribution&utm_medium=referral&utm_campaign=image&utm_content=1455285">Pixabay</a>
|
||||||
@@ -1,20 +1,88 @@
|
|||||||
@font-face {
|
#clock {
|
||||||
font-family: "Abel";
|
font-size: 2em;
|
||||||
src: url(Abel-Regular.woff2) format("woff2");
|
padding-top: 1em;
|
||||||
font-weight: 400;
|
text-align: center;
|
||||||
font-style: normal;
|
|
||||||
font-display: swap;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.font-abel {
|
.frosted {
|
||||||
font-family: "Abel", sans-serif;
|
backdrop-filter: blur(16px) saturate(180%);
|
||||||
|
-webkit-backdrop-filter: blur(16px) saturate(180%);
|
||||||
|
background-color: rgba(78, 86, 106, 0.75);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.125);
|
||||||
}
|
}
|
||||||
|
|
||||||
.antialiased {
|
.page-container {
|
||||||
-webkit-font-smoothing: antialiased;
|
display: grid;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1.4fr 0.6fr;
|
||||||
|
gap: 2em 0em;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-areas:
|
||||||
|
"currentForecast"
|
||||||
|
"hourlyReport"
|
||||||
|
"weeklyReport";
|
||||||
|
max-width: 72em;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 1.5em 0 2.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.min-h-screen {
|
.currentForecast {
|
||||||
min-height: 100vh;
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
grid-template-rows: 1fr;
|
||||||
|
gap: 0px 0px;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-areas:
|
||||||
|
"forecast secondaryInfo";
|
||||||
|
grid-area: currentForecast;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.forecast {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: repeat(3, max-content);
|
||||||
|
gap: 0px 0px;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-areas:
|
||||||
|
"shortDescription"
|
||||||
|
"longDescription"
|
||||||
|
"currentTemp";
|
||||||
|
grid-area: forecast;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shortDescription {
|
||||||
|
font-size: 3em;
|
||||||
|
grid-area: shortDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
.longDescription {
|
||||||
|
font-size: 1.35em;
|
||||||
|
grid-area: longDescription;
|
||||||
|
}
|
||||||
|
|
||||||
|
.currentTemp { grid-area: currentTemp; }
|
||||||
|
.currentTemp > .temperature { font-size: 6em; line-height: 1; }
|
||||||
|
.currentTemp > .unit { font-size: 2em; }
|
||||||
|
|
||||||
|
|
||||||
|
.secondaryInfo {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 1fr 1fr;
|
||||||
|
gap: 2em 0;
|
||||||
|
grid-auto-flow: row;
|
||||||
|
grid-template-areas:
|
||||||
|
"windContainer"
|
||||||
|
"waterConditions";
|
||||||
|
grid-area: secondaryInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
.windContainer { grid-area: windContainer; }
|
||||||
|
.windContainer > div { font-size: 2em; }
|
||||||
|
|
||||||
|
.waterConditions { grid-area: waterConditions; }
|
||||||
|
.waterConditions > div { font-size: 2em; }
|
||||||
|
|
||||||
|
.hourlyReport { grid-area: hourlyReport; }
|
||||||
|
|
||||||
|
.weeklyReport { grid-area: weeklyReport; }
|
||||||
|
|||||||
46
weather/static/tailwind.css
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: "Abel";
|
||||||
|
src: url(Abel-Regular.woff2) format("woff2");
|
||||||
|
font-weight: 400;
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.font-abel { font-family: "Abel", sans-serif; }
|
||||||
|
|
||||||
|
.antialiased {
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-white {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-shadow {
|
||||||
|
text-shadow: 0 2px 8px #0009,0 4px 16px #0006,0 8px 32px #0003;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-h-screen { min-height: 100vh; }
|
||||||
|
|
||||||
|
.grid { display: grid; }
|
||||||
|
|
||||||
|
.grid-cols-6 { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
||||||
|
|
||||||
|
.grid-cols-7 { grid-template-columns: repeat(7, minmax(0, 1fr)); }
|
||||||
|
|
||||||
|
.gap-x-4 { column-gap: 0.5rem; }
|
||||||
|
|
||||||
|
.flex { display: flex; }
|
||||||
|
|
||||||
|
.items-start { align-items: start; }
|
||||||
|
|
||||||
|
.items-center { align-items: center; }
|
||||||
|
|
||||||
|
.justify-between { justify-content: space-between; }
|
||||||
|
|
||||||
|
.justify-center { justify-content: center; }
|
||||||
|
|
||||||
|
.p-4 { padding: 1em; }
|
||||||
|
|
||||||
|
.font-lg { font-size: 1.25em; }
|
||||||
@@ -11,18 +11,24 @@
|
|||||||
|
|
||||||
<!-- CSS -->
|
<!-- CSS -->
|
||||||
<link href="{{ url_for('static', filename='reset.css') }}" rel="stylesheet" media="screen">
|
<link href="{{ url_for('static', filename='reset.css') }}" rel="stylesheet" media="screen">
|
||||||
|
<link href="{{ url_for('static', filename='tailwind.css') }}" rel="stylesheet" media="screen">
|
||||||
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet" media="screen">
|
<link href="{{ url_for('static', filename='style.css') }}" rel="stylesheet" media="screen">
|
||||||
|
|
||||||
<!-- JS that must be executed before the document is loaded -->
|
<!-- JS that must be executed before the document is loaded -->
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body class="font-abel antialiased">
|
<body class="font-abel antialiased">
|
||||||
<div id="app" class="min-h-screen">
|
<div id="app" class="min-h-screen" style="background-image: url({{ url_for('static', filename=condition_image) }}); background-size: cover; background-position: center;">
|
||||||
{% for message in get_flashed_messages() %}
|
|
||||||
<div class="flash">{{ message }}</div>
|
<div id="clock" class="text-white text-shadow">0:00:00 AM</div>
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
<script>
|
||||||
|
let clock = document.getElementById('clock');
|
||||||
|
setInterval(() => {
|
||||||
|
clock.innerText = new Date().toLocaleTimeString();
|
||||||
|
}, 1000)
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,9 +1,76 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
Hey what's going on on this side?
|
<div class="page-container">
|
||||||
|
<div class="currentForecast">
|
||||||
|
<div class="forecast">
|
||||||
|
<div class="shortDescription text-white text-shadow">{{ current_conditions['short_forecast'] }}</div>
|
||||||
|
<div class="longDescription text-white text-shadow">{{ current_conditions['detailed_forecast'] }}</div>
|
||||||
|
<div class="currentTemp text-white text-shadow flex items-start">
|
||||||
|
<span class="temperature">{{ current_conditions['temperature'] }}</span><span class="unit">°F</span>
|
||||||
|
</div>
|
||||||
|
<div class="font-lg text-white text-shadow">{{ current_conditions['relative_humidity'] }}% humidity</div>
|
||||||
|
</div>
|
||||||
|
<div class="secondaryInfo">
|
||||||
|
<div class="windContainer frosted p-4">
|
||||||
|
<h3>Wind Status</h3>
|
||||||
|
<div class="flex items-center justify-center gap-x-4">
|
||||||
|
<svg width="48px" height="48px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18.5 22.75C16.16 22.75 14.25 20.84 14.25 18.5V18C14.25 17.59 14.59 17.25 15 17.25C15.41 17.25 15.75 17.59 15.75 18V18.5C15.75 20.02 16.98 21.25 18.5 21.25C20.02 21.25 21.25 20.02 21.25 18.5C21.25 16.98 20.02 15.75 18.5 15.75H2C1.59 15.75 1.25 15.41 1.25 15C1.25 14.59 1.59 14.25 2 14.25H18.5C20.84 14.25 22.75 16.16 22.75 18.5C22.75 20.84 20.84 22.75 18.5 22.75Z" fill="#000"/>
|
||||||
|
<path d="M18.5 12.75H2C1.59 12.75 1.25 12.41 1.25 12C1.25 11.59 1.59 11.25 2 11.25H18.5C20.02 11.25 21.25 10.02 21.25 8.5C21.25 6.98 20.02 5.75 18.5 5.75C16.98 5.75 15.75 6.98 15.75 8.5V9C15.75 9.41 15.41 9.75 15 9.75C14.59 9.75 14.25 9.41 14.25 9V8.5C14.25 6.16 16.16 4.25 18.5 4.25C20.84 4.25 22.75 6.16 22.75 8.5C22.75 10.84 20.84 12.75 18.5 12.75Z" fill="#000"/>
|
||||||
|
<path d="M9.31 9.75109H2C1.59 9.75109 1.25 9.41109 1.25 9.00109C1.25 8.59109 1.59 8.25109 2 8.25109H9.31C10.38 8.25109 11.25 7.38109 11.25 6.31109C11.25 5.24109 10.38 4.37109 9.31 4.37109C8.24 4.37109 7.37 5.24109 7.37 6.31109V6.69109C7.37 7.10109 7.03 7.44109 6.62 7.44109C6.21 7.44109 5.87 7.11109 5.87 6.69109V6.31109C5.87 4.41109 7.41 2.87109 9.31 2.87109C11.21 2.87109 12.75 4.41109 12.75 6.31109C12.75 8.21109 11.21 9.75109 9.31 9.75109Z" fill="#000"/>
|
||||||
|
</svg>
|
||||||
|
<div class="flex items-center">{{ current_conditions['wind_speed'] }} {{ current_conditions['wind_direction'] }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="waterConditions frosted p-4">
|
||||||
|
<h3>Precipitation</h3>
|
||||||
|
<div class="flex items-center justify-center gap-x-4">
|
||||||
|
<svg width="48px" height="48px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.0001 13.3848C16.0001 14.6088 15.526 15.7828 14.6821 16.6483C14.203 17.1397 13.6269 17.5091 13 17.7364M19 13.6923C19 7.11538 12 2 12 2C12 2 5 7.11538 5 13.6923C5 15.6304 5.7375 17.4893 7.05025 18.8598C8.36301 20.2302 10.1436 20.9994 12.0001 20.9994C13.8566 20.9994 15.637 20.2298 16.9497 18.8594C18.2625 17.4889 19 15.6304 19 13.6923Z" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<div>{{ current_conditions['precipitation_probability'] }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hourlyForecast frosted p-4">
|
||||||
|
<h3>Hourly Forecast</h3>
|
||||||
|
<div class="grid grid-cols-6 gap-x-4">
|
||||||
|
{% for forecast in hourly_conditions %}
|
||||||
|
<div class="grid items-center justify-center gap-x-4">
|
||||||
|
<div>{{ forecast['start_time'] | format_datetime('%I %p') }}</div>
|
||||||
|
<div><img src="{{ forecast['icon_url'] }}" height="48px" width="48px"></div>
|
||||||
|
<div>{{ forecast['temperature'] }}°F</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg width="16px" height="16px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.0001 13.3848C16.0001 14.6088 15.526 15.7828 14.6821 16.6483C14.203 17.1397 13.6269 17.5091 13 17.7364M19 13.6923C19 7.11538 12 2 12 2C12 2 5 7.11538 5 13.6923C5 15.6304 5.7375 17.4893 7.05025 18.8598C8.36301 20.2302 10.1436 20.9994 12.0001 20.9994C13.8566 20.9994 15.637 20.2298 16.9497 18.8594C18.2625 17.4889 19 15.6304 19 13.6923Z" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<div>{{ forecast['precipitation_probability'] }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="weeklyReport frosted p-4">
|
||||||
|
<h3>Weekly Forecast</h3>
|
||||||
|
<div class="grid grid-cols-7 gap-x-4">
|
||||||
|
{% for forecast in week_forcasts %}
|
||||||
|
<div class="grid items-center justify-center gap-x-4">
|
||||||
|
<div>{{ forecast['forecasted_date'] | format_datetime('%a') }}</div>
|
||||||
|
<div><img src="{{ forecast['icon_url'] }}" height="48px" width="48px"></div>
|
||||||
|
<div>{{ forecast['temperature_high'] }}°F</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<svg width="16px" height="16px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M16.0001 13.3848C16.0001 14.6088 15.526 15.7828 14.6821 16.6483C14.203 17.1397 13.6269 17.5091 13 17.7364M19 13.6923C19 7.11538 12 2 12 2C12 2 5 7.11538 5 13.6923C5 15.6304 5.7375 17.4893 7.05025 18.8598C8.36301 20.2302 10.1436 20.9994 12.0001 20.9994C13.8566 20.9994 15.637 20.2298 16.9497 18.8594C18.2625 17.4889 19 15.6304 19 13.6923Z" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
<div>{{ forecast['precipitation_probability'] }}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% for period in periods %}
|
|
||||||
<p>{{ period['start_time'] }} - {{ period['end_time'] }} | {{ period['temperature'] }}{{ period['temperature_unit'] }}</p>
|
|
||||||
{% endfor %}
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,18 +1,87 @@
|
|||||||
from flask import (
|
from flask import (
|
||||||
Blueprint, flash, g, render_template, request, url_for
|
Blueprint, render_template
|
||||||
)
|
)
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from weather.db import get_db
|
from weather.db import (
|
||||||
|
get_db, close_db
|
||||||
|
)
|
||||||
|
from weather.ingest import (
|
||||||
|
fetchHourlyForecasts, fetchDailyForecasts
|
||||||
|
)
|
||||||
|
|
||||||
bp = Blueprint('weather', __name__)
|
bp = Blueprint('weather', __name__)
|
||||||
|
|
||||||
@bp.route('/')
|
@bp.route('/')
|
||||||
def index():
|
def index():
|
||||||
db = get_db()
|
db = get_db()
|
||||||
periods = db.execute(
|
current_conditions = db.execute(
|
||||||
'SELECT *'
|
"SELECT * FROM `current_forecasts` ORDER BY `end_time` DESC LIMIT 1"
|
||||||
' FROM `periods`'
|
).fetchone()
|
||||||
' ORDER BY `id` DESC'
|
|
||||||
|
if current_conditions is None:
|
||||||
|
fetchHourlyForecasts()
|
||||||
|
current_conditions = db.execute(
|
||||||
|
"SELECT * FROM `current_forecasts` ORDER BY `end_time` DESC LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
|
||||||
|
day_or_night = 'day'
|
||||||
|
if current_conditions['is_daytime']:
|
||||||
|
day_or_night = 'night'
|
||||||
|
|
||||||
|
condition = mapForecastToImage(current_conditions['short_forecast'])
|
||||||
|
condition_image = f"images/{day_or_night}_{condition}.jpg"
|
||||||
|
|
||||||
|
hourly_conditions = db.execute(
|
||||||
|
'SELECT * FROM `current_forecasts` WHERE `start_time` > DATETIME("now", "+1 hour") LIMIT 6'
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return render_template('weather/index.html', periods=periods)
|
if len(hourly_conditions) < 6:
|
||||||
|
fetchHourlyForecasts()
|
||||||
|
hourly_conditions = db.execute(
|
||||||
|
'SELECT * FROM `current_forecasts` WHERE `start_time` > DATETIME("now", "+1 hour") LIMIT 6'
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
|
||||||
|
week_forcasts = db.execute(
|
||||||
|
'SELECT * FROM `daily_forecasts` WHERE `forecasted_date` > DATETIME("now") LIMIT 7'
|
||||||
|
).fetchall()
|
||||||
|
if len(week_forcasts) < 7:
|
||||||
|
fetchDailyForecasts()
|
||||||
|
week_forcasts = db.execute(
|
||||||
|
'SELECT * FROM `current_forecasts` WHERE `start_time` > DATETIME("now", "+1 hour") LIMIT 7'
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
close_db()
|
||||||
|
return render_template(
|
||||||
|
'weather/index.html',
|
||||||
|
condition_image=condition_image,
|
||||||
|
current_conditions=current_conditions,
|
||||||
|
hourly_conditions=hourly_conditions,
|
||||||
|
week_forcasts=week_forcasts
|
||||||
|
)
|
||||||
|
|
||||||
|
def mapForecastToImage(condition: str):
|
||||||
|
if not condition:
|
||||||
|
return 'clear'
|
||||||
|
|
||||||
|
condition = condition.lower()
|
||||||
|
if 'thunder' in condition or 'storm' in condition:
|
||||||
|
return 'stormy'
|
||||||
|
elif 'snow' in condition:
|
||||||
|
return 'snowy'
|
||||||
|
elif 'rain' in condition or 'shower' in condition or 'drizzle' in condition:
|
||||||
|
return 'rainy'
|
||||||
|
elif 'wind' in condition:
|
||||||
|
return 'windy'
|
||||||
|
elif 'sunny' in condition or 'clear' in condition:
|
||||||
|
return 'clear'
|
||||||
|
elif 'cloud' in condition or 'overcast' in condition:
|
||||||
|
return 'cloudy'
|
||||||
|
elif 'haze' in condition or 'mist' in condition or 'fog' in condition:
|
||||||
|
return 'hazy'
|
||||||
|
else:
|
||||||
|
return 'clear'
|
||||||
|
|
||||||
|
|
||||||
|
|||||||