fix: preserve local time when parsing naive datetime strings (#1294)

to_datetime() previously interpreted naive strings differently depending on their precision. For example, 2026-01-15 23:45 in Europe/Berlin became 2026-01-16 00:45+01:00, while the equivalent string with seconds kept the intended date and time. Minutes, seconds, and fractional seconds now consistently represent 2026-01-15 23:45+01:00.

Pass the resolved target/local timezone to Pendulum's fallback parser. Explicit Z and numeric offsets continue to determine the input instant, and Unix timestamps retain their UTC semantics. Update the affected docstring examples to distinguish local wall time from UTC conversion.

Regression coverage includes space and T separators, explicit and default Europe/Berlin timezones, winter/summer dates near midnight, fractional-second precision, explicit offsets, and Unix timestamps. The Open-Meteo integration test now independently checks the timestamps and all three irradiance series for all 72 fixture hours, with both UTC and Europe/Berlin as the host timezone. This replaces an expectation that incorrectly associated the 08:00 values with 09:00. The previous parser misplaces all 72 source hours; the corrected parser maps all 72 correctly.
This commit is contained in:
dr-dimitri
2026-09-14 08:24:55 +02:00
committed by GitHub
parent 3b9eecc52b
commit 446ea3da9d
3 changed files with 173 additions and 41 deletions
+24 -11
View File
@@ -1,5 +1,6 @@
import json
from pathlib import Path
from typing import Any, Callable
from unittest.mock import Mock, patch
import pandas as pd
@@ -136,9 +137,18 @@ def test_request_forecast(mock_get, provider, sample_openmeteo_1_json):
@pytest.mark.asyncio
@pytest.mark.parametrize("host_timezone", ["UTC", "Europe/Berlin"])
@patch("requests.get")
async def test_update_data(mock_get, provider, sample_openmeteo_1_json, cache_store):
"""Test fetching and processing forecast from Open-Meteo."""
async def test_update_data(
mock_get: Mock,
provider: WeatherOpenMeteo,
sample_openmeteo_1_json: dict[str, Any],
cache_store: CacheFileStore,
set_other_timezone: Callable[[str], str],
host_timezone: str,
) -> None:
"""Map each forecast hour in the provider timezone, regardless of the host timezone."""
set_other_timezone(host_timezone)
# Mock response object
mock_response = Mock()
mock_response.status_code = 200
@@ -158,15 +168,18 @@ async def test_update_data(mock_get, provider, sample_openmeteo_1_json, cache_st
mock_get.assert_called_once()
assert len(provider) > 0
# Verify that direct radiation values were properly mapped
# Get the first record and check for irradiance values
value_datetime = to_datetime("2026-03-04 09:00:00+01:00", in_timezone="Europe/Berlin")
weather_ghi = await provider.key_to_value("weather_ghi", target_datetime=start_datetime)
weather_dni = await provider.key_to_value("weather_dni", target_datetime=start_datetime)
weather_dhi = await provider.key_to_value("weather_dhi", target_datetime=start_datetime)
assert weather_ghi == 21.8
assert weather_dni == 1.2
assert weather_dhi == 20.5
# Open-Meteo returns local wall times without offsets or seconds. Build the
# expected instants independently of to_datetime, including midnight boundaries.
hourly = sample_openmeteo_1_json["hourly"]
expected_datetimes = pd.DatetimeIndex(hourly["time"], tz="Europe/Berlin").to_pydatetime()
for source_key, record_key in (
("shortwave_radiation", "weather_ghi"),
("direct_radiation", "weather_dni"),
("diffuse_radiation", "weather_dhi"),
):
datetimes, values = await provider.key_to_lists(record_key)
assert list(datetimes) == list(expected_datetimes)
assert values == hourly[source_key]
# ------------------------------------------------