release: v1.6.1 - Weather forecasts, sun times, air quality
Build and Push / build (release) Successful in 52s

- Weather fetch now returns 7-day forecasts with UV index
- New /volatile/fetch/sun/{city} endpoint for sunrise/sunset
- New /volatile/fetch/air_quality/{city} endpoint for AQI and pollutants
- OpenMeteoProvider now implements AirQualityProvider interface

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-30 10:13:06 +01:00
co-authored by Claude Opus 4.5
parent 6b0530ed79
commit d6b30570a0
9 changed files with 561 additions and 27 deletions
+188 -13
View File
@@ -14,6 +14,9 @@ from src.apis import (
AggregatedNewsProvider,
AlphaVantageProvider,
CurrentWeather,
WeatherForecast,
SunTimes,
AirQuality,
NewsFeed,
StockQuote,
)
@@ -68,21 +71,23 @@ class VolatileFetchService:
self,
user: str,
city: str,
days: int = 7,
ttl: int = 86400, # 24 hours
) -> FetchResult:
"""
Fetch current weather for a city and store in volatile cache.
Fetch weather forecast for a city and store in volatile cache.
Args:
user: User identifier
city: City name (will be geocoded)
days: Number of forecast days (1-16)
ttl: Time-to-live in seconds
Returns:
FetchResult with success status and stored record
"""
try:
# Geocode city and get weather
# Geocode city and get forecast
location = await self.weather.geocode(city)
if not location:
return FetchResult(
@@ -92,19 +97,45 @@ class VolatileFetchService:
error=f"Could not geocode city: {city}"
)
weather = await self.weather.get_current(location)
forecast = await self.weather.get_forecast(location, days=days)
current = forecast.current
# Build daily forecast array
daily_forecasts = []
for day in forecast.daily:
daily_forecasts.append({
"date": day.date.isoformat(),
"day_name": day.date.strftime("%A"),
"temp_high": day.temp_high,
"temp_low": day.temp_low,
"conditions": day.condition_text,
"condition_code": day.condition.value,
"precipitation_chance": day.precipitation_chance,
"precipitation_mm": day.precipitation_mm,
"uv_index_max": day.uv_index_max,
})
# Generate natural language summary
forecast_lines = [current.to_text()]
for day in forecast.daily[:5]: # First 5 days
forecast_lines.append(day.to_text())
text = "\n".join(forecast_lines)
# Convert to storage format
data = {
"temperature": weather.temperature,
"feels_like": weather.feels_like,
"humidity": weather.humidity,
"wind_speed": weather.wind_speed,
"wind_direction": weather.wind_direction,
"conditions": weather.condition_text,
"condition_code": weather.condition.value,
"location": weather.location,
"text": weather.to_text(),
"current": {
"temperature": current.temperature,
"feels_like": current.feels_like,
"humidity": current.humidity,
"wind_speed": current.wind_speed,
"wind_direction": current.wind_direction,
"conditions": current.condition_text,
"condition_code": current.condition.value,
"uv_index": current.uv_index,
},
"daily": daily_forecasts,
"location": current.location,
"text": text,
}
# Store in volatile cache
@@ -117,7 +148,7 @@ class VolatileFetchService:
ttl=ttl,
)
logger.info(f"Stored weather for {city} (user={user})")
logger.info(f"Stored {days}-day forecast for {city} (user={user})")
return FetchResult(
success=True,
namespace="weather",
@@ -357,3 +388,147 @@ class VolatileFetchService:
key=f"{symbol.lower()}_{market.lower()}",
error=str(e)
)
async def fetch_sun_times(
self,
user: str,
city: str,
ttl: int = 86400, # 24 hours
) -> FetchResult:
"""
Fetch sunrise/sunset times for a city and store in volatile cache.
Args:
user: User identifier
city: City name (will be geocoded)
ttl: Time-to-live in seconds
Returns:
FetchResult with success status and stored record
"""
try:
# Geocode city and get sun times
location = await self.weather.geocode(city)
if not location:
return FetchResult(
success=False,
namespace="sun",
key=city.lower(),
error=f"Could not geocode city: {city}"
)
sun_times = await self.weather.get_sun_times(location)
# Convert to storage format
data = {
"location": sun_times.location,
"date": sun_times.date.isoformat(),
"sunrise": sun_times.sunrise.strftime("%H:%M"),
"sunset": sun_times.sunset.strftime("%H:%M"),
"sunrise_iso": sun_times.sunrise.isoformat(),
"sunset_iso": sun_times.sunset.isoformat(),
"daylight_duration_seconds": sun_times.daylight_duration,
"daylight_hours": sun_times.daylight_duration / 3600,
"text": sun_times.to_text(),
}
# Store in volatile cache
record = await self.volatile.store(
user=user,
namespace=VolatileNamespace.SUN,
key=city.lower(),
data=data,
source="openmeteo",
ttl=ttl,
)
logger.info(f"Stored sun times for {city} (user={user})")
return FetchResult(
success=True,
namespace="sun",
key=city.lower(),
record=record
)
except Exception as e:
logger.error(f"Failed to fetch sun times for {city}: {e}")
return FetchResult(
success=False,
namespace="sun",
key=city.lower(),
error=str(e)
)
async def fetch_air_quality(
self,
user: str,
city: str,
ttl: int = 3600, # 1 hour
) -> FetchResult:
"""
Fetch air quality data for a city and store in volatile cache.
Args:
user: User identifier
city: City name (will be geocoded)
ttl: Time-to-live in seconds
Returns:
FetchResult with success status and stored record
"""
try:
# Geocode city and get air quality
location = await self.weather.geocode(city)
if not location:
return FetchResult(
success=False,
namespace="air_quality",
key=city.lower(),
error=f"Could not geocode city: {city}"
)
air_quality = await self.weather.get_air_quality(location)
# Convert to storage format
data = {
"location": air_quality.location,
"aqi_european": air_quality.aqi_european,
"aqi_us": air_quality.aqi_us,
"pm2_5": air_quality.pm2_5,
"pm10": air_quality.pm10,
"ozone": air_quality.ozone,
"nitrogen_dioxide": air_quality.nitrogen_dioxide,
"sulphur_dioxide": air_quality.sulphur_dioxide,
"carbon_monoxide": air_quality.carbon_monoxide,
"pollen_grass": air_quality.pollen_grass,
"pollen_birch": air_quality.pollen_birch,
"pollen_alder": air_quality.pollen_alder,
"text": air_quality.to_text(),
}
# Store in volatile cache
record = await self.volatile.store(
user=user,
namespace=VolatileNamespace.AIR_QUALITY,
key=city.lower(),
data=data,
source="openmeteo",
ttl=ttl,
)
logger.info(f"Stored air quality for {city} (user={user})")
return FetchResult(
success=True,
namespace="air_quality",
key=city.lower(),
record=record
)
except Exception as e:
logger.error(f"Failed to fetch air quality for {city}: {e}")
return FetchResult(
success=False,
namespace="air_quality",
key=city.lower(),
error=str(e)
)