Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions tests/test_api_client/test_deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,25 @@ def test_deserialize_datetime_ms_error(data):
deserialize_datetime_ms(data_type, data, model_finder=None)


def test_deserialize_datetime_ms_pre_epoch():
# regression for #59: a timestamp before 1970 (negative ms) must convert
# cleanly on every platform instead of leaking OSError (datetime.fromtimestamp
# rejects negative timestamps on Windows).
result = deserialize_datetime_ms(
"datetime[ms-format]", "/Date(-2208988800000)/", model_finder=None
)
assert result == datetime(1900, 1, 1, tzinfo=tz.UTC)


def test_deserialize_datetime_ms_out_of_range_raises_value_error():
# regression for #59: an out-of-range timestamp must raise the documented
# ValueError, not leak OSError / OverflowError from the platform C library.
with pytest.raises(ValueError):
deserialize_datetime_ms(
"datetime[ms-format]", "/Date(67768036191676800000)/", model_finder=None
)


# deserialize_model tests
def test_deserialize_model():
# given test model and test data
Expand Down
13 changes: 11 additions & 2 deletions xero_python/api_client/deserializer.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,8 +250,17 @@ def deserialize_datetime_ms(data_type, data, model_finder):
tz_info = tz.UTC

timestamp_ms = int(match.groupdict()["timestamp"])
timestamp_s = timestamp_ms / 1000
return datetime.datetime.fromtimestamp(timestamp_s, tz=tz_info)
# datetime.fromtimestamp is bounded by the platform's C library and
# leaks OSError/OverflowError for out-of-range or (on Windows)
# pre-1970 timestamps. Build the datetime from the epoch instead so
# the conversion is platform independent, and surface the documented
# ValueError for values python's datetime cannot represent.
epoch = datetime.datetime(1970, 1, 1, tzinfo=tz.UTC)
try:
utc_datetime = epoch + datetime.timedelta(milliseconds=timestamp_ms)
except (OverflowError, OSError) as error:
raise ValueError("Invalid datetime value {!r}".format(data)) from error
return utc_datetime.astimezone(tz_info)
elif DATE_WITH_NO_DAY_RE.match(str(data)):
return datetime.datetime.strptime(data + "-01", "%Y-%m-%d")
else:
Expand Down