1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
|
# Copyright 2012 (C) Daniel Richman, Adam Greig
#
# This file is part of strict_rfc3339.
#
# strict_rfc3339 is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# strict_rfc3339 is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with strict_rfc3339. If not, see <http://www.gnu.org/licenses/>.
"""
Super simple lightweight RFC3339 functions
"""
import re
import time
import calendar
__all__ = ["validate_rfc3339",
"InvalidRFC3339Error",
"rfc3339_to_timestamp",
"timestamp_to_rfc3339_utcoffset",
"timestamp_to_rfc3339_localoffset",
"now_to_rfc3339_utcoffset",
"now_to_rfc3339_localoffset"]
rfc3339_regex = re.compile(
r"^(\d\d\d\d)\-(\d\d)\-(\d\d)T"
r"(\d\d):(\d\d):(\d\d)(\.\d+)?(Z|([+\-])(\d\d):(\d\d))$")
def validate_rfc3339(datestring):
"""Check an RFC3339 string is valid via a regex and some range checks"""
m = rfc3339_regex.match(datestring)
if m is None:
return False
groups = m.groups()
year, month, day, hour, minute, second = [int(i) for i in groups[:6]]
if not 1 <= year <= 9999:
# Have to reject this, unfortunately (despite it being OK by rfc3339):
# calendar.timegm/calendar.monthrange can't cope (since datetime can't)
return False
if not 1 <= month <= 12:
return False
(_, max_day) = calendar.monthrange(year, month)
if not 1 <= day <= max_day:
return False
if not (0 <= hour <= 23 and 0 <= minute <= 59 and 0 <= second <= 59):
# forbid leap seconds :-(. See README
return False
if groups[7] != "Z":
(offset_sign, offset_hours, offset_mins) = groups[8:]
if not (0 <= int(offset_hours) <= 23 and 0 <= int(offset_mins) <= 59):
return False
# all OK
return True
class InvalidRFC3339Error(ValueError):
"""Subclass of ValueError thrown by rfc3339_to_timestamp"""
pass
def rfc3339_to_timestamp(datestring):
"""Convert an RFC3339 date-time string to a UTC UNIX timestamp"""
if not validate_rfc3339(datestring):
raise InvalidRFC3339Error
groups = rfc3339_regex.match(datestring).groups()
time_tuple = [int(p) for p in groups[:6]]
timestamp = calendar.timegm(time_tuple)
seconds_part = groups[6]
if seconds_part is not None:
timestamp += float("0" + seconds_part)
if groups[7] != "Z":
(offset_sign, offset_hours, offset_mins) = groups[8:]
offset_seconds = int(offset_hours) * 3600 + int(offset_mins) * 60
if offset_sign == '-':
offset_seconds = -offset_seconds
timestamp -= offset_seconds
return timestamp
def _seconds_and_microseconds(timestamp):
"""
Split a floating point timestamp into an integer number of seconds since
the epoch, and an integer number of microseconds (having rounded to the
nearest microsecond).
If `_seconds_and_microseconds(x) = (y, z)` then the following holds (up to
the error introduced by floating point operations):
* `x = y + z / 1_000_000.`
* `0 <= z < 1_000_000.`
"""
if isinstance(timestamp, int):
return (timestamp, 0)
else:
timestamp_us = int(round(timestamp * 1e6))
return divmod(timestamp_us, 1000000)
def _make_datestring_start(time_tuple, microseconds):
ds_format = "{0:04d}-{1:02d}-{2:02d}T{3:02d}:{4:02d}:{5:02d}"
datestring = ds_format.format(*time_tuple)
seconds_part_str = "{0:06d}".format(microseconds)
# There used to be a bug here where it could be 1000000
assert len(seconds_part_str) == 6 and seconds_part_str[0] != '-'
seconds_part_str = seconds_part_str.rstrip("0")
if seconds_part_str != "":
datestring += "." + seconds_part_str
return datestring
def timestamp_to_rfc3339_utcoffset(timestamp):
"""Convert a UTC UNIX timestamp to RFC3339, with the offset as 'Z'"""
seconds, microseconds = _seconds_and_microseconds(timestamp)
time_tuple = time.gmtime(seconds)
datestring = _make_datestring_start(time_tuple, microseconds)
datestring += "Z"
assert abs(rfc3339_to_timestamp(datestring) - timestamp) < 0.000001
return datestring
def timestamp_to_rfc3339_localoffset(timestamp):
"""
Convert a UTC UNIX timestamp to RFC3339, using the local offset.
localtime() provides the time parts. The difference between gmtime and
localtime tells us the offset.
"""
seconds, microseconds = _seconds_and_microseconds(timestamp)
time_tuple = time.localtime(seconds)
datestring = _make_datestring_start(time_tuple, microseconds)
gm_time_tuple = time.gmtime(seconds)
offset = calendar.timegm(time_tuple) - calendar.timegm(gm_time_tuple)
if abs(offset) % 60 != 0:
raise ValueError("Your local offset is not a whole minute")
offset_minutes = abs(offset) // 60
offset_hours = offset_minutes // 60
offset_minutes %= 60
offset_string = "{0:02d}:{1:02d}".format(offset_hours, offset_minutes)
if offset < 0:
datestring += "-"
else:
datestring += "+"
datestring += offset_string
assert abs(rfc3339_to_timestamp(datestring) - timestamp) < 0.000001
return datestring
def now_to_rfc3339_utcoffset(integer=True):
"""Convert the current time to RFC3339, with the offset as 'Z'"""
timestamp = time.time()
if integer:
timestamp = int(timestamp)
return timestamp_to_rfc3339_utcoffset(timestamp)
def now_to_rfc3339_localoffset(integer=True):
"""Convert the current time to RFC3339, using the local offset."""
timestamp = time.time()
if integer:
timestamp = int(timestamp)
return timestamp_to_rfc3339_localoffset(timestamp)
|