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))
Format for time-strings (used with time.strftime
and time.strptime
).
A tzinfo object from dateutil.tz
representing Universal Time,
Coordinated.
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.
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.
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.
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.
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.
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.
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.
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.
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
).
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.