|
9 | 9 |
|
10 | 10 | from __future__ import annotations |
11 | 11 |
|
| 12 | +import datetime as dt |
12 | 13 | from typing import TYPE_CHECKING, Any, Tuple |
13 | 14 |
|
14 | 15 | from docx.exceptions import InvalidXmlError |
@@ -213,6 +214,58 @@ def validate(cls, value: Any) -> None: |
213 | 214 | cls.validate_int_in_range(value, -27273042329600, 27273042316900) |
214 | 215 |
|
215 | 216 |
|
| 217 | +class ST_DateTime(BaseSimpleType): |
| 218 | + @classmethod |
| 219 | + def convert_from_xml(cls, str_value: str) -> dt.datetime: |
| 220 | + """Convert an xsd:dateTime string to a datetime object.""" |
| 221 | + |
| 222 | + def parse_xsd_datetime(dt_str: str) -> dt.datetime: |
| 223 | + # -- handle trailing 'Z' (Zulu/UTC), common in Word files -- |
| 224 | + if dt_str.endswith("Z"): |
| 225 | + try: |
| 226 | + # -- optional fractional seconds case -- |
| 227 | + return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%fZ").replace( |
| 228 | + tzinfo=dt.timezone.utc |
| 229 | + ) |
| 230 | + except ValueError: |
| 231 | + return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%SZ").replace( |
| 232 | + tzinfo=dt.timezone.utc |
| 233 | + ) |
| 234 | + |
| 235 | + # -- handles explicit offsets like +00:00, -05:00, or naive datetimes -- |
| 236 | + try: |
| 237 | + return dt.datetime.fromisoformat(dt_str) |
| 238 | + except ValueError: |
| 239 | + # -- fall-back to parsing as naive datetime (with or without fractional seconds) -- |
| 240 | + try: |
| 241 | + return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S.%f") |
| 242 | + except ValueError: |
| 243 | + return dt.datetime.strptime(dt_str, "%Y-%m-%dT%H:%M:%S") |
| 244 | + |
| 245 | + try: |
| 246 | + # -- parse anything reasonable, but never raise, just use default epoch time -- |
| 247 | + return parse_xsd_datetime(str_value) |
| 248 | + except Exception: |
| 249 | + return dt.datetime(1970, 1, 1, tzinfo=dt.timezone.utc) |
| 250 | + |
| 251 | + @classmethod |
| 252 | + def convert_to_xml(cls, value: dt.datetime) -> str: |
| 253 | + # -- convert naive datetime to timezon-aware assuming local timezone -- |
| 254 | + if value.tzinfo is None: |
| 255 | + value = value.astimezone() |
| 256 | + |
| 257 | + # -- convert to UTC if not already -- |
| 258 | + value = value.astimezone(dt.timezone.utc) |
| 259 | + |
| 260 | + # -- format with 'Z' suffix for UTC -- |
| 261 | + return value.strftime("%Y-%m-%dT%H:%M:%SZ") |
| 262 | + |
| 263 | + @classmethod |
| 264 | + def validate(cls, value: Any) -> None: |
| 265 | + if not isinstance(value, dt.datetime): |
| 266 | + raise TypeError("only a datetime.datetime object may be assigned, got '%s'" % value) |
| 267 | + |
| 268 | + |
216 | 269 | class ST_DecimalNumber(XsdInt): |
217 | 270 | pass |
218 | 271 |
|
|
0 commit comments