|
10 | 10 |
|
11 | 11 | RE_TIMEZONE = re.compile(r"(\d{2}):(\d{2})$")
|
12 | 12 |
|
| 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 | + |
13 | 21 |
|
14 | 22 | class BitbucketBase(AtlassianRestAPI):
|
15 | 23 | CONF_TIMEFORMAT = "%Y-%m-%dT%H:%M:%S.%f%z"
|
@@ -160,35 +168,53 @@ def get_time(self, id):
|
160 | 168 | return value_str
|
161 | 169 |
|
162 | 170 | 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. |
175 | 175 | 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: |
182 | 177 | 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 |
187 | 188 |
|
188 |
| - # If parsing failed for all formats, leave the original string |
189 |
| - # intact so the timeformat lambda can decide what to do with it. |
190 | 189 | 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 |
192 | 218 | else:
|
193 | 219 | value = value_str
|
194 | 220 |
|
|
0 commit comments