1# Copyright (C) 2018 Philipp Hörist <philipp AT hoerist.com> 2# 3# This file is part of nbxmpp. 4# 5# This program is free software; you can redistribute it and/or 6# modify it under the terms of the GNU General Public License 7# as published by the Free Software Foundation; either version 3 8# of the License, or (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; If not, see <http://www.gnu.org/licenses/>. 17 18import re 19import time 20import logging 21from datetime import datetime 22from datetime import timedelta 23from datetime import timezone 24from datetime import tzinfo 25 26log = logging.getLogger('nbxmpp.m.date_and_time') 27 28PATTERN_DATETIME = re.compile( 29 r'([0-9]{4}-[0-9]{2}-[0-9]{2})' 30 r'T' 31 r'([0-9]{2}:[0-9]{2}:[0-9]{2})' 32 r'(\.[0-9]{0,6})?' 33 r'(?:[0-9]+)?' 34 r'(?:(Z)|(?:([-+][0-9]{2}):([0-9]{2})))$' 35) 36 37PATTERN_DELAY = re.compile( 38 r'([0-9]{4}-[0-9]{2}-[0-9]{2})' 39 r'T' 40 r'([0-9]{2}:[0-9]{2}:[0-9]{2})' 41 r'(\.[0-9]{0,6})?' 42 r'(?:[0-9]+)?' 43 r'(?:(Z)|(?:([-+][0]{2}):([0]{2})))$' 44) 45 46 47ZERO = timedelta(0) 48HOUR = timedelta(hours=1) 49SECOND = timedelta(seconds=1) 50 51STDOFFSET = timedelta(seconds=-time.timezone) 52if time.daylight: 53 DSTOFFSET = timedelta(seconds=-time.altzone) 54else: 55 DSTOFFSET = STDOFFSET 56 57DSTDIFF = DSTOFFSET - STDOFFSET 58 59 60class LocalTimezone(tzinfo): 61 ''' 62 A class capturing the platform's idea of local time. 63 May result in wrong values on historical times in 64 timezones where UTC offset and/or the DST rules had 65 changed in the past. 66 ''' 67 def fromutc(self, dt): 68 assert dt.tzinfo is self 69 stamp = (dt - datetime(1970, 1, 1, tzinfo=self)) // SECOND 70 args = time.localtime(stamp)[:6] 71 dst_diff = DSTDIFF // SECOND 72 # Detect fold 73 fold = (args == time.localtime(stamp - dst_diff)) 74 return datetime(*args, microsecond=dt.microsecond, 75 tzinfo=self, fold=fold) 76 77 def utcoffset(self, dt): 78 if self._isdst(dt): 79 return DSTOFFSET 80 return STDOFFSET 81 82 def dst(self, dt): 83 if self._isdst(dt): 84 return DSTDIFF 85 return ZERO 86 87 def tzname(self, dt): 88 return 'local' 89 90 @staticmethod 91 def _isdst(dt): 92 tt = (dt.year, dt.month, dt.day, 93 dt.hour, dt.minute, dt.second, 94 dt.weekday(), 0, 0) 95 stamp = time.mktime(tt) 96 tt = time.localtime(stamp) 97 return tt.tm_isdst > 0 98 99 100def create_tzinfo(hours=0, minutes=0, tz_string=None): 101 if tz_string is None: 102 return timezone(timedelta(hours=hours, minutes=minutes)) 103 104 if tz_string.lower() == 'z': 105 return timezone.utc 106 107 try: 108 hours, minutes = map(int, tz_string.split(':')) 109 except Exception: 110 log.warning('Wrong tz string: %s', tz_string) 111 return None 112 113 if hours not in range(-24, 24): 114 log.warning('Wrong tz string: %s', tz_string) 115 return None 116 117 if minutes not in range(0, 59): 118 log.warning('Wrong tz string: %s', tz_string) 119 return None 120 121 if hours in (24, -24) and minutes != 0: 122 log.warning('Wrong tz string: %s', tz_string) 123 return None 124 return timezone(timedelta(hours=hours, minutes=minutes)) 125 126 127def parse_datetime(timestring, check_utc=False, 128 convert='utc', epoch=False): 129 ''' 130 Parse a XEP-0082 DateTime Profile String 131 132 :param timestring: a XEP-0082 DateTime profile formated string 133 134 :param check_utc: if True, returns None if timestring is not 135 a timestring expressing UTC 136 137 :param convert: convert the given timestring to utc or local time 138 139 :param epoch: if True, returns the time in epoch 140 141 Examples: 142 '2017-11-05T01:41:20Z' 143 '2017-11-05T01:41:20.123Z' 144 '2017-11-05T01:41:20.123+05:00' 145 146 return a datetime or epoch 147 ''' 148 if timestring is None: 149 return None 150 if convert not in (None, 'utc', 'local'): 151 raise TypeError('"%s" is not a valid value for convert') 152 if check_utc: 153 match = PATTERN_DELAY.match(timestring) 154 else: 155 match = PATTERN_DATETIME.match(timestring) 156 157 if match: 158 timestring = ''.join(match.groups('')) 159 strformat = '%Y-%m-%d%H:%M:%S%z' 160 if match.group(3): 161 # Fractional second addendum to Time 162 strformat = '%Y-%m-%d%H:%M:%S.%f%z' 163 if match.group(4): 164 # UTC string denoted by addition of the character 'Z' 165 timestring = timestring[:-1] + '+0000' 166 try: 167 date_time = datetime.strptime(timestring, strformat) 168 except ValueError: 169 pass 170 else: 171 if check_utc: 172 if convert != 'utc': 173 raise ValueError( 174 'check_utc can only be used with convert="utc"') 175 date_time.replace(tzinfo=timezone.utc) 176 if epoch: 177 return date_time.timestamp() 178 return date_time 179 180 if convert == 'utc': 181 date_time = date_time.astimezone(timezone.utc) 182 if epoch: 183 return date_time.timestamp() 184 return date_time 185 186 if epoch: 187 # epoch is always UTC, use convert='utc' or check_utc=True 188 raise ValueError( 189 'epoch not available while converting to local') 190 191 if convert == 'local': 192 date_time = date_time.astimezone(LocalTimezone()) 193 return date_time 194 195 # convert=None 196 return date_time 197 return None 198