.profile/app.py

206 lines
7.1 KiB
Python

import sys
from pathlib import Path
from typing import ClassVar
import pendulum
import requests
from pydantic import Field
from pydantic_settings import BaseSettings, CliApp
class App(BaseSettings):
"""Application settings for weather SVG generator."""
latitude: float = Field(
default=48.7519,
description="Latitude for weather location (default: Bellingham, WA)"
)
longitude: float = Field(
default=-122.4787,
description="Longitude for weather location (default: Bellingham, WA)"
)
grad_date: str = Field(
default="2023-12-14",
description="Graduation date in YYYY-MM-DD format"
)
template_path: Path = Field(
default=Path("template.svg"),
description="Path to template SVG file"
)
output_path: Path = Field(
default=Path("chat.svg"),
description="Path to output SVG file"
)
# Weather icon to emoji mapping (based on weather.gov icon URLs)
WEATHER_EMOJIS: ClassVar = {
"skc": "☀️", # Sky Clear
"few": "🌤", # Few Clouds
"sct": "", # Scattered Clouds
"bkn": "🌥", # Broken Clouds
"ovc": "☁️", # Overcast
"wind_skc": "💨", # Windy and Clear
"wind_few": "💨", # Windy and Few Clouds
"wind_sct": "💨", # Windy and Scattered Clouds
"wind_bkn": "💨", # Windy and Broken Clouds
"wind_ovc": "💨", # Windy and Overcast
"snow": "❄️", # Snow
"rain_snow": "🌨", # Rain/Snow
"rain_sleet": "🌨", # Rain/Sleet
"snow_sleet": "🌨", # Snow/Sleet
"fzra": "🌧", # Freezing Rain
"rain_fzra": "🌧", # Rain/Freezing Rain
"snow_fzra": "🌨", # Snow/Freezing Rain
"sleet": "🌨", # Sleet
"rain": "🌧", # Rain
"rain_showers": "🌦", # Rain Showers
"rain_showers_hi": "🌦", # Rain Showers (High)
"tsra": "", # Thunderstorm
"tsra_sct": "", # Scattered Thunderstorms
"tsra_hi": "", # Thunderstorm (High)
"tornado": "🌪", # Tornado
"hurricane": "🌀", # Hurricane
"tropical_storm": "🌀", # Tropical Storm
"dust": "🌫", # Dust
"smoke": "🌫", # Smoke
"haze": "🌫", # Haze
"hot": "🥵", # Hot
"cold": "🥶", # Cold
"blizzard": "🌨", # Blizzard
"fog": "🌫", # Fog
}
DAY_BUBBLE_WIDTHS: ClassVar = {
"Monday": 235,
"Tuesday": 235,
"Wednesday": 260,
"Thursday": 245,
"Friday": 220,
"Saturday": 245,
"Sunday": 230,
}
@staticmethod
def get_weather_icon_code(icon_url: str) -> str:
"""Extract weather condition code from weather.gov icon URL.
Example: https://api.weather.gov/icons/land/day/tsra,40?size=medium
Returns: tsra
"""
if not icon_url:
return "skc"
# Extract the condition from the URL path
parts = icon_url.split("/")
if len(parts) >= 2:
# Get the last part and remove query parameters and comma-separated values
condition = parts[-1].split("?")[0].split(",")[0]
return condition
return "skc"
@classmethod
def get_weather_emoji(cls, icon_url: str) -> str:
"""Get emoji for weather condition from icon URL."""
code = cls.get_weather_icon_code(icon_url)
return cls.WEATHER_EMOJIS.get(code, "☀️")
@staticmethod
def fetch_weather(lat: float, lon: float) -> tuple[int, int, str]:
"""Fetch weather data from weather.gov API.
Returns:
tuple: (temperature_f, temperature_c, icon_url)
"""
# Weather.gov requires a User-Agent header
headers = {
"User-Agent": "(Weather SVG Generator, contact@example.com)"
}
# First, get the grid endpoint for this location
points_url = f"https://api.weather.gov/points/{lat},{lon}"
response = requests.get(points_url, headers=headers)
response.raise_for_status()
points_data = response.json()
# Get the forecast URL
forecast_url = points_data["properties"]["forecast"]
# Fetch the forecast
forecast_response = requests.get(forecast_url, headers=headers)
forecast_response.raise_for_status()
forecast_data = forecast_response.json()
# Get today's forecast (first period)
today_forecast = forecast_data["properties"]["periods"][0]
temp_f = today_forecast["temperature"]
temp_c = round((temp_f - 32) * 5 / 9)
icon_url = today_forecast["icon"]
return temp_f, temp_c, icon_url
def cli_cmd(self) -> None:
"""Generate the weather SVG file."""
try:
# Get current date info
today = pendulum.now()
today_day = today.format("dddd")
# Calculate time since graduation
grad_date = pendulum.parse(self.grad_date)
ps_time = pendulum.instance(today).diff_for_humans(grad_date, absolute=True)
# Fetch weather data
deg_f, deg_c, icon_url = self.fetch_weather(self.latitude, self.longitude)
weather_emoji = self.get_weather_emoji(icon_url)
# Read template
template_path = self.template_path
if not template_path.is_absolute():
template_path = Path(__file__).parent / template_path
template_content = template_path.read_text(encoding="utf-8")
# Replace placeholders
output_content = template_content.replace("{degF}", str(deg_f))
output_content = output_content.replace("{degC}", str(deg_c))
output_content = output_content.replace("{weatherEmoji}", weather_emoji)
output_content = output_content.replace("{psTime}", ps_time)
output_content = output_content.replace("{todayDay}", today_day)
output_content = output_content.replace(
"{dayBubbleWidth}",
str(self.DAY_BUBBLE_WIDTHS[today_day])
)
# Write output
output_path = self.output_path
if not output_path.is_absolute():
output_path = Path(__file__).parent / output_path
output_path.write_text(output_content, encoding="utf-8")
print(f"✅ Successfully generated {output_path}")
print(f"🌡️ Temperature: {deg_f}°F ({deg_c}°C)")
print(f"☁️ Condition: {weather_emoji}")
except requests.exceptions.RequestException as err:
print(f"❌ Error fetching weather data: {err}", file=sys.stderr)
sys.exit(1)
except FileNotFoundError as err:
print(f"❌ File not found: {err}", file=sys.stderr)
sys.exit(1)
except Exception as err:
print(f"❌ Unexpected error: {err}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
CliApp.run(App)