potluck.time_utils

Time and date management utilities.

time_utils.py

  1"""
  2Time and date management utilities.
  3
  4time_utils.py
  5"""
  6
  7import time
  8import datetime
  9
 10# Needed for better timezone handling, or ANY timezone handling in 2.7
 11import dateutil.tz
 12
 13
 14#---------#
 15# Globals #
 16#---------#
 17
 18TIMESTRING_FORMAT = "%Y%m%d@%H:%M:%S(UTC)"
 19"""
 20Format for time-strings (used with `time.strftime` and `time.strptime`).
 21"""
 22
 23UTC = dateutil.tz.tzutc()
 24"""
 25A tzinfo object from `dateutil.tz` representing Universal Time,
 26Coordinated.
 27"""
 28
 29
 30#-----------------#
 31# Time management #
 32#-----------------#
 33
 34def now():
 35    """
 36    Returns a time-zone-aware datetime representing the current time.
 37    """
 38    return datetime.datetime.now(UTC)
 39
 40
 41def timestring(when=None):
 42    """
 43    Returns a time string based on the current time, or based on a given
 44    datetime.datetime object.
 45
 46    The time is always converted to UTC first.
 47    """
 48    if when is None:
 49        when = now()
 50
 51    # Ensure UTC timezone
 52    if when.tzinfo is None:
 53        when = when.replace(tzinfo=UTC)
 54    else:
 55        when = when.astimezone(UTC)
 56    return when.strftime(TIMESTRING_FORMAT)
 57
 58
 59def time_from_timestamp(timestamp):
 60    """
 61    Converts a timestamp value into a timezone-aware datetime.datetime
 62    which will always be in UTC.
 63    """
 64    result = datetime.datetime.fromtimestamp(timestamp)
 65    if result.tzinfo is None:
 66        result = result.replace(tzinfo=UTC)
 67    else:
 68        result = result.astimezone(UTC)
 69    return result
 70
 71
 72def time_from_timestring(timestring):
 73    """
 74    Converts a time string back into a (timezone-aware)
 75    datetime.datetime. The resulting datetime object is always in UTC.
 76    """
 77    # Version that includes a timezone
 78    result = datetime.datetime.strptime(timestring, TIMESTRING_FORMAT)
 79
 80    # Ensure we're a TZ-aware object in UTC
 81    if result.tzinfo is None:
 82        result = result.replace(tzinfo=UTC)
 83    else:
 84        result = result.astimezone(UTC)
 85    return result
 86
 87
 88def fmt_datetime(when):
 89    """
 90    Formats a datetime using 24-hour notation w/ extra a.m./p.m.
 91    annotations in the morning for clarity, and a timezone attached.
 92    """
 93    # Use a.m. for extra clarity when hour < 12, and p.m. for 12:XX
 94    am_hint = ''
 95    if when.hour < 12:
 96        am_hint = ' a.m.'
 97    elif when.hour == 12:
 98        am_hint = ' p.m.'
 99
