Skip to content

Commit 0bb19a8

Browse files
committed
[Bitbucket] Robust datetime parsing for commit dates (#1589)
Normalize timestamps like 'YYYY-MM-DDTHH:MM:SS+00:00Z', handle Python <3.7 %z format, and try multiple datetime formats.
1 parent cfbc483 commit 0bb19a8

File tree

2 files changed

+51
-27
lines changed

2 files changed

+51
-27
lines changed

atlassian/bitbucket/base.py

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@
1010

1111
RE_TIMEZONE = re.compile(r"(\d{2}):(\d{2})$")
1212

13+
# dateutil is optional but handles many ISO8601 variants more robustly than
14+
# strptime across Python versions. Import if available and fall back to the
15+
# built-in parsing logic below when it's not.
16+
try:
17+
from dateutil import parser as _dateutil_parser # type: ignore
18+
except Exception: # pragma: no cover - optional dependency
19+
_dateutil_parser = None
20+
1321

1422
class BitbucketBase(AtlassianRestAPI):
1523
CONF_TIMEFORMAT = "%Y-%m-%dT%H:%M:%S.%f%z"
@@ -160,35 +168,53 @@ def get_time(self, id):
160168
return value_str
161169

162170
if isinstance(value_str, str):
163-
# Normalize timestamps that include a timezone followed by an
164-
# extraneous 'Z' (e.g. '2025-09-18T21:26:38+00:00Z') by removing the
165-
# final 'Z'. This pattern appears in some Bitbucket responses.
166-
if re.search(r"[+-]\d{2}:\d{2}Z$", value_str):
167-
value_str = value_str[:-1]
168-
169-
# Python < 3.7 does not accept a ':' in the %z timezone, so strip
170-
# it for older interpreters.
171-
if sys.version_info < (3, 7):
172-
value_str = RE_TIMEZONE.sub(r"\1\2", value_str)
173-
174-
# Try several likely formats, from most to least specific.
171+
# Try to use dateutil when available because it correctly handles
172+
# many ISO8601 edge cases across Python versions (for example,
173+
# '2025-09-18T21:26:38+00:00Z'). If dateutil isn't present or it
174+
# fails to parse, fall back to the existing strptime-based logic.
175175
value = None
176-
for fmt in (
177-
"%Y-%m-%dT%H:%M:%S.%f%z",
178-
"%Y-%m-%dT%H:%M:%S%z",
179-
"%Y-%m-%dT%H:%M:%S.%f",
180-
"%Y-%m-%dT%H:%M:%S",
181-
):
176+
if _dateutil_parser is not None:
182177
try:
183-
value = datetime.strptime(value_str, fmt)
184-
break
185-
except ValueError:
186-
continue
178+
# isoparse is preferable for strict ISO parsing when
179+
# available; otherwise fall back to parse.
180+
if hasattr(_dateutil_parser, "isoparse"):
181+
value = _dateutil_parser.isoparse(value_str)
182+
else:
183+
value = _dateutil_parser.parse(value_str)
184+
except Exception:
185+
# If dateutil can't parse it for any reason, we'll
186+
# continue to the manual fallback below.
187+
value = None
187188

188-
# If parsing failed for all formats, leave the original string
189-
# intact so the timeformat lambda can decide what to do with it.
190189
if value is None:
191-
value = value_str
190+
# Normalize timestamps that include a timezone followed by an
191+
# extraneous 'Z' (e.g. '2025-09-18T21:26:38+00:00Z') by removing the
192+
# final 'Z'. This pattern appears in some Bitbucket responses.
193+
if re.search(r"[+-]\d{2}:\d{2}Z$", value_str):
194+
value_str = value_str[:-1]
195+
196+
# Python < 3.7 does not accept a ':' in the %z timezone, so strip
197+
# it for older interpreters.
198+
if sys.version_info < (3, 7):
199+
value_str = RE_TIMEZONE.sub(r"\1\2", value_str)
200+
201+
# Try several likely formats, from most to least specific.
202+
for fmt in (
203+
"%Y-%m-%dT%H:%M:%S.%f%z",
204+
"%Y-%m-%dT%H:%M:%S%z",
205+
"%Y-%m-%dT%H:%M:%S.%f",
206+
"%Y-%m-%dT%H:%M:%S",
207+
):
208+
try:
209+
value = datetime.strptime(value_str, fmt)
210+
break
211+
except ValueError:
212+
continue
213+
214+
# If parsing failed for all formats, leave the original string
215+
# intact so the timeformat lambda can decide what to do with it.
216+
if value is None:
217+
value = value_str
192218
else:
193219
value = value_str
194220

tests/test_bug_reproduction.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import pytest
2-
31
from atlassian.bitbucket.cloud.repositories.commits import Commit
42

53

0 commit comments

Comments
 (0)