Compare commits
1 commit
87317cb685
...
2030a4f7da
Author | SHA1 | Date | |
---|---|---|---|
2030a4f7da |
1 changed files with 134 additions and 78 deletions
132
main.py
132
main.py
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue