From d86d3fe333d74c1ddf1ebbac72154646b64a9a4e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 21:57:37 +0000 Subject: [PATCH 1/3] fix(CORE-355): handle abbreviated timezone offsets in convert_partial_iso_format_to_full_iso_format - Add _normalize_timezone_offset to expand +HH to +HH:00 before parsing - Python 3.10 datetime.fromisoformat() rejects +00 format from PostgreSQL - Also fix typo: 'covert' -> 'convert' in error log message Co-Authored-By: Itamar Hartstein --- elementary/utils/time.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/elementary/utils/time.py b/elementary/utils/time.py index 96f0cfce3..7aa98febb 100644 --- a/elementary/utils/time.py +++ b/elementary/utils/time.py @@ -1,3 +1,4 @@ +import re from datetime import datetime, timedelta, timezone from typing import Optional @@ -89,17 +90,24 @@ def datetime_strftime(datetime: datetime, include_timezone: bool = False) -> str ) +_ABBREVIATED_TZ_OFFSET_PATTERN = re.compile(r"([+-])(\d{2})$") + + +def _normalize_timezone_offset(time_string: str) -> str: + return _ABBREVIATED_TZ_OFFSET_PATTERN.sub(r"\1\2:00", time_string) + + def convert_partial_iso_format_to_full_iso_format(partial_iso_format_time: str) -> str: try: - date = datetime.fromisoformat(partial_iso_format_time) - # Get the given date timezone + normalized = _normalize_timezone_offset(partial_iso_format_time) + date = datetime.fromisoformat(normalized) time_zone_name = date.strftime("%Z") time_zone = tz.gettz(time_zone_name) if time_zone_name else tz.UTC date_with_timezone = date.replace(tzinfo=time_zone, microsecond=0) return date_with_timezone.isoformat() except ValueError: logger.exception( - f'Failed to covert time string: "{partial_iso_format_time}" to ISO format' + f'Failed to convert time string: "{partial_iso_format_time}" to ISO format' ) return partial_iso_format_time From b38a4b543760a2f161375f7a5eef9a5d53ec8551 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:19:01 +0000 Subject: [PATCH 2/3] fix: tighten regex to require time component before abbreviated tz offset Anchors the pattern to :\d{2} (seconds) or .\d+ (fractional seconds) before the +/- sign, preventing false matches on date-only strings like '2024-01-15' where '-15' would incorrectly match. Co-Authored-By: Itamar Hartstein --- elementary/utils/time.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/elementary/utils/time.py b/elementary/utils/time.py index 7aa98febb..7b861aa1d 100644 --- a/elementary/utils/time.py +++ b/elementary/utils/time.py @@ -90,11 +90,11 @@ def datetime_strftime(datetime: datetime, include_timezone: bool = False) -> str ) -_ABBREVIATED_TZ_OFFSET_PATTERN = re.compile(r"([+-])(\d{2})$") +_ABBREVIATED_TZ_OFFSET_PATTERN = re.compile(r"(:\d{2}(?:\.\d+)?)([+-])(\d{2})$") def _normalize_timezone_offset(time_string: str) -> str: - return _ABBREVIATED_TZ_OFFSET_PATTERN.sub(r"\1\2:00", time_string) + return _ABBREVIATED_TZ_OFFSET_PATTERN.sub(r"\1\2\3:00", time_string) def convert_partial_iso_format_to_full_iso_format(partial_iso_format_time: str) -> str: From c1dbe6435d42919e7606140f96f23dddd53cfe8d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 17 Feb 2026 22:24:38 +0000 Subject: [PATCH 3/3] test: add parametrized tests for convert_partial_iso_format_to_full_iso_format Covers abbreviated offsets (+00, -05), fractional seconds, full offsets, no-offset input, and date-only strings to prevent regressions. Co-Authored-By: Itamar Hartstein --- tests/unit/utils/test_time.py | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/tests/unit/utils/test_time.py b/tests/unit/utils/test_time.py index f2e010803..37499b357 100644 --- a/tests/unit/utils/test_time.py +++ b/tests/unit/utils/test_time.py @@ -1,8 +1,10 @@ from datetime import datetime +import pytest from dateutil import tz from elementary.utils.time import ( + convert_partial_iso_format_to_full_iso_format, convert_time_to_timezone, datetime_strftime, get_formatted_timedelta, @@ -67,3 +69,49 @@ def test_convert_time_to_timezone(): assert date.hour == 1 assert date_with_timezone.hour == 2 assert (date - date_with_timezone).total_seconds() == 0 + + +@pytest.mark.parametrize( + "input_time, expected_output", + [ + pytest.param( + "2024-01-01T12:00:00+00", + "2024-01-01T12:00:00+00:00", + id="abbreviated_utc_offset", + ), + pytest.param( + "2024-01-01T12:00:00-05", + "2024-01-01T12:00:00-05:00", + id="abbreviated_negative_offset", + ), + pytest.param( + "2024-01-01 12:00:00.123456+00", + "2024-01-01T12:00:00+00:00", + id="abbreviated_offset_with_fractional_seconds", + ), + pytest.param( + "2024-01-01T12:00:00+00:00", + "2024-01-01T12:00:00+00:00", + id="full_utc_offset", + ), + pytest.param( + "2024-01-01T12:00:00+05:30", + "2024-01-01T12:00:00+05:30", + id="full_non_utc_offset", + ), + pytest.param( + "2024-01-01T12:00:00", + "2024-01-01T12:00:00+00:00", + id="no_offset_defaults_to_utc", + ), + pytest.param( + "2024-01-15", + "2024-01-15T00:00:00+00:00", + id="date_only_not_corrupted", + ), + ], +) +def test_convert_partial_iso_format_to_full_iso_format( + input_time: str, expected_output: str +) -> None: + assert convert_partial_iso_format_to_full_iso_format(input_time) == expected_output