100    tz = when.strftime("%Z")
101    if tz != '':
102        tz = ' ' + tz
103    return when.strftime("at %H:%M{}{} on %Y-%m-%d".format(am_hint, tz))
104
105
106def at_time(time_obj):
107    """
108    Uses `fmt_datetime` to produce a string, but accepts and converts
109    `datetime` objects, `struct_time` objects, and time-strings.
110    """
111    if isinstance(time_obj, datetime.datetime):
112        return fmt_datetime(time_obj)
113    elif isinstance(time_obj, (int, float)):
114        return fmt_datetime(time_from_timestamp(time_obj))
115    elif isinstance(time_obj, str): # we assume it's a time string
116        return fmt_datetime(time_from_timestring(time_obj))
117    else:
118        raise TypeError(
119            "Cannot convert a {} ({}) to a time value.".format(
120                type(time_obj),
121                repr(time_obj)
122            )
123        )
124
125
126def task_time__time(
127    tasks_data,
128    time_string,
129    default_time_of_day=None,
130    default_tz=None
131):
132    """
133    Converts a time string from task info into a time value. Uses
134    str__time with the default time zone, hours, and minutes from the
135    task data.
136
137    Requires a tasks data dictionary (loaded from tasks.json) from which
138    it can get a "default_time_of_day" and/or "default_tz" value in case
139    an explicit default_time_of_day is not specified. The time string to
140    convert is also required.
141
142    See `local_timezone` for information on how a timezone is derived
143    from the tasks data.
144    """
145    if default_time_of_day is None:
146        default_time_of_day = tasks_data.get("default_time_of_day", "23:59")
147
148    if default_tz is None:
149        default_tz = local_timezone(tasks_data)
150
151    hd = int(default_time_of_day.split(':')[0])
152    md = int(default_time_of_day.split(':')[1])
153
154    return str__time(
155        time_string,
156        default_hour=hd,
157        default_minute=md,
158        default_tz=default_tz
159    )
160
161
162def str__time(
163    tstr,
164    default_hour=23,
165    default_minute=59,
166    default_second=59,
167    default_tz=UTC
168):
169    """
170    Converts a string to a datetime object. Default format is:
171
172    yyyy-mm-dd HH:MM:SS TZ
173
174    The hours, minutes, seconds, and timezone are optional.
175
176    Timezone must be given as +HHMM or -HHMM (e.g., -0400 for 4-hours
177    after UTC).
178
179    Hours/minutes/seconds default to the end of the given day/hour/minute
180    (i.e., 23:59:59), not to 00:00:00, unless alternative defaults are
181    specified.
182    """
183    formats = [
184        ("%Y-%m-%d %H:%M:%S %z", {}),
185        ("%Y-%m-%d %H:%M:%S", {"tzinfo": default_tz}),
186        ("%Y-%m-%d %H:%M %z", {"second": default_second}),
187        ("%Y-%m-%d %H:%M", {"tzinfo": default_tz, "second": default_second}),
188        (
189            "%Y-%m-%d",
190            {
191                "tzinfo": default_tz,
192                "second": default_second,
193                "minute": default_minute,
194                "hour": default_hour
195            }
196        )
197    ]
198    result = None
199    for f, defaults in formats:
200        try:
201            # TODO: Some way to ward against very occasional
202            # threading-related AttributeErrors when this is used in the
203            # server???
204            result = datetime.datetime.fromtimestamp(
205                time.mktime(time.strptime(tstr, f))
206            )
207        except ValueError:
208            pass
209
210        if result is not None:
211            result = result.replace(**defaults)
212            break
213
214    if result is None:
215        raise ValueError("Couldn't parse time data: '{}'".format(tstr))
216
217    return result
218
219
220def local_timezone(tasks_data):
221    """
222    Returns the timezone object implied by the settings in the given
223    tasks data.
224
225    The tasks data's "timezone" slot will be used; its value should be
226    a string that identifies a timezone, like "UTC" or "America/New_York"
227    (values are given to
228    [`dateutil.tz.gettz`](https://dateutil.readthedocs.io/en/stable/tz.html#dateutil.tz.gettz)).
229    """
230    return dateutil.tz.gettz(tasks_data.get("timezone"))
231
232
233def local_time(
234    tasks_data,
235    time_obj
236):
237    """
238    Given access to the tasks data object and a datetime, returns an
239    equivalent datetime with the timezone set to the time zone specified
240    by the tasks data. Uses UTC if the tasks data does not specify a
241    timezone.
242    """
243    return time_obj.astimezone(local_timezone(tasks_data))
TIMESTRING_FORMAT = '%Y%m%d@%H:%M:%S(UTC)'

Format for time-strings (used with time.strftime and time.strptime).

UTC = tzutc()

A tzinfo object from dateutil.tz representing Universal Time, Coordinated.

def now():
35def now():
36    """
37    Returns a time-zone-aware datetime representing the current time.
38    """
39    return datetime.datetime.now(UTC)

Returns a time-zone-aware datetime representing the current time.

