Compare commits

...

18 Commits

Author SHA1 Message Date
a5af30df6f adding screenshot file 2026-01-12 15:16:31 -07:00
e291f232ba adding screenshot 2026-01-12 15:16:13 -07:00
3b3d7c5247 get the correct current forecast 2026-01-12 15:14:11 -07:00
1dc30200fd adding build file 2026-01-12 15:10:06 -07:00
02d7245212 fixing API to return current conditions 2026-01-12 15:06:28 -07:00
7277a5da9d doing updating UI, adding jinja filters 2026-01-12 15:03:04 -07:00
bd31c5dbac adding hazy condition images 2026-01-12 15:02:43 -07:00
1640d30551 properly importing functions 2026-01-12 14:26:12 -07:00
123ccb68b6 fixing paths in ingest, forgot to commit statements 2026-01-12 14:24:58 -07:00
c6aa52772d update to a right mess for fetching data and doing some sanity checks 2026-01-12 14:10:56 -07:00
331d2757bf ingest data from national weather service 2026-01-12 14:10:30 -07:00
f9bcdf6ef9 fixing schema after some more thought 2026-01-12 14:10:13 -07:00
8fb0ce7e08 adding day/night to background images, added some more conditions 2026-01-12 10:46:33 -07:00
93e6485ddf moving API route to separate file 2026-01-11 18:29:00 -07:00
7b5a49979e a terrible but somewhat functional state 2026-01-11 17:50:32 -07:00
9dbd72f4ee removing some debug code 2026-01-11 10:10:58 -07:00
196873aac9 getting some ui and index route going 2026-01-10 19:04:48 -07:00
8e7512965c database migration stuff 2026-01-10 18:34:10 -07:00
42 changed files with 10235 additions and 3 deletions

View File

@@ -1,3 +1,5 @@
# Weather (Python)
Recreating a weather reporting app from Laravel to Flask
<img src="./screenshot.png" width="200" />

5008
data/2026-01-10/daily.json Normal file

File diff suppressed because it is too large Load Diff

4265
data/2026-01-10/hourly.json Normal file

File diff suppressed because it is too large Load Diff

319
data/2026-01-10/weekly.json Normal file
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 KiB

View File

@@ -1,6 +1,7 @@
import os
from flask import Flask
from datetime import datetime
def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
@@ -19,8 +20,19 @@ def create_app(test_config=None):
except OSError:
pass
@app.route('/hello')
def hello():
return "'Ello, Wurld!"
@app.template_filter('format_datetime')
def format_datetime(value, format):
return datetime.strptime(value, "%Y-%m-%d %H:%M:%S").strftime(format)
from . import db
db.init_app(app)
from . import weather
app.register_blueprint(weather.bp)
app.add_url_rule('/', endpoint='index')
from . import api
app.register_blueprint(api.bp)
app.add_url_rule('/api', endpoint='api')
return app

18
weather/api.py Normal file
View 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

46
weather/db.py Normal file
View File

@@ -0,0 +1,46 @@
import sqlite3
from datetime import datetime
import click
from flask import current_app, g
def get_db():
if 'db' not in g:
g.db = sqlite3.connect(
current_app.config['DATABASE'],
detect_types=sqlite3.PARSE_DECLTYPES
)
g.db.row_factory = sqlite3.Row
return g.db
def close_db(e=None):
db = g.pop('db', None)
if db is not None:
db.close()
def init_db():
db = get_db()
with current_app.open_resource('schema.sql') as f:
db.executescript(f.read().decode('utf8'))
@click.command('init-db')
def init_db_command():
"""Refresh database"""
init_db()
click.echo('Initialized the database.')
sqlite3.register_converter(
"timestamp", lambda v: datetime.fromisoformat(v.decode())
)
def init_app(app):
app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command)

122
weather/ingest.py Normal file
View 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)

31
weather/schema.sql Normal file
View File

@@ -0,0 +1,31 @@
DROP TABLE IF EXISTS current_forecasts;
DROP TABLE IF EXISTS daily_forecasts;
CREATE TABLE "current_forecasts" (
"id" Integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"start_time" DateTime NOT NULL,
"end_time" DateTime NOT NULL,
"is_daytime" Integer NOT NULL,
"temperature" Integer NOT NULL,
"precipitation_probability" Integer,
"relative_humidity" Integer,
"wind_speed" Text,
"wind_direction" Text,
"icon_url" Text,
"short_forecast" Text,
"detailed_forecast" Text,
"created_at" DateTime,
"updated_at" DateTime
);
CREATE TABLE "daily_forecasts" (
"id" Integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"forecasted_date" DateTime NOT NULL,
"temperature_high" Integer NOT NULL,
"precipitation_probability" Integer,
"icon_url" Text,
"short_forecast" Text,
"created_at" DateTime,
"updated_at" DateTime
);

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 MiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 KiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View 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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 KiB

View 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>

54
weather/static/reset.css Normal file
View File

@@ -0,0 +1,54 @@
/* 1. Use a more-intuitive box-sizing model */
*, *::before, *::after {
box-sizing: border-box;
}
/* 2. Remove default margin */
*:not(dialog) {
margin: 0;
}
/* 3. Enable keyword animations */
@media (prefers-reduced-motion: no-preference) {
html {
interpolate-size: allow-keywords;
}
}
body {
/* 4. Add accessible line-height */
line-height: 1.5;
/* 5. Improve text rendering */
-webkit-font-smoothing: antialiased;
}
/* 6. Improve media defaults */
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
/* 7. Inherit fonts for form controls */
input, button, textarea, select {
font: inherit;
}
/* 8. Avoid text overflows */
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
/* 9. Improve line wrapping */
p {
text-wrap: pretty;
}
h1, h2, h3, h4, h5, h6 {
text-wrap: balance;
}
/*
10. Create a root stacking context
*/
#root, #__next {
isolation: isolate;
}

88
weather/static/style.css Normal file
View File

@@ -0,0 +1,88 @@
#clock {
font-size: 2em;
padding-top: 1em;
text-align: center;
}
.frosted {
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);
}
.page-container {
display: grid;
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;
}
.currentForecast {
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; }

View 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; }

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html lang="en-US">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}{% endblock %}</title>
<!-- Font preloads (should be done for each font file) -->
<link href="{{ url_for('static', filename='Abel-Regular.woff2') }}" rel="preload" as="font" type="font/woff2" crossorigin="anonymous">
<!-- CSS -->
<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">
<!-- JS that must be executed before the document is loaded -->
</head>
<body class="font-abel antialiased">
<div id="app" class="min-h-screen" style="background-image: url({{ url_for('static', filename=condition_image) }}); background-size: cover; background-position: center;">
<div id="clock" class="text-white text-shadow">0:00:00 AM</div>
{% block content %}{% endblock %}
</div>
<script>
let clock = document.getElementById('clock');
setInterval(() => {
clock.innerText = new Date().toLocaleTimeString();
}, 1000)
</script>
</body>
</html>

View File

@@ -0,0 +1,76 @@
{% extends 'base.html' %}
{% block content %}
<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>
{% endblock %}

87
weather/weather.py Normal file
View File

@@ -0,0 +1,87 @@
from flask import (
Blueprint, render_template
)
from werkzeug.exceptions import abort
from datetime import datetime
from weather.db import (
get_db, close_db
)
from weather.ingest import (
fetchHourlyForecasts, fetchDailyForecasts
)
bp = Blueprint('weather', __name__)
@bp.route('/')
def index():
db = get_db()
current_conditions = db.execute(
"SELECT * FROM `current_forecasts` ORDER BY `end_time` DESC LIMIT 1"
).fetchone()
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()
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'