# services/external/app/repositories/weather_repository.py from typing import List, Dict, Any, Optional from datetime import datetime from sqlalchemy import select, and_ from sqlalchemy.ext.asyncio import AsyncSession import structlog import json from app.models.weather import WeatherData logger = structlog.get_logger() class WeatherRepository: """ Repository for weather data operations, adapted for WeatherService. """ def __init__(self, session: AsyncSession): self.session = session async def get_historical_weather(self, location_id: str, start_date: datetime, end_date: datetime) -> List[WeatherData]: """ Retrieves historical weather data for a specific location and date range. This method directly supports the data retrieval logic in WeatherService. """ try: stmt = select(WeatherData).where( and_( WeatherData.location_id == location_id, WeatherData.date >= start_date, WeatherData.date <= end_date ) ).order_by(WeatherData.date) result = await self.session.execute(stmt) records = result.scalars().all() logger.debug(f"Retrieved {len(records)} historical records for location {location_id}") return list(records) except Exception as e: logger.error( "Failed to get historical weather from repository", error=str(e), location_id=location_id ) raise def _serialize_json_fields(self, data: Dict[str, Any]) -> Dict[str, Any]: """ Serialize JSON fields (raw_data, processed_data) to ensure proper JSON storage """ serialized = data.copy() # Serialize raw_data if present if 'raw_data' in serialized and serialized['raw_data'] is not None: if not isinstance(serialized['raw_data'], str): try: # Convert datetime objects to strings for JSON serialization raw_data = serialized['raw_data'] if isinstance(raw_data, dict): # Handle datetime objects in the dict json_safe_data = {} for k, v in raw_data.items(): if hasattr(v, 'isoformat'): # datetime-like object json_safe_data[k] = v.isoformat() else: json_safe_data[k] = v serialized['raw_data'] = json_safe_data except Exception as e: logger.warning(f"Could not serialize raw_data, storing as string: {e}") serialized['raw_data'] = str(raw_data) # Serialize processed_data if present if 'processed_data' in serialized and serialized['processed_data'] is not None: if not isinstance(serialized['processed_data'], str): try: processed_data = serialized['processed_data'] if isinstance(processed_data, dict): json_safe_data = {} for k, v in processed_data.items(): if hasattr(v, 'isoformat'): # datetime-like object json_safe_data[k] = v.isoformat() else: json_safe_data[k] = v serialized['processed_data'] = json_safe_data except Exception as e: logger.warning(f"Could not serialize processed_data, storing as string: {e}") serialized['processed_data'] = str(processed_data) return serialized async def bulk_create_weather_data(self, weather_records: List[Dict[str, Any]]) -> None: """ Bulk inserts new weather records into the database. Used by WeatherService after fetching new historical data from an external API. """ try: if not weather_records: return # Serialize JSON fields before creating model instances serialized_records = [self._serialize_json_fields(data) for data in weather_records] records = [WeatherData(**data) for data in serialized_records] self.session.add_all(records) await self.session.commit() logger.info(f"Successfully bulk inserted {len(records)} weather records") except Exception as e: await self.session.rollback() logger.error( "Failed to bulk create weather records", error=str(e), count=len(weather_records) ) raise async def create_weather_data(self, data: Dict[str, Any]) -> WeatherData: """ Creates a single new weather data record. """ try: # Serialize JSON fields before creating model instance serialized_data = self._serialize_json_fields(data) new_record = WeatherData(**serialized_data) self.session.add(new_record) await self.session.commit() await self.session.refresh(new_record) logger.info(f"Created new weather record with ID {new_record.id}") return new_record except Exception as e: await self.session.rollback() logger.error("Failed to create single weather record", error=str(e)) raise