REFACTOR AEMET weather file
This commit is contained in:
@@ -1,18 +1,653 @@
|
||||
# ================================================================
|
||||
# services/data/tests/conftest.py
|
||||
# services/data/tests/conftest.py - AEMET Test Configuration
|
||||
# ================================================================
|
||||
"""Test configuration for data service"""
|
||||
"""
|
||||
Test configuration and fixtures for AEMET weather API client tests
|
||||
Provides shared fixtures, mock data, and test utilities
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
||||
from sqlalchemy.pool import StaticPool
|
||||
from fastapi.testclient import TestClient
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
import asyncio
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import Mock, AsyncMock, patch
|
||||
from typing import Dict, List, Any, Generator
|
||||
import os
|
||||
|
||||
from app.main import app
|
||||
from app.core.database import Base, get_db
|
||||
from app.models.sales import SalesData
|
||||
from app.models.weather import WeatherData, WeatherForecast
|
||||
from app.models.traffic import TrafficData
|
||||
# Import the classes we're testing
|
||||
from app.external.aemet import (
|
||||
AEMETClient,
|
||||
WeatherDataParser,
|
||||
SyntheticWeatherGenerator,
|
||||
LocationService,
|
||||
WeatherSource
|
||||
)
|
||||
|
||||
|
||||
# ================================================================
|
||||
# PYTEST CONFIGURATION
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def event_loop():
|
||||
"""Create an instance of the default event loop for the test session."""
|
||||
loop = asyncio.get_event_loop_policy().new_event_loop()
|
||||
yield loop
|
||||
loop.close()
|
||||
|
||||
|
||||
# ================================================================
|
||||
# CLIENT AND SERVICE FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def aemet_client():
|
||||
"""Create AEMET client instance for testing"""
|
||||
return AEMETClient()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def weather_parser():
|
||||
"""Create WeatherDataParser instance for testing"""
|
||||
return WeatherDataParser()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def synthetic_generator():
|
||||
"""Create SyntheticWeatherGenerator instance for testing"""
|
||||
return SyntheticWeatherGenerator()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def location_service():
|
||||
"""Create LocationService instance for testing"""
|
||||
return LocationService()
|
||||
|
||||
|
||||
# ================================================================
|
||||
# COORDINATE AND LOCATION FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def madrid_coords():
|
||||
"""Standard Madrid coordinates for testing"""
|
||||
return (40.4168, -3.7038) # Madrid city center
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def madrid_coords_variants():
|
||||
"""Various Madrid area coordinates for testing"""
|
||||
return {
|
||||
"center": (40.4168, -3.7038), # Madrid center
|
||||
"north": (40.4677, -3.5552), # Madrid north (near station)
|
||||
"south": (40.2987, -3.7216), # Madrid south (near station)
|
||||
"east": (40.4200, -3.6500), # Madrid east
|
||||
"west": (40.4100, -3.7500), # Madrid west
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def invalid_coords():
|
||||
"""Invalid coordinates for error testing"""
|
||||
return [
|
||||
(200, 200), # Out of range
|
||||
(-200, -200), # Out of range
|
||||
(0, 0), # Not in Madrid area
|
||||
(50, 10), # Europe but not Madrid
|
||||
(None, None), # None values
|
||||
]
|
||||
|
||||
|
||||
# ================================================================
|
||||
# DATE AND TIME FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def test_dates():
|
||||
"""Standard date ranges for testing"""
|
||||
now = datetime.now()
|
||||
return {
|
||||
"now": now,
|
||||
"yesterday": now - timedelta(days=1),
|
||||
"last_week": now - timedelta(days=7),
|
||||
"last_month": now - timedelta(days=30),
|
||||
"last_quarter": now - timedelta(days=90),
|
||||
"one_year_ago": now - timedelta(days=365),
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def historical_date_ranges():
|
||||
"""Historical date ranges for testing"""
|
||||
end_date = datetime.now()
|
||||
return {
|
||||
"one_day": {
|
||||
"start": end_date - timedelta(days=1),
|
||||
"end": end_date,
|
||||
"expected_days": 1
|
||||
},
|
||||
"one_week": {
|
||||
"start": end_date - timedelta(days=7),
|
||||
"end": end_date,
|
||||
"expected_days": 7
|
||||
},
|
||||
"one_month": {
|
||||
"start": end_date - timedelta(days=30),
|
||||
"end": end_date,
|
||||
"expected_days": 30
|
||||
},
|
||||
"large_range": {
|
||||
"start": end_date - timedelta(days=65),
|
||||
"end": end_date,
|
||||
"expected_days": 65
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ================================================================
|
||||
# MOCK API RESPONSE FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def mock_aemet_api_response():
|
||||
"""Mock AEMET API initial response structure"""
|
||||
return {
|
||||
"datos": "https://opendata.aemet.es/opendata/sh/12345abcdef",
|
||||
"metadatos": "https://opendata.aemet.es/opendata/sh/metadata123"
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_aemet_error_response():
|
||||
"""Mock AEMET API error response"""
|
||||
return {
|
||||
"descripcion": "Error en la petición",
|
||||
"estado": 404
|
||||
}
|
||||
|
||||
|
||||
# ================================================================
|
||||
# WEATHER DATA FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def mock_current_weather_data():
|
||||
"""Mock current weather data from AEMET API"""
|
||||
return {
|
||||
"idema": "3195", # Station ID
|
||||
"ubi": "MADRID", # Location
|
||||
"fint": "2025-07-24T14:00:00", # Observation time
|
||||
"ta": 18.5, # Temperature (°C)
|
||||
"tamin": 12.3, # Min temperature
|
||||
"tamax": 25.7, # Max temperature
|
||||
"hr": 65.0, # Humidity (%)
|
||||
"prec": 0.0, # Precipitation (mm)
|
||||
"vv": 12.0, # Wind speed (km/h)
|
||||
"dv": 180, # Wind direction (degrees)
|
||||
"pres": 1015.2, # Pressure (hPa)
|
||||
"presMax": 1018.5, # Max pressure
|
||||
"presMin": 1012.1, # Min pressure
|
||||
"descripcion": "Despejado" # Description
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_forecast_data():
|
||||
"""Mock forecast data from AEMET API"""
|
||||
return [{
|
||||
"origen": {
|
||||
"productor": "Agencia Estatal de Meteorología - AEMET"
|
||||
},
|
||||
"elaborado": "2025-07-24T12:00:00UTC",
|
||||
"nombre": "Madrid",
|
||||
"provincia": "Madrid",
|
||||
"prediccion": {
|
||||
"dia": [
|
||||
{
|
||||
"fecha": "2025-07-25T00:00:00",
|
||||
"temperatura": {
|
||||
"maxima": 28,
|
||||
"minima": 15,
|
||||
"dato": [
|
||||
{"value": 15, "hora": 6},
|
||||
{"value": 28, "hora": 15}
|
||||
]
|
||||
},
|
||||
"sensTermica": {
|
||||
"maxima": 30,
|
||||
"minima": 16
|
||||
},
|
||||
"humedadRelativa": {
|
||||
"maxima": 85,
|
||||
"minima": 45,
|
||||
"dato": [
|
||||
{"value": 85, "hora": 6},
|
||||
{"value": 45, "hora": 15}
|
||||
]
|
||||
},
|
||||
"probPrecipitacion": [
|
||||
{"value": 10, "periodo": "00-24"}
|
||||
],
|
||||
"viento": [
|
||||
{
|
||||
"direccion": ["N"],
|
||||
"velocidad": [15],
|
||||
"periodo": "00-24"
|
||||
}
|
||||
],
|
||||
"estadoCielo": [
|
||||
{
|
||||
"value": "11",
|
||||
"descripcion": "Despejado",
|
||||
"periodo": "00-24"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fecha": "2025-07-26T00:00:00",
|
||||
"temperatura": {
|
||||
"maxima": 30,
|
||||
"minima": 17
|
||||
},
|
||||
"probPrecipitacion": [
|
||||
{"value": 5, "periodo": "00-24"}
|
||||
],
|
||||
"viento": [
|
||||
{
|
||||
"direccion": ["NE"],
|
||||
"velocidad": [10],
|
||||
"periodo": "00-24"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_historical_data():
|
||||
"""Mock historical weather data from AEMET API"""
|
||||
return [
|
||||
{
|
||||
"indicativo": "3195",
|
||||
"nombre": "MADRID",
|
||||
"fecha": "2025-07-20",
|
||||
"tmax": 25.2,
|
||||
"horatmax": "1530",
|
||||
"tmin": 14.8,
|
||||
"horatmin": "0630",
|
||||
"tmed": 20.0,
|
||||
"prec": 0.0,
|
||||
"racha": 25.0,
|
||||
"horaracha": "1445",
|
||||
"sol": 8.5,
|
||||
"presMax": 1018.5,
|
||||
"horaPresMax": "1000",
|
||||
"presMin": 1012.3,
|
||||
"horaPresMin": "1700",
|
||||
"hr": 58,
|
||||
"velmedia": 8.5,
|
||||
"dir": "180"
|
||||
},
|
||||
{
|
||||
"indicativo": "3195",
|
||||
"nombre": "MADRID",
|
||||
"fecha": "2025-07-21",
|
||||
"tmax": 27.1,
|
||||
"horatmax": "1615",
|
||||
"tmin": 16.2,
|
||||
"horatmin": "0700",
|
||||
"tmed": 21.6,
|
||||
"prec": 2.5,
|
||||
"racha": 30.0,
|
||||
"horaracha": "1330",
|
||||
"sol": 6.2,
|
||||
"presMax": 1015.8,
|
||||
"horaPresMax": "0930",
|
||||
"presMin": 1010.1,
|
||||
"horaPresMin": "1800",
|
||||
"hr": 72,
|
||||
"velmedia": 12.0,
|
||||
"dir": "225"
|
||||
},
|
||||
{
|
||||
"indicativo": "3195",
|
||||
"nombre": "MADRID",
|
||||
"fecha": "2025-07-22",
|
||||
"tmax": 23.8,
|
||||
"horatmax": "1500",
|
||||
"tmin": 13.5,
|
||||
"horatmin": "0615",
|
||||
"tmed": 18.7,
|
||||
"prec": 0.2,
|
||||
"racha": 22.0,
|
||||
"horaracha": "1200",
|
||||
"sol": 7.8,
|
||||
"presMax": 1020.2,
|
||||
"horaPresMax": "1100",
|
||||
"presMin": 1014.7,
|
||||
"horaPresMin": "1900",
|
||||
"hr": 63,
|
||||
"velmedia": 9.2,
|
||||
"dir": "270"
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
# ================================================================
|
||||
# EXPECTED RESULT FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def expected_current_weather_structure():
|
||||
"""Expected structure for current weather results"""
|
||||
return {
|
||||
"required_fields": [
|
||||
"date", "temperature", "precipitation", "humidity",
|
||||
"wind_speed", "pressure", "description", "source"
|
||||
],
|
||||
"field_types": {
|
||||
"date": datetime,
|
||||
"temperature": (int, float),
|
||||
"precipitation": (int, float),
|
||||
"humidity": (int, float),
|
||||
"wind_speed": (int, float),
|
||||
"pressure": (int, float),
|
||||
"description": str,
|
||||
"source": str
|
||||
},
|
||||
"valid_ranges": {
|
||||
"temperature": (-30, 50),
|
||||
"precipitation": (0, 200),
|
||||
"humidity": (0, 100),
|
||||
"wind_speed": (0, 200),
|
||||
"pressure": (900, 1100)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def expected_forecast_structure():
|
||||
"""Expected structure for forecast results"""
|
||||
return {
|
||||
"required_fields": [
|
||||
"forecast_date", "generated_at", "temperature", "precipitation",
|
||||
"humidity", "wind_speed", "description", "source"
|
||||
],
|
||||
"field_types": {
|
||||
"forecast_date": datetime,
|
||||
"generated_at": datetime,
|
||||
"temperature": (int, float),
|
||||
"precipitation": (int, float),
|
||||
"humidity": (int, float),
|
||||
"wind_speed": (int, float),
|
||||
"description": str,
|
||||
"source": str
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def expected_historical_structure():
|
||||
"""Expected structure for historical weather results"""
|
||||
return {
|
||||
"required_fields": [
|
||||
"date", "temperature", "precipitation", "humidity",
|
||||
"wind_speed", "pressure", "description", "source"
|
||||
],
|
||||
"field_types": {
|
||||
"date": datetime,
|
||||
"temperature": (int, float, type(None)),
|
||||
"precipitation": (int, float),
|
||||
"humidity": (int, float, type(None)),
|
||||
"wind_speed": (int, float, type(None)),
|
||||
"pressure": (int, float, type(None)),
|
||||
"description": str,
|
||||
"source": str
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ================================================================
|
||||
# MOCK AND PATCH FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def mock_successful_api_calls():
|
||||
"""Mock successful AEMET API calls"""
|
||||
def _mock_api_calls(client, response_data, fetch_data):
|
||||
with patch.object(client, '_get', new_callable=AsyncMock) as mock_get, \
|
||||
patch.object(client, '_fetch_from_url', new_callable=AsyncMock) as mock_fetch:
|
||||
|
||||
mock_get.return_value = response_data
|
||||
mock_fetch.return_value = fetch_data
|
||||
|
||||
return mock_get, mock_fetch
|
||||
|
||||
return _mock_api_calls
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_failed_api_calls():
|
||||
"""Mock failed AEMET API calls"""
|
||||
def _mock_failed_calls(client, error_type="network"):
|
||||
if error_type == "network":
|
||||
return patch.object(client, '_get', side_effect=Exception("Network error"))
|
||||
elif error_type == "timeout":
|
||||
return patch.object(client, '_get', side_effect=asyncio.TimeoutError("Request timeout"))
|
||||
elif error_type == "invalid_response":
|
||||
return patch.object(client, '_get', new_callable=AsyncMock, return_value=None)
|
||||
else:
|
||||
return patch.object(client, '_get', new_callable=AsyncMock, return_value={"error": "API error"})
|
||||
|
||||
return _mock_failed_calls
|
||||
|
||||
|
||||
# ================================================================
|
||||
# VALIDATION HELPER FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def weather_data_validator():
|
||||
"""Weather data validation helper functions"""
|
||||
|
||||
def validate_weather_record(record: Dict[str, Any], expected_structure: Dict[str, Any]) -> None:
|
||||
"""Validate a weather record against expected structure"""
|
||||
# Check required fields
|
||||
for field in expected_structure["required_fields"]:
|
||||
assert field in record, f"Missing required field: {field}"
|
||||
|
||||
# Check field types
|
||||
for field, expected_type in expected_structure["field_types"].items():
|
||||
if field in record and record[field] is not None:
|
||||
assert isinstance(record[field], expected_type), f"Field {field} has wrong type: {type(record[field])}"
|
||||
|
||||
# Check valid ranges where applicable
|
||||
if "valid_ranges" in expected_structure:
|
||||
for field, (min_val, max_val) in expected_structure["valid_ranges"].items():
|
||||
if field in record and record[field] is not None:
|
||||
value = record[field]
|
||||
assert min_val <= value <= max_val, f"Field {field} value {value} outside valid range [{min_val}, {max_val}]"
|
||||
|
||||
def validate_weather_list(records: List[Dict[str, Any]], expected_structure: Dict[str, Any]) -> None:
|
||||
"""Validate a list of weather records"""
|
||||
assert isinstance(records, list), "Records should be a list"
|
||||
|
||||
for i, record in enumerate(records):
|
||||
try:
|
||||
validate_weather_record(record, expected_structure)
|
||||
except AssertionError as e:
|
||||
raise AssertionError(f"Record {i} validation failed: {e}")
|
||||
|
||||
def validate_date_sequence(records: List[Dict[str, Any]], date_field: str = "date") -> None:
|
||||
"""Validate that dates in records are in chronological order"""
|
||||
dates = [r[date_field] for r in records if date_field in r and r[date_field] is not None]
|
||||
|
||||
if len(dates) > 1:
|
||||
assert dates == sorted(dates), "Dates should be in chronological order"
|
||||
|
||||
return {
|
||||
"validate_record": validate_weather_record,
|
||||
"validate_list": validate_weather_list,
|
||||
"validate_dates": validate_date_sequence
|
||||
}
|
||||
|
||||
|
||||
# ================================================================
|
||||
# PERFORMANCE TESTING FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def performance_tracker():
|
||||
"""Performance tracking utilities for tests"""
|
||||
|
||||
class PerformanceTracker:
|
||||
def __init__(self):
|
||||
self.start_time = None
|
||||
self.measurements = {}
|
||||
|
||||
def start(self, operation_name: str = "default"):
|
||||
self.start_time = datetime.now()
|
||||
self.operation_name = operation_name
|
||||
|
||||
def stop(self) -> float:
|
||||
if self.start_time:
|
||||
duration = (datetime.now() - self.start_time).total_seconds() * 1000
|
||||
self.measurements[self.operation_name] = duration
|
||||
return duration
|
||||
return 0.0
|
||||
|
||||
def assert_performance(self, max_duration_ms: float, operation_name: str = "default"):
|
||||
duration = self.measurements.get(operation_name, float('inf'))
|
||||
assert duration <= max_duration_ms, f"Operation {operation_name} took {duration:.0f}ms, expected <= {max_duration_ms}ms"
|
||||
|
||||
return PerformanceTracker()
|
||||
|
||||
|
||||
# ================================================================
|
||||
# INTEGRATION TEST FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def integration_test_config():
|
||||
"""Configuration for integration tests"""
|
||||
return {
|
||||
"api_timeout_ms": 5000,
|
||||
"max_retries": 3,
|
||||
"test_api_key": os.getenv("AEMET_API_KEY_TEST", ""),
|
||||
"skip_real_api_tests": os.getenv("SKIP_REAL_API_TESTS", "false").lower() == "true",
|
||||
"madrid_test_coords": (40.4168, -3.7038),
|
||||
"performance_thresholds": {
|
||||
"current_weather_ms": 5000,
|
||||
"forecast_ms": 5000,
|
||||
"historical_ms": 10000
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# ================================================================
|
||||
# TEST REPORTING FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture
|
||||
def test_reporter():
|
||||
"""Test reporting utilities"""
|
||||
|
||||
class TestReporter:
|
||||
def __init__(self):
|
||||
self.results = []
|
||||
|
||||
def log_success(self, test_name: str, details: str = ""):
|
||||
message = f"✅ {test_name}"
|
||||
if details:
|
||||
message += f" - {details}"
|
||||
print(message)
|
||||
self.results.append({"test": test_name, "status": "PASS", "details": details})
|
||||
|
||||
def log_failure(self, test_name: str, error: str = ""):
|
||||
message = f"❌ {test_name}"
|
||||
if error:
|
||||
message += f" - {error}"
|
||||
print(message)
|
||||
self.results.append({"test": test_name, "status": "FAIL", "error": error})
|
||||
|
||||
def log_info(self, test_name: str, info: str = ""):
|
||||
message = f"ℹ️ {test_name}"
|
||||
if info:
|
||||
message += f" - {info}"
|
||||
print(message)
|
||||
self.results.append({"test": test_name, "status": "INFO", "info": info})
|
||||
|
||||
def summary(self):
|
||||
passed = len([r for r in self.results if r["status"] == "PASS"])
|
||||
failed = len([r for r in self.results if r["status"] == "FAIL"])
|
||||
print(f"\n📊 Test Summary: {passed} passed, {failed} failed")
|
||||
return passed, failed
|
||||
|
||||
return TestReporter()
|
||||
|
||||
|
||||
# ================================================================
|
||||
# CLEANUP FIXTURES
|
||||
# ================================================================
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def cleanup_after_test():
|
||||
"""Automatic cleanup after each test"""
|
||||
yield
|
||||
# Add any cleanup logic here
|
||||
# For example, clearing caches, resetting global state, etc.
|
||||
pass
|
||||
|
||||
|
||||
# ================================================================
|
||||
# HELPER FUNCTIONS
|
||||
# ================================================================
|
||||
|
||||
def assert_weather_data_structure(data: Dict[str, Any], data_type: str = "current"):
|
||||
"""Assert that weather data has the correct structure"""
|
||||
if data_type == "current":
|
||||
required_fields = ["date", "temperature", "precipitation", "humidity", "wind_speed", "pressure", "description", "source"]
|
||||
elif data_type == "forecast":
|
||||
required_fields = ["forecast_date", "generated_at", "temperature", "precipitation", "humidity", "wind_speed", "description", "source"]
|
||||
elif data_type == "historical":
|
||||
required_fields = ["date", "temperature", "precipitation", "humidity", "wind_speed", "pressure", "description", "source"]
|
||||
else:
|
||||
raise ValueError(f"Unknown data type: {data_type}")
|
||||
|
||||
for field in required_fields:
|
||||
assert field in data, f"Missing required field: {field}"
|
||||
|
||||
# Validate source
|
||||
valid_sources = [WeatherSource.AEMET.value, WeatherSource.SYNTHETIC.value, WeatherSource.DEFAULT.value]
|
||||
assert data["source"] in valid_sources, f"Invalid source: {data['source']}"
|
||||
|
||||
|
||||
def assert_forecast_list_structure(forecast_list: List[Dict[str, Any]], expected_days: int):
|
||||
"""Assert that forecast list has correct structure"""
|
||||
assert isinstance(forecast_list, list), "Forecast should be a list"
|
||||
assert len(forecast_list) == expected_days, f"Expected {expected_days} forecast days, got {len(forecast_list)}"
|
||||
|
||||
for i, day in enumerate(forecast_list):
|
||||
assert_weather_data_structure(day, "forecast")
|
||||
|
||||
# Check date progression
|
||||
if len(forecast_list) > 1:
|
||||
for i in range(1, len(forecast_list)):
|
||||
prev_date = forecast_list[i-1]["forecast_date"]
|
||||
curr_date = forecast_list[i]["forecast_date"]
|
||||
date_diff = (curr_date - prev_date).days
|
||||
assert date_diff == 1, f"Forecast dates should be consecutive, got {date_diff} day difference"
|
||||
|
||||
|
||||
def assert_historical_list_structure(historical_list: List[Dict[str, Any]]):
|
||||
"""Assert that historical list has correct structure"""
|
||||
assert isinstance(historical_list, list), "Historical data should be a list"
|
||||
|
||||
for i, record in enumerate(historical_list):
|
||||
assert_weather_data_structure(record, "historical")
|
||||
|
||||
# Check date ordering
|
||||
dates = [r["date"] for r in historical_list if "date" in r]
|
||||
if len(dates) > 1:
|
||||
assert dates == sorted(dates), "Historical dates should be in chronological order"
|
||||
Reference in New Issue
Block a user