Compare commits

...

9 Commits

15 changed files with 136 additions and 89 deletions

View File

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

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,6 +20,10 @@ def create_app(test_config=None):
except OSError:
pass
@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)

View File

@@ -10,12 +10,8 @@ bp = Blueprint('api', __name__)
@bp.route('/api')
def api():
db = get_db()
latest_period = dict(db.execute(
'SELECT `id` FROM `reports` WHERE `type` = "hourly" ORDER BY `reported_at` DESC'
).fetchone())
current_conditions = dict(db.execute(
f"SELECT * FROM `periods` WHERE `report_id` = {latest_period['id']} LIMIT 1"
"SELECT * FROM `current_forecasts` WHERE DATETIME('now', 'localtime') BETWEEN `start_time` AND `end_time` LIMIT 1"
).fetchone())
return current_conditions

View File

@@ -3,7 +3,9 @@ import json
import requests
from datetime import datetime
from weather.db import get_db
from weather.db import (
get_db, close_db
)
def fetchHourlyForecasts():
try:
@@ -15,15 +17,16 @@ def fetchHourlyForecasts():
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)
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"
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`
(
@@ -44,24 +47,25 @@ def fetchHourlyForecasts():
)
"""
for period_report in response.json()['properties']['periods']:
for forecast in response.json()['properties']['periods']:
# TODO: this should be a transaction with rollback, pretty hacky right now
db.execute(insert_sql, (
datetime.strptime(period_report['startTime'], "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%d %H:%M:%S"),
datetime.strptime(period_report['endTime'], "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%d %H:%M:%S"),
period_report['isDaytime'],
period_report['temperature'],
period_report['probabilityOfPrecipitation']['value'],
period_report['relativeHumidity']['value'],
period_report['windSpeed'],
period_report['windDirection'],
period_report['icon'],
period_report['shortForecast'],
period_report['detailedForecast'],
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)
@@ -77,14 +81,15 @@ def fetchDailyForecasts():
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"
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`
(
@@ -99,18 +104,19 @@ def fetchDailyForecasts():
)
"""
for period_report in response.json()['properties']['periods']:
if period_report['isDaytime']:
for forecast in response.json()['properties']['periods']:
if forecast['isDaytime']:
# TODO: this should be a transaction with rollback
db.execute(insert_sql, (
datetime.strptime(period_report['startTime'], "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%d %H:%M:%S"),
period_report['temperature'],
period_report['probabilityOfPrecipitation']['value'],
period_report['icon'],
period_report['shortForecast'],
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)

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

View File

@@ -46,8 +46,7 @@
grid-template-areas:
"shortDescription"
"longDescription"
"currentTemp"
"waterConditions";
"currentTemp";
grid-area: forecast;
}
@@ -62,10 +61,9 @@
}
.currentTemp { grid-area: currentTemp; }
.currentTemp > .temperature { font-size: 6em; }
.currentTemp > .temperature { font-size: 6em; line-height: 1; }
.currentTemp > .unit { font-size: 2em; }
.waterConditions { grid-area: waterConditions; }
.secondaryInfo {
display: grid;
@@ -75,23 +73,16 @@
grid-auto-flow: row;
grid-template-areas:
"windContainer"
"solarClock";
"waterConditions";
grid-area: secondaryInfo;
}
.windContainer { grid-area: windContainer; }
.windContainer > div { font-size: 2em; }
.solarClock { grid-area: solarClock; }
.waterConditions { grid-area: waterConditions; }
.waterConditions > div { font-size: 2em; }
.hourlyReport { grid-area: hourlyReport; }
.weeklyReport {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr;
grid-template-rows: 1fr;
gap: 0px 0px;
grid-auto-flow: row;
grid-template-areas:
". . . . . .";
grid-area: weeklyReport;
}
.weeklyReport { grid-area: weeklyReport; }

View File

@@ -13,6 +13,14 @@
-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; }
@@ -25,12 +33,14 @@
.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: 1rem; }
.p-4 { padding: 1em; }
.my-6 { margin-top: 1.25rem; margin-bottom: 1.25rem; }
.font-lg { font-size: 1.25em; }

View File

@@ -20,7 +20,7 @@
<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">0:00:00 AM</div>
<div id="clock" class="text-white text-shadow">0:00:00 AM</div>
{% block content %}{% endblock %}
</div>

View File

@@ -4,50 +4,70 @@
<div class="page-container">
<div class="currentForecast">
<div class="forecast">
<div class="shortDescription">{{ current_conditions['short_forecast'] }}</div>
<div class="longDescription">{{ current_conditions['detailed_forecast'] }}</div>
<div class="currentTemp">
<span class="temperature">{{ current_conditions['temperature'] }}</span><span class="unit">°{{ current_conditions['temperature_unit'] }}</span>
<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="waterConditions">{{ current_conditions['precipitation_probability'] }}% percip | {{ current_conditions['relative_humidity'] }}% humidity (relative)</div>
<div class="font-lg text-white text-shadow">{{ current_conditions['relative_humidity'] }}% humidity</div>
</div>
<div class="secondaryInfo">
<div class="windContainer frosted">
<div class="flex items-center justify-between">
<div class="flex items-center">Wind Status</div>
<div class="flex items-center">{{ current_conditions['wind_speed'] }}</div>
</div>
<div class="flex items-center justify-center">
</div>
<div class="flex items-center justify-center">
Direction: {{ current_conditions['wind_direction'] }}
<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="solarClock frosted">
<div class="flex items-center justify-between">
<div class="flex items-center">Sunrise</div>
<div class="flex items-center">Sunset</div>
</div>
<div class="flex items-center justify-center">
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">Time AM</div>
<div class="flex items-center">Time PM</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 my-6">
<div class="hourlyForecast frosted p-4">
<h3>Hourly Forecast</h3>
<div class="grid grid-cols-6 gap-x-4">
{{ current_conditions | tojson(2) }}
{% 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">
<div class="weeklyReport frosted p-4">
<h3>Weekly Forecast</h3>
<div class="grid grid-cols-7 gap-x-4">
{% for period in periods %}
<div>{{ period['start_time'] }} - {{ period['end_time'] }} | {{ period['temperature'] }}{{ period['temperature_unit'] }}</div>
{% 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>

View File

@@ -4,7 +4,9 @@ from flask import (
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
)
@@ -51,6 +53,7 @@ def index():
'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,
@@ -61,7 +64,7 @@ def index():
def mapForecastToImage(condition: str):
if not condition:
return 'cloudy'
return 'clear'
condition = condition.lower()
if 'thunder' in condition or 'storm' in condition:
@@ -76,9 +79,9 @@ def mapForecastToImage(condition: str):
return 'clear'
elif 'cloud' in condition or 'overcast' in condition:
return 'cloudy'
elif 'fog' in condition or 'mist' in condition:
return 'cloudy'
elif 'haze' in condition or 'mist' in condition or 'fog' in condition:
return 'hazy'
else:
return 'cloudy'
return 'clear'