convert app to pydantic and weather.gov api
This commit is contained in:
205
app.py
Normal file
205
app.py
Normal file
@@ -0,0 +1,205 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user