def timestring(when=None):
42def timestring(when=None):
43    """
44    Returns a time string based on the current time, or based on a given
45    datetime.datetime object.
46
47    The time is always converted to UTC first.
48    """
49    if when is None:
50        when = now()
51
52    # Ensure UTC timezone
53    if when.tzinfo is None:
54        when = when.replace(tzinfo=UTC)
55    else:
56        when = when.astimezone(UTC)
57    return when.strftime(TIMESTRING_FORMAT)

Returns a time string based on the current time, or based on a given datetime.datetime object.

The time is always converted to UTC first.

def time_from_timestamp(timestamp):
60def time_from_timestamp(timestamp):
61    """
62    Converts a timestamp value into a timezone-aware datetime.datetime
63    which will always be in UTC.
64    """
65    result = datetime.datetime.fromtimestamp(timestamp)
66    if result.tzinfo is None:
67        result = result.replace(tzinfo=UTC)
68    else:
69        result = result.astimezone(UTC)
70    return result

Converts a timestamp value into a timezone-aware datetime.datetime which will always be in UTC.

def time_from_timestring(timestring):
73def time_from_timestring(timestring):
74    """
75    Converts a time string back into a (timezone-aware)
76    datetime.datetime. The resulting datetime object is always in UTC.
77    """
78    # Version that includes a timezone
79    result = datetime.datetime.strptime(timestring, TIMESTRING_FORMAT)
80
81    # Ensure we're a TZ-aware object in UTC
82    if result.tzinfo is None:
83        result = result.replace(tzinfo=UTC)
84    else:
85        result = result.astimezone(UTC)
86    return result

Converts a time string back into a (timezone-aware) datetime.datetime. The resulting datetime object is always in UTC.

def fmt_datetime(when):
 89def fmt_datetime(when):
 90    """
 91    Formats a datetime using 24-hour notation w/ extra a.m./p.m.
 92    annotations in the morning for clarity, and a timezone attached.
 93    """
 94    # Use a.m. for extra clarity when hour < 12, and p.m. for 12:XX
 95    am_hint = ''
 96    if when.hour < 12:
 97        am_hint = ' a.m.'
 98    elif when.hour == 12:
 99        am_hint = ' p.m.'
100
101    tz = when.strftime("%Z")
102    if tz != '':
103        tz = ' ' + tz
104    return when.strftime("at %H:%M{}{} on %Y-%m-%d".format(am_hint, tz))

Formats a datetime using 24-hour notation w/ extra a.m./p.m. annotations in the morning for clarity, and a timezone attached.

def at_time(time_obj):
107def at_time(time_obj):
108    """
109    Uses `fmt_datetime` to produce a string, but accepts and converts
110    `datetime` objects, `struct_time` objects, and time-strings.
111    """
112    if isinstance(time_obj, datetime.datetime):
113        return fmt_datetime(time_obj)
114    elif isinstance(time_obj, (int, float)):
115        return fmt_datetime(time_from_timestamp(time_obj))
116    elif isinstance(time_obj, str): # we assume it's a time string
117        return fmt_datetime(time_from_timestring(time_obj))
118    else:
119        raise TypeError(
120            "Cannot convert a {} ({}) to a time value.".format(
121                type(time_obj),
122                repr(time_obj)
123            )
124        )

Uses fmt_datetime to produce a string, but accepts and converts datetime objects, struct_time objects, and time-strings.

def task_time__time(tasks_data, time_string, default_time_of_day=None, default_tz=None):
127def task_time__time(
128    tasks_data,
129    time_string,
130    default_time_of_day=None,
131    default_tz=None
132):
133    """
134    Converts a time string from task info into a time value. Uses
135    str__time with the default time zone, hours, and minutes from the
136    task data.
137
138    Requires a tasks data dictionary (loaded from tasks.json) from which
139    it can get a "default_time_of_day" and/or "default_tz" value in case
140    an explicit default_time_of_day is not specified. The time string to
141    convert is also required.
142
143    See `local_timezone` for information on how a timezone is derived
144    from the tasks data.
145    """
146    if default_time_of_day is None:
147        default_time_of_day = tasks_data.get("default_time_of_day", "23:59")
148
149    if default_tz is None:
150        default_tz = local_timezone(tasks_data)
151
152    hd = int(default_time_of_day.split(':')[0])
153    md = int(default_time_of_day.split(':')[1])
154
155    return str__time(
156        time_string,
157        default_hour=hd,
158        default_minute=md,
159        default_tz=default_tz
160    )

