Created
August 1, 2021 18:17
-
-
Save jharley/f790972698ce0e917b3f2f9eb7402bbd to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import warnings | |
from datetime import datetime, timedelta, timezone | |
import pytest | |
from .unixdatetime import UnixDatetime | |
@pytest.mark.parametrize( | |
"epoch_seconds, dt_time", | |
[ | |
# UNIX Epoch is Jan 01 1970 00:00:00 GMT | |
(0, datetime(1970, 1, 1, tzinfo=timezone.utc)), | |
(-1, datetime(1969, 12, 31, 23, 59, 59, tzinfo=timezone.utc)), | |
(119731017, datetime(1973, 10, 17, 18, 36, 57, tzinfo=timezone.utc)), | |
# The Unix "billennium" | |
(1000000000, datetime(2001, 9, 9, 1, 46, 40, tzinfo=timezone.utc)), | |
# 32-bit signed int overflow time | |
(2147483647, datetime(2038, 1, 19, 3, 14, 7, tzinfo=timezone.utc)), | |
# 32-bit unsigned int overflow time | |
(4294967295, datetime(2106, 2, 7, 6, 28, 15, tzinfo=timezone.utc)), | |
# above plus one :) | |
(4294967296, datetime(2106, 2, 7, 6, 28, 16, tzinfo=timezone.utc)), | |
], | |
) | |
def test_unixdatetime(epoch_seconds: int, dt_time: datetime) -> None: | |
udt = UnixDatetime(epoch_seconds) | |
assert epoch_seconds == udt | |
assert dt_time == udt | |
assert udt.to_datetime().tzinfo == timezone.utc | |
assert udt == UnixDatetime.from_datetime(dt_time) | |
def test_unixdatetime_from_naive_datetime() -> None: | |
with warnings.catch_warnings(record=True) as wmsg: | |
# ensure all warnings triggered | |
warnings.simplefilter("always") | |
epoch = UnixDatetime.from_datetime(datetime(1970, 1, 1)) | |
assert epoch == 0 | |
assert len(wmsg) == 1 | |
assert issubclass(wmsg[-1].category, UserWarning) | |
assert "Converting non-timezone aware datetime object, assuming UTC" in str( | |
wmsg[-1].message | |
) | |
def test_unixdatetime_from_nonutc_datetime() -> None: | |
# March 30, 2021 09:30:00 GMT-4 | |
edt = datetime(2021, 3, 30, 9, 30, tzinfo=timezone(timedelta(hours=-4), "EDT")) | |
# generated at unixtimestamp.com | |
edt_epoch_secs = 1617111000 | |
udt = UnixDatetime.from_datetime(edt) | |
assert udt == edt_epoch_secs |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from datetime import datetime, timezone | |
import time | |
import warnings | |
from pydantic import StrictInt | |
class UnixDatetime(StrictInt): | |
""" | |
UnixDatetime objects represent the number of seconds that have passsed since the | |
UNIX epoch: January 1, 1970 00:00:00 UTC | |
Intended to be used as a Pyndantic type, perhaps helpful elsewhere | |
""" | |
def __repr__(self) -> str: | |
return f"UnixDatetime({self.__str__()})" | |
def __str__(self) -> str: | |
return f"{self.to_datetime()}" | |
def __eq__(self, other: Any) -> bool: | |
if isinstance(other, datetime): | |
return self.to_datetime() == other | |
else: | |
return super().__eq__(other) | |
def __lt__(self, other: Any) -> bool: | |
if isinstance(other, datetime): | |
return self.to_datetime() < other | |
else: | |
return super().__lt__(other) | |
def __le__(self, other: Any) -> bool: | |
if isinstance(other, datetime): | |
return self.to_datetime() <= other | |
else: | |
return super().__le__(other) | |
def __gt__(self, other: Any) -> bool: | |
if isinstance(other, datetime): | |
return self.to_datetime() > other | |
else: | |
return super().__gt__(other) | |
def __ge__(self, other: Any) -> bool: | |
if isinstance(other, datetime): | |
return self.to_datetime() >= other | |
else: | |
return super().__ge__(other) | |
def to_datetime(self) -> datetime: | |
""" | |
Returns the UnixDatetime as a timezone-aware datetime object | |
""" | |
# Time in Python is a hot mess, and time.mktime assumes localtime(?!) | |
# doing this to ensure the resulting datetime has a proper timezone. | |
return datetime(*time.gmtime(self)[:6], tzinfo=timezone.utc) | |
@classmethod | |
def from_datetime(cls, date: datetime) -> "UnixDatetime": | |
""" | |
Converts a datetime object into a UnixDatetime | |
Naive (non-timezone aware) datetime objects will have their timezones | |
assumed to be UTC. | |
Non-UTC datetime objects will are properly converted to UTC as part of | |
the conversion. | |
""" | |
if not date.tzinfo: | |
warnings.warn("Converting non-timezone aware datetime object, assuming UTC") | |
date = date.replace(tzinfo=timezone.utc) | |
elif date.tzinfo.utc != timezone.utc: | |
date = date.astimezone(tz=timezone.utc) | |
return cls(date.timestamp()) | |
@classmethod | |
def __get_valdiators__(cls): | |
yield cls.validate | |
@classmethod | |
def __modify_schema__(cls, field_schema): | |
# __modify_schema__ should mutate the dict it receives in place, | |
# the returned value will be ignored | |
field_schema.update( | |
pattern=r"^\d*$", | |
examples=[0, 119731017], | |
) | |
@classmethod | |
def validate(cls, value) -> "UnixDatetime": | |
if not isinstance(value, int): | |
raise TypeError("Not an integer") | |
return cls(value) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment