From b4728e3dde604fd0b3f80e37ad0021895ab00c08 Mon Sep 17 00:00:00 2001 From: josh Date: Sat, 24 Jan 2026 17:20:00 -0500 Subject: [PATCH] refactor from disgusting monolith --- .gitignore | 4 + Dockerfile | 12 ++- app.py | 172 ++------------------------------------ config.py | 2 + old.py | 194 +++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 4 +- static/main.css | 38 +++++++++ static/main.js | 44 ++++++++++ templates/index.html | 25 ++++++ utils/__init__.py | 0 utils/geocode.py | 17 ++++ utils/weather.py | 43 ++++++++++ 12 files changed, 390 insertions(+), 165 deletions(-) create mode 100644 .gitignore create mode 100644 config.py create mode 100644 old.py create mode 100644 static/main.css create mode 100644 static/main.js create mode 100644 templates/index.html create mode 100644 utils/__init__.py create mode 100644 utils/geocode.py create mode 100644 utils/weather.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2d63c9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +utils/__pycache__/__init__.cpython-313.pyc +utils/__pycache__/geocode.cpython-313.pyc +utils/__pycache__/weather.cpython-313.pyc +__pycache__/config.cpython-313.pyc diff --git a/Dockerfile b/Dockerfile index bb3e50d..2f70489 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,22 @@ +# Use Python 3.12 slim image FROM python:3.12-slim +# Set working directory WORKDIR /app +# Copy and install dependencies first (cache-friendly) COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt +# Copy the rest of the app COPY app.py . +COPY config.py . +COPY utils/ ./utils +COPY templates/ ./templates +COPY static/ ./static +# Expose the Flask port EXPOSE 5000 -CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"] \ No newline at end of file +# Run the app with Gunicorn +CMD ["gunicorn", "-b", "0.0.0.0:5000", "app:app"] diff --git a/app.py b/app.py index 3f4246a..11c6af3 100644 --- a/app.py +++ b/app.py @@ -1,182 +1,28 @@ -from flask import Flask, render_template_string, jsonify, request -from datetime import datetime, date -import requests -import pytz -from timezonefinder import TimezoneFinder +from flask import Flask, render_template, jsonify, request +from utils.weather import get_today_snowfall +from utils.geocode import get_city_name +from config import MAX_SNOW_INCHES app = Flask(__name__) -MAX_SNOW_INCHES = 24 -OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast" -tf = TimezoneFinder() - -HTML = """ - - - - -Are We Buried? - - - - -
-
- - - -
-
-
-

Are We Buried?

-
-- in
-
--
-
Last updated: --
-
- - - -""" - -def get_today_snowfall(lat, lon): - try: - # Determine timezone for location - tz_name = tf.timezone_at(lat=lat, lng=lon) or "UTC" - - params = { - "latitude": lat, - "longitude": lon, - "hourly": "snowfall", - "timezone": tz_name - } - - r = requests.get(OPEN_METEO_URL, params=params, timeout=5) - r.raise_for_status() - data = r.json() - - hourly = data.get("hourly", {}) - times = hourly.get("time", []) - snow_values = hourly.get("snowfall", []) - - today_str = datetime.now(pytz.timezone(tz_name)).date().isoformat() - - snowfall_cm = 0.0 - snowing_now = False - for t, s in zip(times, snow_values): - if t.startswith(today_str): - snowfall_cm += s - if s > 0: - snowing_now = True - - inches = round(snowfall_cm / 2.54, 1) - return inches, snowing_now, tz_name - - except Exception as e: - print("Error fetching snowfall:", e) - return 0.0, False, "UTC" - -def get_city_name(lat, lon): - """Use OpenStreetMap Nominatim to get the city name for coordinates.""" - try: - r = requests.get( - "https://nominatim.openstreetmap.org/reverse", - params={ - "lat": lat, - "lon": lon, - "format": "json", - "zoom": 10, # city-level - "addressdetails": 1 - }, - headers={"User-Agent": "AreWeBuriedApp/1.0"} - ) - r.raise_for_status() - data = r.json() - address = data.get("address", {}) - city = address.get("city") or address.get("town") or address.get("village") or address.get("county") or f"{lat:.2f},{lon:.2f}" - return city - except Exception as e: - print("Reverse geocoding error:", e) - return f"{lat:.2f},{lon:.2f}" - @app.route("/") def index(): - return render_template_string(HTML) + return render_template("index.html") @app.route("/api/snowfall") def snowfall_api(): lat = request.args.get("lat", type=float) lon = request.args.get("lon", type=float) - if lat is None or lon is None: return jsonify({"error": "Missing lat/lon"}), 400 inches, snowing, tz_name = get_today_snowfall(lat, lon) - today = datetime.now(pytz.timezone(tz_name)).strftime("%B %d, %Y") - city_name = get_city_name(lat, lon) + from datetime import datetime + import pytz + today = datetime.now(pytz.timezone(tz_name)).strftime("%B %d, %Y") + return jsonify({ "location": city_name, "lat": lat, diff --git a/config.py b/config.py new file mode 100644 index 0000000..ce22a0c --- /dev/null +++ b/config.py @@ -0,0 +1,2 @@ +MAX_SNOW_INCHES = 24 +OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast" diff --git a/old.py b/old.py new file mode 100644 index 0000000..60d7554 --- /dev/null +++ b/old.py @@ -0,0 +1,194 @@ +from flask import Flask, render_template_string, jsonify, request +from datetime import datetime, date +import requests +import pytz +from timezonefinder import TimezoneFinder +from math import floor + +app = Flask(__name__) + +MAX_SNOW_INCHES = 24 +OPEN_METEO_URL = "https://api.open-meteo.com/v1/forecast" +tf = TimezoneFinder() + +HTML = """ + + + + +Are We Buried? + + + + +
+
+ + + +
+
+
+

