Compare commits

..

1 commit

Author SHA1 Message Date
2030a4f7da WIP: refactor 2025-08-30 21:44:44 -04:00

132
main.py
View file

@ -20,12 +20,14 @@ POLLEN_MAX_INDEX = 12.0
POLLEN_PERCENTAGE_SCALE = 100 POLLEN_PERCENTAGE_SCALE = 100
HTTP_TIMEOUT = 10.0 HTTP_TIMEOUT = 10.0
class WeatherCondition(TypedDict): class WeatherCondition(TypedDict):
description: str description: str
main: str main: str
id: int id: int
icon: str icon: str
class CurrentWeather(TypedDict): class CurrentWeather(TypedDict):
dt: int dt: int
sunrise: int sunrise: int
@ -36,10 +38,12 @@ class CurrentWeather(TypedDict):
humidity: int humidity: int
weather: list[WeatherCondition] weather: list[WeatherCondition]
class DailyTemperature(TypedDict): class DailyTemperature(TypedDict):
min: float min: float
max: float max: float
class DailyWeather(TypedDict): class DailyWeather(TypedDict):
dt: int dt: int
sunrise: int sunrise: int
@ -49,17 +53,21 @@ class DailyWeather(TypedDict):
pressure: int pressure: int
weather: list[WeatherCondition] weather: list[WeatherCondition]
class WeatherApiResponse(TypedDict): class WeatherApiResponse(TypedDict):
current: CurrentWeather current: CurrentWeather
daily: list[DailyWeather] daily: list[DailyWeather]
class PollenLocation(TypedDict): class PollenLocation(TypedDict):
periods: list[dict[str, Union[str, int, float]]] periods: list[dict[str, Union[str, int, float]]]
class PollenApiResponse(TypedDict): class PollenApiResponse(TypedDict):
ForecastDate: str ForecastDate: str
Location: PollenLocation Location: PollenLocation
class WeatherPeriod(TypedDict): class WeatherPeriod(TypedDict):
low: int low: int
high: int high: int
@ -70,10 +78,12 @@ class WeatherPeriod(TypedDict):
pressure: int pressure: int
period: str period: str
class PollenPeriod(TypedDict): class PollenPeriod(TypedDict):
index: int index: int
period: str period: str
class WeatherReport(TypedDict): class WeatherReport(TypedDict):
forecast_date: str forecast_date: str
current_temp: int current_temp: int
@ -85,10 +95,12 @@ class WeatherReport(TypedDict):
current_desc: str current_desc: str
periods: list[WeatherPeriod] periods: list[WeatherPeriod]
class PollenReport(TypedDict): class PollenReport(TypedDict):
forecast_date: str forecast_date: str
periods: list[PollenPeriod] periods: list[PollenPeriod]
class CurrentWeatherData(TypedDict): class CurrentWeatherData(TypedDict):
temp: int temp: int
feels_like: int feels_like: int
@ -96,6 +108,7 @@ class CurrentWeatherData(TypedDict):
humidity: int humidity: int
pressure: int pressure: int
class DailyData(TypedDict, total=False): class DailyData(TypedDict, total=False):
pollen: int pollen: int
low: int low: int
@ -106,12 +119,14 @@ class DailyData(TypedDict, total=False):
sunset: str sunset: str
pressure: int pressure: int
class FinalReport(TypedDict): class FinalReport(TypedDict):
fetched_at: str fetched_at: str
current: CurrentWeatherData current: CurrentWeatherData
today: DailyData today: DailyData
tomorrow: DailyData tomorrow: DailyData
app = FastAPI(title="TRMNL Weather & Pollen Report") app = FastAPI(title="TRMNL Weather & Pollen Report")
weather_cache: TTLCache = TTLCache(maxsize=CACHE_MAX_SIZE, ttl=CACHE_TTL_SECONDS) weather_cache: TTLCache = TTLCache(maxsize=CACHE_MAX_SIZE, ttl=CACHE_TTL_SECONDS)
@ -158,7 +173,9 @@ class DateTimeFormatter:
@staticmethod @staticmethod
def format_datetime(dt: datetime) -> str: def format_datetime(dt: datetime) -> str:
"""Format datetime as date and time string.""" """Format datetime as date and time string."""
return f"{DateTimeFormatter.format_date(dt)} {DateTimeFormatter.format_time(dt)}" return (
f"{DateTimeFormatter.format_date(dt)} {DateTimeFormatter.format_time(dt)}"
)
@staticmethod @staticmethod
def relative_day_to_date(relative_day: str) -> str: def relative_day_to_date(relative_day: str) -> str:
@ -234,18 +251,27 @@ class PollenData:
if period_type in ["today", "tomorrow"]: if period_type in ["today", "tomorrow"]:
index_value = float(period.get("Index", 0)) index_value = float(period.get("Index", 0))
# Convert pollen index to percentage scale (0-100) # Convert pollen index to percentage scale (0-100)
pollen_percentage = int(index_value / POLLEN_MAX_INDEX * POLLEN_PERCENTAGE_SCALE) pollen_percentage = int(
index_value / POLLEN_MAX_INDEX * POLLEN_PERCENTAGE_SCALE
)
valid_periods.append(PollenPeriod({ valid_periods.append(
PollenPeriod(
{
"index": pollen_percentage, "index": pollen_percentage,
"period": DateTimeFormatter.relative_day_to_date(period_type), "period": DateTimeFormatter.relative_day_to_date(
})) period_type
),
}
)
)
return valid_periods return valid_periods
def error_handler(func): def error_handler(func):
"""Decorator to handle exceptions in async functions.""" """Decorator to handle exceptions in async functions."""
@functools.wraps(func) @functools.wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
try: try:
@ -253,6 +279,7 @@ def error_handler(func):
except Exception as e: except Exception as e:
logger.exception(f"Error in {func.__name__}: {e}") logger.exception(f"Error in {func.__name__}: {e}")
return [] return []
return wrapper return wrapper
@ -261,7 +288,9 @@ class WeatherService:
@staticmethod @staticmethod
@error_handler @error_handler
async def fetch_weather(latitude: str, longitude: str, api_key: str) -> list[WeatherReport]: async def fetch_weather(
latitude: str, longitude: str, api_key: str
) -> list[WeatherReport]:
"""Fetch weather data from OpenWeatherMap API.""" """Fetch weather data from OpenWeatherMap API."""
cache_key = (latitude, longitude) cache_key = (latitude, longitude)
@ -292,36 +321,50 @@ class WeatherService:
current_dt = datetime.fromtimestamp(current["dt"]) current_dt = datetime.fromtimestamp(current["dt"])
return [WeatherReport({ return [
WeatherReport(
{
"forecast_date": DateTimeFormatter.format_datetime(current_dt), "forecast_date": DateTimeFormatter.format_datetime(current_dt),
"current_temp": int(round(current["temp"])), "current_temp": int(round(current["temp"])),
"current_feels_like": int(round(current["feels_like"])), "current_feels_like": int(round(current["feels_like"])),
"current_humidity": current["humidity"], "current_humidity": current["humidity"],
"sunrise": DateTimeFormatter.format_time(datetime.fromtimestamp(current["sunrise"])), "sunrise": DateTimeFormatter.format_time(
"sunset": DateTimeFormatter.format_time(datetime.fromtimestamp(current["sunset"])), datetime.fromtimestamp(current["sunrise"])
),
"sunset": DateTimeFormatter.format_time(
datetime.fromtimestamp(current["sunset"])
),
"current_pressure": current["pressure"], "current_pressure": current["pressure"],
"current_desc": current["weather"][0]["description"], "current_desc": current["weather"][0]["description"],
"periods": [ "periods": [
WeatherService._format_daily_period(period) WeatherService._format_daily_period(period)
for period in daily_periods for period in daily_periods
], ],
})] }
)
]
@staticmethod @staticmethod
def _format_daily_period(period: DailyWeather) -> WeatherPeriod: def _format_daily_period(period: DailyWeather) -> WeatherPeriod:
"""Format a single daily weather period.""" """Format a single daily weather period."""
period_dt = datetime.fromtimestamp(period["dt"]) period_dt = datetime.fromtimestamp(period["dt"])
return WeatherPeriod({ return WeatherPeriod(
{
"low": int(round(period["temp"]["min"])), "low": int(round(period["temp"]["min"])),
"high": int(round(period["temp"]["max"])), "high": int(round(period["temp"]["max"])),
"desc": period["weather"][0]["description"], "desc": period["weather"][0]["description"],
"humidity": period["humidity"], "humidity": period["humidity"],
"sunrise": DateTimeFormatter.format_time(datetime.fromtimestamp(period["sunrise"])), "sunrise": DateTimeFormatter.format_time(
"sunset": DateTimeFormatter.format_time(datetime.fromtimestamp(period["sunset"])), datetime.fromtimestamp(period["sunrise"])
),
"sunset": DateTimeFormatter.format_time(
datetime.fromtimestamp(period["sunset"])
),
"pressure": period["pressure"], "pressure": period["pressure"],
"period": DateTimeFormatter.format_date(period_dt), "period": DateTimeFormatter.format_date(period_dt),
}) }
)
class PollenService: class PollenService:
@ -352,10 +395,14 @@ class PollenService:
raw_data = response.json() raw_data = response.json()
pollen_data = PollenData(raw_data) pollen_data = PollenData(raw_data)
result = [PollenReport({ result = [
PollenReport(
{
"forecast_date": pollen_data.forecast_date, "forecast_date": pollen_data.forecast_date,
"periods": pollen_data.periods, "periods": pollen_data.periods,
})] }
)
]
pollen_cache[zip_code] = result pollen_cache[zip_code] = result
return result return result
@ -368,7 +415,7 @@ class DataAggregator:
def build_daily_data( def build_daily_data(
date: str, date: str,
pollen_periods: dict[str, PollenPeriod], pollen_periods: dict[str, PollenPeriod],
weather_periods: dict[str, WeatherPeriod] weather_periods: dict[str, WeatherPeriod],
) -> DailyData: ) -> DailyData:
"""Build daily data combining weather and pollen information.""" """Build daily data combining weather and pollen information."""
daily_data: DailyData = {} daily_data: DailyData = {}
@ -378,7 +425,8 @@ class DataAggregator:
if date in weather_periods: if date in weather_periods:
weather_data = weather_periods[date] weather_data = weather_periods[date]
daily_data.update({ daily_data.update(
{
"low": weather_data["low"], "low": weather_data["low"],
"high": weather_data["high"], "high": weather_data["high"],
"desc": weather_data["desc"], "desc": weather_data["desc"],
@ -386,46 +434,46 @@ class DataAggregator:
"sunrise": weather_data["sunrise"], "sunrise": weather_data["sunrise"],
"sunset": weather_data["sunset"], "sunset": weather_data["sunset"],
"pressure": weather_data["pressure"], "pressure": weather_data["pressure"],
}) }
)
return daily_data return daily_data
@staticmethod @staticmethod
def create_periods_lookup( def create_periods_lookup(
pollen_data: list[PollenReport], pollen_data: list[PollenReport], weather_data: list[WeatherReport]
weather_data: list[WeatherReport]
) -> tuple[dict[str, PollenPeriod], dict[str, WeatherPeriod], CurrentWeatherData]: ) -> tuple[dict[str, PollenPeriod], dict[str, WeatherPeriod], CurrentWeatherData]:
"""Create lookup dictionaries for pollen and weather periods.""" """Create lookup dictionaries for pollen and weather periods."""
pollen_periods: dict[str, PollenPeriod] = {} pollen_periods: dict[str, PollenPeriod] = {}
weather_periods: dict[str, WeatherPeriod] = {} weather_periods: dict[str, WeatherPeriod] = {}
if pollen_data and pollen_data[0].get("periods"): if pollen_data and pollen_data[0].get("periods"):
pollen_periods = { pollen_periods = {p["period"]: p for p in pollen_data[0]["periods"]}
p["period"]: p for p in pollen_data[0]["periods"]
}
if weather_data and weather_data[0].get("periods"): if weather_data and weather_data[0].get("periods"):
weather_periods = { weather_periods = {p["period"]: p for p in weather_data[0]["periods"]}
p["period"]: p for p in weather_data[0]["periods"]
}
current_weather_info = CurrentWeatherData({ current_weather_info = CurrentWeatherData(
{
"temp": 0, "temp": 0,
"feels_like": 0, "feels_like": 0,
"desc": "", "desc": "",
"humidity": 0, "humidity": 0,
"pressure": 0, "pressure": 0,
}) }
)
if weather_data and weather_data[0]: if weather_data and weather_data[0]:
data = weather_data[0] data = weather_data[0]
current_weather_info = CurrentWeatherData({ current_weather_info = CurrentWeatherData(
{
"temp": data["current_temp"], "temp": data["current_temp"],
"feels_like": data["current_feels_like"], "feels_like": data["current_feels_like"],
"desc": data["current_desc"], "desc": data["current_desc"],
"humidity": data["current_humidity"], "humidity": data["current_humidity"],
"pressure": data["current_pressure"], "pressure": data["current_pressure"],
}) }
)
return pollen_periods, weather_periods, current_weather_info return pollen_periods, weather_periods, current_weather_info
@ -449,29 +497,37 @@ async def get_weather_pollen_report(token: str) -> FinalReport:
pollen_data, weather_data = await asyncio.gather( pollen_data, weather_data = await asyncio.gather(
PollenService.fetch_pollen(config.zip_code), PollenService.fetch_pollen(config.zip_code),
WeatherService.fetch_weather(config.latitude, config.longitude, config.weather_api_key), WeatherService.fetch_weather(
config.latitude, config.longitude, config.weather_api_key
),
) )
pollen_periods, weather_periods, current_weather_info = DataAggregator.create_periods_lookup( pollen_periods, weather_periods, current_weather_info = (
pollen_data, weather_data DataAggregator.create_periods_lookup(pollen_data, weather_data)
) )
now = datetime.now() now = datetime.now()
today_date = DateTimeFormatter.format_date(now) today_date = DateTimeFormatter.format_date(now)
tomorrow_date = DateTimeFormatter.format_date(now + timedelta(days=1)) tomorrow_date = DateTimeFormatter.format_date(now + timedelta(days=1))
result: FinalReport = FinalReport({ result: FinalReport = FinalReport(
{
"fetched_at": DateTimeFormatter.format_datetime(now), "fetched_at": DateTimeFormatter.format_datetime(now),
"current": current_weather_info, "current": current_weather_info,
"today": DailyData({}), "today": DailyData({}),
"tomorrow": DailyData({}), "tomorrow": DailyData({}),
}) }
)
today_data = DataAggregator.build_daily_data(today_date, pollen_periods, weather_periods) today_data = DataAggregator.build_daily_data(
today_date, pollen_periods, weather_periods
)
if today_data: if today_data:
result["today"] = today_data result["today"] = today_data
tomorrow_data = DataAggregator.build_daily_data(tomorrow_date, pollen_periods, weather_periods) tomorrow_data = DataAggregator.build_daily_data(
tomorrow_date, pollen_periods, weather_periods
)
if tomorrow_data: if tomorrow_data:
result["tomorrow"] = tomorrow_data result["tomorrow"] = tomorrow_data