Converts a time string from task info into a time value. Uses str__time with the default time zone, hours, and minutes from the task data.

Requires a tasks data dictionary (loaded from tasks.json) from which it can get a "default_time_of_day" and/or "default_tz" value in case an explicit default_time_of_day is not specified. The time string to convert is also required.

See local_timezone for information on how a timezone is derived from the tasks data.

def str__time( tstr, default_hour=23, default_minute=59, default_second=59, default_tz=tzutc()):
163def str__time(
164    tstr,
165    default_hour=23,
166    default_minute=59,
167    default_second=59,
168    default_tz=UTC
169):
170    """
171    Converts a string to a datetime object. Default format is:
172
173    yyyy-mm-dd HH:MM:SS TZ
174
175    The hours, minutes, seconds, and timezone are optional.
176
177    Timezone must be given as +HHMM or -HHMM (e.g., -0400 for 4-hours
178    after UTC).
179
180    Hours/minutes/seconds default to the end of the given day/hour/minute
181    (i.e., 23:59:59), not to 00:00:00, unless alternative defaults are
182    specified.
183    """
184    formats = [
185        ("%Y-%m-%d %H:%M:%S %z", {}),
186        ("%Y-%m-%d %H:%M:%S", {"tzinfo": default_tz}),
187        ("%Y-%m-%d %H:%M %z", {"second": default_second}),
188        ("%Y-%m-%d %H:%M", {"tzinfo": default_tz, "second": default_second}),
189        (
190            "%Y-%m-%d",
191            {
192                "tzinfo": default_tz,
193                "second": default_second,
194                "minute": default_minute,
195                "hour": default_hour
196            }
197        )
198    ]
199    result = None
200    for f, defaults in formats:
201        try:
202            # TODO: Some way to ward against very occasional
203            # threading-related AttributeErrors when this is used in the
204            # server???
205            result = datetime.datetime.fromtimestamp(
206                time.mktime(time.strptime(tstr, f))
207            )
208        except ValueError:
209            pass
210
211        if result is not None:
212            result = result.replace(**defaults)
213            break
214
215    if result is None:
216        raise ValueError("Couldn't parse time data: '{}'".format(tstr))
217
218    return result

Converts a string to a datetime object. Default format is:

yyyy-mm-dd HH:MM:SS TZ

The hours, minutes, seconds, and timezone are optional.

Timezone must be given as +HHMM or -HHMM (e.g., -0400 for 4-hours after UTC).

Hours/minutes/seconds default to the end of the given day/hour/minute (i.e., 23:59:59), not to 00:00:00, unless alternative defaults are specified.

def local_timezone(tasks_data):
221def local_timezone(tasks_data):
222    """
223    Returns the timezone object implied by the settings in the given
224    tasks data.
225
226    The tasks data's "timezone" slot will be used; its value should be
227    a string that identifies a timezone, like "UTC" or "America/New_York"
228    (values are given to
229    [`dateutil.tz.gettz`](https://dateutil.readthedocs.io/en/stable/tz.html#dateutil.tz.gettz)).
230    """
231    return dateutil.tz.gettz(tasks_data.get("timezone"))

Returns the timezone object implied by the settings in the given tasks data.

The tasks data's "timezone" slot will be used; its value should be a string that identifies a timezone, like "UTC" or "America/New_York" (values are given to dateutil.tz.gettz).

def local_time(tasks_data, time_obj):
234def local_time(
235    tasks_data,
236    time_obj
237):
238    """
239    Given access to the tasks data object and a datetime, returns an
240    equivalent datetime with the timezone set to the time zone specified
241    by the tasks data. Uses UTC if the tasks data does not specify a
242    timezone.
243    """
244    return time_obj.astimezone(local_timezone(tasks_data))

Given access to the tasks data object and a datetime, returns an equivalent datetime with the timezone set to the time zone specified by the tasks data. Uses UTC if the tasks data does not specify a timezone.