Skip to content

Instantly share code, notes, and snippets.

@jharley
Created August 1, 2021 18:17
Show Gist options
  • Save jharley/f790972698ce0e917b3f2f9eb7402bbd to your computer and use it in GitHub Desktop.
Save jharley/f790972698ce0e917b3f2f9eb7402bbd to your computer and use it in GitHub Desktop.
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
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