Are We Buried?

+
-- in
+
--
+
Last updated: --
+
+ + + +""" + +def get_today_snowfall(lat, lon): + try: + # Determine timezone for location + tz_name = tf.timezone_at(lat=lat, lng=lon) or "UTC" + + params = { + "latitude": lat, + "longitude": lon, + "hourly": "snowfall", + "timezone": tz_name + } + + r = requests.get(OPEN_METEO_URL, params=params, timeout=5) + r.raise_for_status() + data = r.json() + + hourly = data.get("hourly", {}) + times = hourly.get("time", []) + snow_values = hourly.get("snowfall", []) + + today_str = datetime.now(pytz.timezone(tz_name)).date().isoformat() + + snowfall_cm = 0.0 + snowing_now = False + for t, s in zip(times, snow_values): + if t.startswith(today_str): + snowfall_cm += s + if s > 0: + snowing_now = True + + inches = round(snowfall_cm / 2.54, 1) + return inches, snowing_now, tz_name + + except Exception as e: + print("Error fetching snowfall:", e) + return 0.0, False, "UTC" + +def get_city_name(lat, lon): + """Use OpenStreetMap Nominatim to get the city name for coordinates.""" + try: + r = requests.get( + "https://nominatim.openstreetmap.org/reverse", + params={ + "lat": lat, + "lon": lon, + "format": "json", + "zoom": 10, # city-level + "addressdetails": 1 + }, + headers={"User-Agent": "AreWeBuriedApp/1.0"} + ) + r.raise_for_status() + data = r.json() + address = data.get("address", {}) + city = address.get("city") or address.get("town") or address.get("village") or address.get("county") or f"{lat:.2f},{lon:.2f}" + return city + except Exception as e: + print("Reverse geocoding error:", e) + return f"{lat:.2f},{lon:.2f}" + +@app.route("/") +def index(): + return render_template_string(HTML) + +@app.route("/api/snowfall") +def snowfall_api(): + lat = request.args.get("lat", type=float) + lon = request.args.get("lon", type=float) + + if lat is None or lon is None: + return jsonify({"error": "Missing lat/lon"}), 400 + + inches, snowing, tz_name = get_today_snowfall(lat, lon) + today = datetime.now(pytz.timezone(tz_name)).strftime("%B %d, %Y") + + city_name = get_city_name(lat, lon) + + return jsonify({ + "location": city_name, + "lat": lat, + "lon": lon, + "inches": inches, + "max_inches": MAX_SNOW_INCHES, + "snowing": snowing, + "today": today, + "updated": datetime.now().isoformat() + }) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/requirements.txt b/requirements.txt index 4287cd1..40d2675 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ flask requests -gunicorn \ No newline at end of file +gunicorn +pytz +timezonefinder diff --git a/static/main.css b/static/main.css new file mode 100644 index 0000000..dc7fbb6 --- /dev/null +++ b/static/main.css @@ -0,0 +1,38 @@ +html, body { + margin:0; padding:0; height:100%; + background:#0b1320; color:#fff; + font-family:system-ui, sans-serif; + overflow:hidden; +} + +.snowfall { position: fixed; inset:0; pointer-events:none; z-index:1; } +.flake { + position:absolute; top:-10px; width:6px; height:6px; + background:rgba(255,255,255,0.9); border-radius:50%; + animation:fall linear infinite; +} +@keyframes fall { to { transform: translateY(110vh); } } + +.snow-wrapper { + position:fixed; bottom:0; left:0; width:100%; height:0%; + transition:height 2s ease-out; z-index:2; overflow:hidden; +} +svg { position:absolute; top:-80px; width:100%; height:160px; } +.snow-body { + position:absolute; bottom:0; width:100%; height:100%; + background: + radial-gradient(circle at 20% 20%, #fff 0 2px, transparent 3px), + radial-gradient(circle at 60% 40%, #f2f8ff 0 2px, transparent 3px), + radial-gradient(circle at 80% 30%, #fff 0 2px, transparent 3px), + linear-gradient(#fff, #e6f2ff); + background-size:40px 40px,60px 60px,50px 50px,100% 100%; +} + +.container { + position:relative; z-index:3; height:100%; + display:flex; flex-direction:column; justify-content:center; + align-items:center; gap:.75rem; pointer-events:none; +} +h1 { font-size:clamp(2.5rem,6vw,4rem); margin:0; } +.amount { font-size:clamp(1.5rem,4vw,2.5rem); opacity:0.9; } +.meta { opacity:0.6; font-size:0.9rem; } diff --git a/static/main.js b/static/main.js new file mode 100644 index 0000000..bead025 --- /dev/null +++ b/static/main.js @@ -0,0 +1,44 @@ +let flakesStarted = false; + +async function loadSnow() { + if (!navigator.geolocation) return alert('Geolocation not supported'); + + navigator.geolocation.getCurrentPosition(async (pos) => { + const lat = pos.coords.latitude; + const lon = pos.coords.longitude; + + const res = await fetch(`/api/snowfall?lat=${lat}&lon=${lon}`); + const data = await res.json(); + + document.getElementById('amount').textContent = data.inches + ' inches today'; + document.getElementById('meta').textContent = data.location + ' • ' + data.today; + + const percent = Math.min(data.inches / data.max_inches, 1); + document.getElementById('snowWrapper').style.height = (percent * 100) + '%'; + + const now = new Date(); + document.getElementById('updated').textContent = + 'Last updated: ' + now.toLocaleTimeString(); + + if (data.snowing && !flakesStarted) { + spawnSnowflakes(); + flakesStarted = true; + } + }); +} + +function spawnSnowflakes() { + const container = document.getElementById('snowfall'); + for (let i = 0; i < 90; i++) { + const flake = document.createElement('div'); + flake.className = 'flake'; + flake.style.left = Math.random()*100+'vw'; + flake.style.animationDuration = 5+Math.random()*10+'s'; + flake.style.opacity = Math.random(); + flake.style.transform = `scale(${Math.random()+0.5})`; + container.appendChild(flake); + } +} + +loadSnow(); +setInterval(loadSnow, 10*60*1000); diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..f99ef8c --- /dev/null +++ b/templates/index.html @@ -0,0 +1,25 @@ + + + + +Are We Buried? + + + + +
+
+ + + +
+
+
+

Are We Buried?

+
-- in
+
--
+
Last updated: --
+
+ + + diff --git a/utils/__init__.py b/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/utils/geocode.py b/utils/geocode.py new file mode 100644 index 0000000..f9345d9 --- /dev/null +++ b/utils/geocode.py @@ -0,0 +1,17 @@ +import requests + +def get_city_name(lat, lon): + try: + r = requests.get( + "https://nominatim.openstreetmap.org/reverse", + params={"lat": lat, "lon": lon, "format": "json", "zoom": 10, "addressdetails": 1}, + headers={"User-Agent": "AreWeBuriedApp/1.0"} + ) + r.raise_for_status() + data = r.json() + address = data.get("address", {}) + city = address.get("city") or address.get("town") or address.get("village") or address.get("county") or f"{lat:.2f},{lon:.2f}" + return city + except Exception as e: + print("Reverse geocoding error:", e) + return f"{lat:.2f},{lon:.2f}" diff --git a/utils/weather.py b/utils/weather.py new file mode 100644 index 0000000..51ddd90 --- /dev/null +++ b/utils/weather.py @@ -0,0 +1,43 @@ +import requests +from datetime import datetime +import pytz +from timezonefinder import TimezoneFinder +from config import OPEN_METEO_URL + +tf = TimezoneFinder() + +def get_today_snowfall(lat, lon): + try: + tz_name = tf.timezone_at(lat=lat, lng=lon) or "UTC" + + params = { + "latitude": lat, + "longitude": lon, + "hourly": "snowfall", + "timezone": tz_name + } + + r = requests.get(OPEN_METEO_URL, params=params, timeout=5) + r.raise_for_status() + data = r.json() + + hourly = data.get("hourly", {}) + times = hourly.get("time", []) + snow_values = hourly.get("snowfall", []) + + today_str = datetime.now(pytz.timezone(tz_name)).date().isoformat() + + snowfall_cm = 0.0 + snowing_now = False + for t, s in zip(times, snow_values): + if t.startswith(today_str): + snowfall_cm += s + if s > 0: + snowing_now = True + + inches = round(snowfall_cm / 2.54, 1) + return inches, snowing_now, tz_name + + except Exception as e: + print("Error fetching snowfall:", e) + return 0.0, False, "UTC"