mirror of
https://github.com/Akkudoktor-EOS/EOS.git
synced 2026-10-13 09:31:56 +00:00
fix: parse naive date/time strings in to_time's target timezone (#1359)
* fix: interpret naive time date strings in target timezone * test: cover naive and offset time date strings
This commit is contained in:
@@ -672,12 +672,15 @@ def to_time(
|
|||||||
- pendulum.Time or pendulum.DateTime
|
- pendulum.Time or pendulum.DateTime
|
||||||
- datetime.time or datetime.datetime
|
- datetime.time or datetime.datetime
|
||||||
- strings like "14:30", "2:30 PM", "1430", "14:30:00.123", "2PM", "14h30"
|
- strings like "14:30", "2:30 PM", "1430", "14:30:00.123", "2PM", "14h30"
|
||||||
|
- date/time strings (e.g. "2026-01-15T23:45")
|
||||||
- int (e.g. 14 → 14:00)
|
- int (e.g. 14 → 14:00)
|
||||||
- float (e.g. 14.5 → 14:30)
|
- float (e.g. 14.5 → 14:30)
|
||||||
- tuple like (14,), (14, 30), (14, 30, 15)
|
- tuple like (14,), (14, 30), (14, 30, 15)
|
||||||
|
|
||||||
in_timezone: Optional timezone name or object (e.g., "Europe/Berlin").
|
in_timezone: Optional timezone name or object (e.g., "Europe/Berlin").
|
||||||
Defaults to the local timezone.
|
Defaults to the local timezone. Date/time strings without an offset use this
|
||||||
|
timezone as their wall time; strings with `Z` or an offset represent an instant
|
||||||
|
that is converted to this timezone.
|
||||||
|
|
||||||
to_naive: If True, return a timezone-naive Time object.
|
to_naive: If True, return a timezone-naive Time object.
|
||||||
|
|
||||||
@@ -812,16 +815,18 @@ def to_time(
|
|||||||
|
|
||||||
# Fallback to pendulum's parser
|
# Fallback to pendulum's parser
|
||||||
try:
|
try:
|
||||||
dt = cast(pendulum.DateTime, pendulum.parse(value, strict=False)).in_tz(timezone)
|
dt = cast(
|
||||||
|
pendulum.DateTime, pendulum.parse(value, tz=timezone, strict=False)
|
||||||
|
).in_tz(timezone)
|
||||||
return finalize(dt.time())
|
return finalize(dt.time())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.trace(f"Pendulum parser failed for '{value}': {e}")
|
logger.trace(f"Pendulum parser failed for '{value}': {e}")
|
||||||
|
|
||||||
# Try parsing with ISO time prefix
|
# Try parsing with ISO time prefix
|
||||||
try:
|
try:
|
||||||
dt = cast(pendulum.DateTime, pendulum.parse(f"T{value}", strict=False)).in_tz(
|
dt = cast(
|
||||||
timezone
|
pendulum.DateTime, pendulum.parse(f"T{value}", tz=timezone, strict=False)
|
||||||
)
|
).in_tz(timezone)
|
||||||
return finalize(dt.time())
|
return finalize(dt.time())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.trace(f"ISO time parser failed for 'T{value}': {e}")
|
logger.trace(f"ISO time parser failed for 'T{value}': {e}")
|
||||||
@@ -829,7 +834,8 @@ def to_time(
|
|||||||
# Try parsing as part of a full datetime
|
# Try parsing as part of a full datetime
|
||||||
try:
|
try:
|
||||||
dt = cast(
|
dt = cast(
|
||||||
pendulum.DateTime, pendulum.parse(f"2000-01-01 {value}", strict=False)
|
pendulum.DateTime,
|
||||||
|
pendulum.parse(f"2000-01-01 {value}", tz=timezone, strict=False),
|
||||||
).in_tz(timezone)
|
).in_tz(timezone)
|
||||||
return finalize(dt.time())
|
return finalize(dt.time())
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -675,6 +675,36 @@ class TestToTime:
|
|||||||
assert result.minute == 30
|
assert result.minute == 30
|
||||||
assert result.second == 45
|
assert result.second == 45
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("host_timezone", ["UTC", "America/New_York"])
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"value, expected_time, expected_microsecond",
|
||||||
|
[
|
||||||
|
("2026-01-15T23:45", "23:45:00", 0),
|
||||||
|
("2026-07-15 23:45", "23:45:00", 0),
|
||||||
|
("2026-01-15 23:45:30", "23:45:30", 0),
|
||||||
|
("2026-07-15T23:45:30.123456", "23:45:30", 123456),
|
||||||
|
("2026-07-15T23:45Z", "01:45:00", 0),
|
||||||
|
("2026-01-15 23:45+00:00", "00:45:00", 0),
|
||||||
|
("2026-07-15T23:45:30.123456+03:00", "22:45:30", 123456),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_to_time_datetime_strings_use_target_timezone(
|
||||||
|
self, set_other_timezone, host_timezone, value, expected_time, expected_microsecond
|
||||||
|
):
|
||||||
|
set_other_timezone(host_timezone)
|
||||||
|
result = to_time(value, in_timezone="Europe/Berlin")
|
||||||
|
assert isinstance(result, Time)
|
||||||
|
assert result.format("HH:mm:ss") == expected_time
|
||||||
|
assert result.microsecond == expected_microsecond
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("local_timezone", ["Europe/Berlin", "America/New_York"])
|
||||||
|
def test_to_time_naive_datetime_string_defaults_to_local_timezone(
|
||||||
|
self, set_other_timezone, local_timezone
|
||||||
|
):
|
||||||
|
set_other_timezone(local_timezone)
|
||||||
|
assert to_time("2026-01-15T23:45", as_string="HH:mm:ss") == "23:45:00"
|
||||||
|
assert to_time("23:45", as_string="HH:mm:ss") == "23:45:00"
|
||||||
|
|
||||||
@patch('akkudoktoreos.utils.datetimeutil.logger.trace')
|
@patch('akkudoktoreos.utils.datetimeutil.logger.trace')
|
||||||
def test_to_time_logging_on_parse_failures(self, mock_trace):
|
def test_to_time_logging_on_parse_failures(self, mock_trace):
|
||||||
"""Test that parsing failures are logged appropriately."""
|
"""Test that parsing failures are logged appropriately."""
|
||||||
|
|||||||
Reference in New Issue
Block a user