Created
March 24, 2017 17:13
-
-
Save samuelcolvin/a961116e6723f0e3b97a4059650d27be to your computer and use it in GitHub Desktop.
toolbox of useful methods for working with datetimes in python
This file contains hidden or 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
| """ | |
| Useful utilities when working with datetimes, written for and tested with python 3.6 | |
| With other versions of python you're on your own. | |
| (should work find with at least 3.5 though) | |
| """ | |
| from datetime import datetime, date, timedelta, timezone, tzinfo | |
| def set_timezone(dt: datetime, tz: tzinfo) -> datetime: | |
| """ | |
| Sets the timezone on a datetime such that the date and time numbers | |
| do not change. This may mean the point in time of the datetime changes. eg.: | |
| 2017-03-01 12:00:00 Europe/London will become 2017-03-01 12:00:00 US/Eastern | |
| or | |
| 2017-06-02 11:00:00 Naive will become 2017-06-02 11:00:00 US/Eastern | |
| :param dt: datetime.datetime | |
| :param tz: timezone to use | |
| """ | |
| if hasattr(tz, 'localize'): | |
| return tz.localize(dt.replace(tzinfo=None)) | |
| else: | |
| return dt.replace(tzinfo=tz) | |
| def move_timezone(dt: datetime, tz: tzinfo) -> datetime: | |
| """ | |
| Move a datetime from one timezone to another so it's point in time remains | |
| unchanged. This only works with timezone aware datetimes. | |
| :param dt: datetime.datetime | |
| :param tz: timezone to use | |
| """ | |
| assert dt.tzinfo is not None, 'move_timezone cannot be used with naive datetimes' | |
| return dt.astimezone(tz) | |
| def date2datetime(date: date, tz: tzinfo, day_end=False) -> datetime: | |
| """ | |
| convert date to datetime in given timezone. | |
| :param date: datetime.date | |
| :param tz: timezone to use for new datetime | |
| :param day_end: if true 23.59.59 is used for the time | |
| """ | |
| hms = (23, 59, 59) if day_end else (0, 0, 0) | |
| return set_timezone(datetime(date.year, date.month, date.day, *hms), tz) | |
| EPOCH = datetime(1970, 1, 1) | |
| EPOCH_TZ = EPOCH.replace(tzinfo=timezone.utc) | |
| def to_unix_seconds(dt: datetime) -> float: | |
| """ | |
| (Taken broadly from https://github.com/samuelcolvin/arq/blob/master/arq/utils.py) | |
| convert a datetime to number of milliseconds since 1970 | |
| :param dt: datetime to evaluate | |
| :return: unix time in seconds | |
| """ | |
| if dt.tzinfo is not None: | |
| return (dt - EPOCH_TZ).total_seconds() | |
| else: | |
| return (dt - EPOCH).total_seconds() | |
| def from_unix_seconds(seconds: int, tz: tzinfo=None) -> datetime: | |
| """ | |
| convert int to a datetime. | |
| :param seconds: number of seconds since 1970 | |
| :param tz: if set a timezone to return the dt in | |
| """ | |
| dt = EPOCH + timedelta(seconds=seconds) | |
| if tz: | |
| return move_timezone(dt.replace(tzinfo=timezone.utc), tz) | |
| else: | |
| return dt |
This file contains hidden or 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
| """ | |
| Tests for datetime_toolbox.py | |
| Run with | |
| TZ=Asia/Singapore pytest test_datetime_toolbox.py | |
| """ | |
| import os | |
| from datetime import datetime, timedelta, timezone | |
| import pytest | |
| import pytz | |
| from datetime_toolbox import set_timezone, move_timezone, date2datetime, to_unix_seconds, from_unix_seconds | |
| toronto = pytz.timezone('America/Toronto') | |
| london = pytz.timezone('Europe/London') | |
| custom_tz = timezone(timedelta(seconds=3600 * 3.2), name='FOO') | |
| @pytest.mark.parametrize('dt, tz, expected', [ | |
| (datetime(2017, 6, 6, 10), timezone.utc, '17-06-06 10:00:00 UTC +0000'), | |
| (toronto.localize(datetime(2017, 6, 6, 10)), timezone.utc, '17-06-06 10:00:00 UTC +0000'), | |
| (toronto.localize(datetime(2017, 6, 6, 10)), london, '17-06-06 10:00:00 BST +0100'), | |
| (toronto.localize(datetime(2017, 11, 6, 10)), london, '17-11-06 10:00:00 GMT +0000'), | |
| (datetime(2017, 6, 6, 10, tzinfo=timezone.utc), toronto, '17-06-06 10:00:00 EDT -0400'), | |
| (london.localize(datetime(2017, 6, 6, 10)), toronto, '17-06-06 10:00:00 EDT -0400'), | |
| (datetime(2017, 6, 6, 10, tzinfo=custom_tz), toronto, '17-06-06 10:00:00 EDT -0400'), | |
| (toronto.localize(datetime(2017, 6, 6, 10)), custom_tz, '17-06-06 10:00:00 FOO +0312'), | |
| ]) | |
| def test_set_timezone(dt, tz, expected): | |
| new_dt = set_timezone(dt, tz) | |
| assert new_dt.strftime('%y-%m-%d %H:%M:%S %Z %z') == expected | |
| @pytest.mark.parametrize('dt, tz, expected', [ | |
| (toronto.localize(datetime(2017, 6, 6, 10)), timezone.utc, '17-06-06 14:00:00 UTC +0000'), | |
| (toronto.localize(datetime(2017, 6, 6, 10)), london, '17-06-06 15:00:00 BST +0100'), | |
| (toronto.localize(datetime(2017, 11, 6, 10)), london, '17-11-06 15:00:00 GMT +0000'), | |
| (datetime(2017, 6, 6, 10, tzinfo=timezone.utc), toronto, '17-06-06 06:00:00 EDT -0400'), | |
| (london.localize(datetime(2017, 6, 6, 10)), toronto, '17-06-06 05:00:00 EDT -0400'), | |
| (datetime(2017, 6, 6, 10, tzinfo=custom_tz), toronto, '17-06-06 02:48:00 EDT -0400'), | |
| (datetime(2017, 6, 6, 10, tzinfo=timezone.utc), custom_tz, '17-06-06 13:12:00 FOO +0312'), | |
| (london.localize(datetime(2017, 6, 6, 10)), custom_tz, '17-06-06 12:12:00 FOO +0312'), | |
| (toronto.localize(datetime(2017, 6, 6, 10)), custom_tz, '17-06-06 17:12:00 FOO +0312'), | |
| ]) | |
| def test_move_timezone(dt, tz, expected): | |
| new_dt = move_timezone(dt, tz) | |
| assert new_dt.strftime('%y-%m-%d %H:%M:%S %Z %z') == expected | |
| def test_set_timezone_error(): | |
| with pytest.raises(AssertionError) as exc_info: | |
| move_timezone(datetime(2017, 6, 6, 10), timezone.utc) | |
| assert exc_info.value.args[0] == 'move_timezone cannot be used with naive datetimes' | |
| @pytest.mark.parametrize('dt, tz, day_end, expected', [ | |
| (toronto.localize(datetime(2017, 6, 6, 10)), timezone.utc, False, '17-06-06 00:00:00 UTC +0000'), | |
| (toronto.localize(datetime(2017, 6, 6, 10)), timezone.utc, True, '17-06-06 23:59:59 UTC +0000'), | |
| (toronto.localize(datetime(2017, 6, 6)), timezone.utc, False, '17-06-06 00:00:00 UTC +0000'), | |
| (toronto.localize(datetime(2017, 6, 6)), timezone.utc, True, '17-06-06 23:59:59 UTC +0000'), | |
| ]) | |
| def test_date2datetime(dt, tz, day_end, expected): | |
| new_dt = date2datetime(dt, tz, day_end) | |
| assert new_dt.strftime('%y-%m-%d %H:%M:%S %Z %z') == expected | |
| UNIX1 = 1496743200.0 | |
| @pytest.mark.parametrize('dt, expected', [ | |
| (datetime(1970, 1, 1), 0), | |
| (datetime(1970, 1, 1, 1), 3600), | |
| (move_timezone(datetime(1970, 1, 1, 10, tzinfo=timezone.utc), toronto), 3600 * 10), | |
| (set_timezone(datetime(1970, 1, 1, 10, tzinfo=timezone.utc), london), 3600 * 9), | |
| (datetime(2017, 6, 6, 10), UNIX1), | |
| (datetime(2017, 6, 6, 10, tzinfo=timezone.utc), UNIX1), | |
| (move_timezone(datetime(2017, 6, 6, 10, tzinfo=timezone.utc), toronto), UNIX1), | |
| (set_timezone(datetime(2017, 6, 6, 10, tzinfo=timezone.utc), toronto), UNIX1 + 4 * 3600), | |
| ]) | |
| def test_to_unix_seconds(dt, expected): | |
| assert os.getenv('TZ') == 'Asia/Singapore', 'tests should always be run with TZ=Asia/Singapore' | |
| assert 7.99 < (datetime.now() - datetime.utcnow()).total_seconds() / 3600 < 8.01, ('timezone not set to ' | |
| 'Asia/Singapore') | |
| unix_stamp = to_unix_seconds(dt) | |
| assert unix_stamp == expected | |
| @pytest.mark.parametrize('s, tz, expected', [ | |
| (UNIX1, None, '17-06-06 10:00:00 '), | |
| (UNIX1, timezone.utc, '17-06-06 10:00:00 UTC +0000'), | |
| (UNIX1, toronto, '17-06-06 06:00:00 EDT -0400'), | |
| (UNIX1 + 4 * 3600, toronto, '17-06-06 10:00:00 EDT -0400'), | |
| ]) | |
| def test_from_unix_seconds(s, tz, expected): | |
| new_dt = from_unix_seconds(s, tz) | |
| assert new_dt.strftime('%y-%m-%d %H:%M:%S %Z %z') == expected |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment