[106] | 1 | """Utility functions and date/time routines.
|
---|
| 2 |
|
---|
| 3 | Copyright 2002-2006 John J Lee <jjl@pobox.com>
|
---|
| 4 |
|
---|
| 5 | This code is free software; you can redistribute it and/or modify it
|
---|
| 6 | under the terms of the BSD or ZPL 2.1 licenses (see the file
|
---|
| 7 | COPYING.txt included with the distribution).
|
---|
| 8 |
|
---|
| 9 | """
|
---|
| 10 |
|
---|
| 11 | import re, time, warnings
|
---|
| 12 |
|
---|
| 13 |
|
---|
| 14 | class ExperimentalWarning(UserWarning):
|
---|
| 15 | pass
|
---|
| 16 |
|
---|
| 17 | def experimental(message):
|
---|
| 18 | warnings.warn(message, ExperimentalWarning, stacklevel=3)
|
---|
| 19 | def hide_experimental_warnings():
|
---|
| 20 | warnings.filterwarnings("ignore", category=ExperimentalWarning)
|
---|
| 21 | def reset_experimental_warnings():
|
---|
| 22 | warnings.filterwarnings("default", category=ExperimentalWarning)
|
---|
| 23 |
|
---|
| 24 | def deprecation(message):
|
---|
| 25 | warnings.warn(message, DeprecationWarning, stacklevel=3)
|
---|
| 26 | def hide_deprecations():
|
---|
| 27 | warnings.filterwarnings("ignore", category=DeprecationWarning)
|
---|
| 28 | def reset_deprecations():
|
---|
| 29 | warnings.filterwarnings("default", category=DeprecationWarning)
|
---|
| 30 |
|
---|
| 31 |
|
---|
| 32 | def isstringlike(x):
|
---|
| 33 | try: x + ""
|
---|
| 34 | except: return False
|
---|
| 35 | else: return True
|
---|
| 36 |
|
---|
| 37 | ## def caller():
|
---|
| 38 | ## try:
|
---|
| 39 | ## raise SyntaxError
|
---|
| 40 | ## except:
|
---|
| 41 | ## import sys
|
---|
| 42 | ## return sys.exc_traceback.tb_frame.f_back.f_back.f_code.co_name
|
---|
| 43 |
|
---|
| 44 |
|
---|
| 45 | from calendar import timegm
|
---|
| 46 |
|
---|
| 47 | # Date/time conversion routines for formats used by the HTTP protocol.
|
---|
| 48 |
|
---|
| 49 | EPOCH = 1970
|
---|
| 50 | def my_timegm(tt):
|
---|
| 51 | year, month, mday, hour, min, sec = tt[:6]
|
---|
| 52 | if ((year >= EPOCH) and (1 <= month <= 12) and (1 <= mday <= 31) and
|
---|
| 53 | (0 <= hour <= 24) and (0 <= min <= 59) and (0 <= sec <= 61)):
|
---|
| 54 | return timegm(tt)
|
---|
| 55 | else:
|
---|
| 56 | return None
|
---|
| 57 |
|
---|
| 58 | days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
|
---|
| 59 | months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
---|
| 60 | "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
|
---|
| 61 | months_lower = []
|
---|
| 62 | for month in months: months_lower.append(month.lower())
|
---|
| 63 |
|
---|
| 64 |
|
---|
| 65 | def time2isoz(t=None):
|
---|
| 66 | """Return a string representing time in seconds since epoch, t.
|
---|
| 67 |
|
---|
| 68 | If the function is called without an argument, it will use the current
|
---|
| 69 | time.
|
---|
| 70 |
|
---|
| 71 | The format of the returned string is like "YYYY-MM-DD hh:mm:ssZ",
|
---|
| 72 | representing Universal Time (UTC, aka GMT). An example of this format is:
|
---|
| 73 |
|
---|
| 74 | 1994-11-24 08:49:37Z
|
---|
| 75 |
|
---|
| 76 | """
|
---|
| 77 | if t is None: t = time.time()
|
---|
| 78 | year, mon, mday, hour, min, sec = time.gmtime(t)[:6]
|
---|
| 79 | return "%04d-%02d-%02d %02d:%02d:%02dZ" % (
|
---|
| 80 | year, mon, mday, hour, min, sec)
|
---|
| 81 |
|
---|
| 82 | def time2netscape(t=None):
|
---|
| 83 | """Return a string representing time in seconds since epoch, t.
|
---|
| 84 |
|
---|
| 85 | If the function is called without an argument, it will use the current
|
---|
| 86 | time.
|
---|
| 87 |
|
---|
| 88 | The format of the returned string is like this:
|
---|
| 89 |
|
---|
| 90 | Wed, DD-Mon-YYYY HH:MM:SS GMT
|
---|
| 91 |
|
---|
| 92 | """
|
---|
| 93 | if t is None: t = time.time()
|
---|
| 94 | year, mon, mday, hour, min, sec, wday = time.gmtime(t)[:7]
|
---|
| 95 | return "%s %02d-%s-%04d %02d:%02d:%02d GMT" % (
|
---|
| 96 | days[wday], mday, months[mon - 1], year, hour, min, sec)
|
---|
| 97 |
|
---|
| 98 |
|
---|
| 99 | UTC_ZONES = {"GMT": None, "UTC": None, "UT": None, "Z": None}
|
---|
| 100 |
|
---|
| 101 | timezone_re = re.compile(r"^([-+])?(\d\d?):?(\d\d)?$")
|
---|
| 102 | def offset_from_tz_string(tz):
|
---|
| 103 | offset = None
|
---|
| 104 | if UTC_ZONES.has_key(tz):
|
---|
| 105 | offset = 0
|
---|
| 106 | else:
|
---|
| 107 | m = timezone_re.search(tz)
|
---|
| 108 | if m:
|
---|
| 109 | offset = 3600 * int(m.group(2))
|
---|
| 110 | if m.group(3):
|
---|
| 111 | offset = offset + 60 * int(m.group(3))
|
---|
| 112 | if m.group(1) == '-':
|
---|
| 113 | offset = -offset
|
---|
| 114 | return offset
|
---|
| 115 |
|
---|
| 116 | def _str2time(day, mon, yr, hr, min, sec, tz):
|
---|
| 117 | # translate month name to number
|
---|
| 118 | # month numbers start with 1 (January)
|
---|
| 119 | try:
|
---|
| 120 | mon = months_lower.index(mon.lower()) + 1
|
---|
| 121 | except ValueError:
|
---|
| 122 | # maybe it's already a number
|
---|
| 123 | try:
|
---|
| 124 | imon = int(mon)
|
---|
| 125 | except ValueError:
|
---|
| 126 | return None
|
---|
| 127 | if 1 <= imon <= 12:
|
---|
| 128 | mon = imon
|
---|
| 129 | else:
|
---|
| 130 | return None
|
---|
| 131 |
|
---|
| 132 | # make sure clock elements are defined
|
---|
| 133 | if hr is None: hr = 0
|
---|
| 134 | if min is None: min = 0
|
---|
| 135 | if sec is None: sec = 0
|
---|
| 136 |
|
---|
| 137 | yr = int(yr)
|
---|
| 138 | day = int(day)
|
---|
| 139 | hr = int(hr)
|
---|
| 140 | min = int(min)
|
---|
| 141 | sec = int(sec)
|
---|
| 142 |
|
---|
| 143 | if yr < 1000:
|
---|
| 144 | # find "obvious" year
|
---|
| 145 | cur_yr = time.localtime(time.time())[0]
|
---|
| 146 | m = cur_yr % 100
|
---|
| 147 | tmp = yr
|
---|
| 148 | yr = yr + cur_yr - m
|
---|
| 149 | m = m - tmp
|
---|
| 150 | if abs(m) > 50:
|
---|
| 151 | if m > 0: yr = yr + 100
|
---|
| 152 | else: yr = yr - 100
|
---|
| 153 |
|
---|
| 154 | # convert UTC time tuple to seconds since epoch (not timezone-adjusted)
|
---|
| 155 | t = my_timegm((yr, mon, day, hr, min, sec, tz))
|
---|
| 156 |
|
---|
| 157 | if t is not None:
|
---|
| 158 | # adjust time using timezone string, to get absolute time since epoch
|
---|
| 159 | if tz is None:
|
---|
| 160 | tz = "UTC"
|
---|
| 161 | tz = tz.upper()
|
---|
| 162 | offset = offset_from_tz_string(tz)
|
---|
| 163 | if offset is None:
|
---|
| 164 | return None
|
---|
| 165 | t = t - offset
|
---|
| 166 |
|
---|
| 167 | return t
|
---|
| 168 |
|
---|
| 169 |
|
---|
| 170 | strict_re = re.compile(r"^[SMTWF][a-z][a-z], (\d\d) ([JFMASOND][a-z][a-z]) "
|
---|
| 171 | r"(\d\d\d\d) (\d\d):(\d\d):(\d\d) GMT$")
|
---|
| 172 | wkday_re = re.compile(
|
---|
| 173 | r"^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)[a-z]*,?\s*", re.I)
|
---|
| 174 | loose_http_re = re.compile(
|
---|
| 175 | r"""^
|
---|
| 176 | (\d\d?) # day
|
---|
| 177 | (?:\s+|[-\/])
|
---|
| 178 | (\w+) # month
|
---|
| 179 | (?:\s+|[-\/])
|
---|
| 180 | (\d+) # year
|
---|
| 181 | (?:
|
---|
| 182 | (?:\s+|:) # separator before clock
|
---|
| 183 | (\d\d?):(\d\d) # hour:min
|
---|
| 184 | (?::(\d\d))? # optional seconds
|
---|
| 185 | )? # optional clock
|
---|
| 186 | \s*
|
---|
| 187 | ([-+]?\d{2,4}|(?![APap][Mm]\b)[A-Za-z]+)? # timezone
|
---|
| 188 | \s*
|
---|
| 189 | (?:\(\w+\))? # ASCII representation of timezone in parens.
|
---|
| 190 | \s*$""", re.X)
|
---|
| 191 | def http2time(text):
|
---|
| 192 | """Returns time in seconds since epoch of time represented by a string.
|
---|
| 193 |
|
---|
| 194 | Return value is an integer.
|
---|
| 195 |
|
---|
| 196 | None is returned if the format of str is unrecognized, the time is outside
|
---|
| 197 | the representable range, or the timezone string is not recognized. If the
|
---|
| 198 | string contains no timezone, UTC is assumed.
|
---|
| 199 |
|
---|
| 200 | The timezone in the string may be numerical (like "-0800" or "+0100") or a
|
---|
| 201 | string timezone (like "UTC", "GMT", "BST" or "EST"). Currently, only the
|
---|
| 202 | timezone strings equivalent to UTC (zero offset) are known to the function.
|
---|
| 203 |
|
---|
| 204 | The function loosely parses the following formats:
|
---|
| 205 |
|
---|
| 206 | Wed, 09 Feb 1994 22:23:32 GMT -- HTTP format
|
---|
| 207 | Tuesday, 08-Feb-94 14:15:29 GMT -- old rfc850 HTTP format
|
---|
| 208 | Tuesday, 08-Feb-1994 14:15:29 GMT -- broken rfc850 HTTP format
|
---|
| 209 | 09 Feb 1994 22:23:32 GMT -- HTTP format (no weekday)
|
---|
| 210 | 08-Feb-94 14:15:29 GMT -- rfc850 format (no weekday)
|
---|
| 211 | 08-Feb-1994 14:15:29 GMT -- broken rfc850 format (no weekday)
|
---|
| 212 |
|
---|
| 213 | The parser ignores leading and trailing whitespace. The time may be
|
---|
| 214 | absent.
|
---|
| 215 |
|
---|
| 216 | If the year is given with only 2 digits, the function will select the
|
---|
| 217 | century that makes the year closest to the current date.
|
---|
| 218 |
|
---|
| 219 | """
|
---|
| 220 | # fast exit for strictly conforming string
|
---|
| 221 | m = strict_re.search(text)
|
---|
| 222 | if m:
|
---|
| 223 | g = m.groups()
|
---|
| 224 | mon = months_lower.index(g[1].lower()) + 1
|
---|
| 225 | tt = (int(g[2]), mon, int(g[0]),
|
---|
| 226 | int(g[3]), int(g[4]), float(g[5]))
|
---|
| 227 | return my_timegm(tt)
|
---|
| 228 |
|
---|
| 229 | # No, we need some messy parsing...
|
---|
| 230 |
|
---|
| 231 | # clean up
|
---|
| 232 | text = text.lstrip()
|
---|
| 233 | text = wkday_re.sub("", text, 1) # Useless weekday
|
---|
| 234 |
|
---|
| 235 | # tz is time zone specifier string
|
---|
| 236 | day, mon, yr, hr, min, sec, tz = [None] * 7
|
---|
| 237 |
|
---|
| 238 | # loose regexp parse
|
---|
| 239 | m = loose_http_re.search(text)
|
---|
| 240 | if m is not None:
|
---|
| 241 | day, mon, yr, hr, min, sec, tz = m.groups()
|
---|
| 242 | else:
|
---|
| 243 | return None # bad format
|
---|
| 244 |
|
---|
| 245 | return _str2time(day, mon, yr, hr, min, sec, tz)
|
---|
| 246 |
|
---|
| 247 |
|
---|
| 248 | iso_re = re.compile(
|
---|
| 249 | """^
|
---|
| 250 | (\d{4}) # year
|
---|
| 251 | [-\/]?
|
---|
| 252 | (\d\d?) # numerical month
|
---|
| 253 | [-\/]?
|
---|
| 254 | (\d\d?) # day
|
---|
| 255 | (?:
|
---|
| 256 | (?:\s+|[-:Tt]) # separator before clock
|
---|
| 257 | (\d\d?):?(\d\d) # hour:min
|
---|
| 258 | (?::?(\d\d(?:\.\d*)?))? # optional seconds (and fractional)
|
---|
| 259 | )? # optional clock
|
---|
| 260 | \s*
|
---|
| 261 | ([-+]?\d\d?:?(:?\d\d)?
|
---|
| 262 | |Z|z)? # timezone (Z is "zero meridian", i.e. GMT)
|
---|
| 263 | \s*$""", re.X)
|
---|
| 264 | def iso2time(text):
|
---|
| 265 | """
|
---|
| 266 | As for http2time, but parses the ISO 8601 formats:
|
---|
| 267 |
|
---|
| 268 | 1994-02-03 14:15:29 -0100 -- ISO 8601 format
|
---|
| 269 | 1994-02-03 14:15:29 -- zone is optional
|
---|
| 270 | 1994-02-03 -- only date
|
---|
| 271 | 1994-02-03T14:15:29 -- Use T as separator
|
---|
| 272 | 19940203T141529Z -- ISO 8601 compact format
|
---|
| 273 | 19940203 -- only date
|
---|
| 274 |
|
---|
| 275 | """
|
---|
| 276 | # clean up
|
---|
| 277 | text = text.lstrip()
|
---|
| 278 |
|
---|
| 279 | # tz is time zone specifier string
|
---|
| 280 | day, mon, yr, hr, min, sec, tz = [None] * 7
|
---|
| 281 |
|
---|
| 282 | # loose regexp parse
|
---|
| 283 | m = iso_re.search(text)
|
---|
| 284 | if m is not None:
|
---|
| 285 | # XXX there's an extra bit of the timezone I'm ignoring here: is
|
---|
| 286 | # this the right thing to do?
|
---|
| 287 | yr, mon, day, hr, min, sec, tz, _ = m.groups()
|
---|
| 288 | else:
|
---|
| 289 | return None # bad format
|
---|
| 290 |
|
---|
| 291 | return _str2time(day, mon, yr, hr, min, sec, tz)
|
---|