00001 """
00002 Unified RPKI date/time handling, based on the standard Python datetime module.
00003
00004 Module name chosen to sidestep a nightmare of import-related errors
00005 that occur with the more obvious module names.
00006
00007 List of arithmetic methods that require result casting was derived by
00008 inspection of the datetime module, to wit:
00009
00010 >>> import datetime
00011 >>> for t in (datetime.datetime, datetime.timedelta):
00012 ... for k in t.__dict__.keys():
00013 ... if k.startswith("__"):
00014 ... print "%s.%s()" % (t.__name__, k)
00015
00016 $Id: sundial.py 3449 2010-09-16 21:30:30Z sra $
00017
00018 Copyright (C) 2009--2010 Internet Systems Consortium ("ISC")
00019
00020 Permission to use, copy, modify, and distribute this software for any
00021 purpose with or without fee is hereby granted, provided that the above
00022 copyright notice and this permission notice appear in all copies.
00023
00024 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
00025 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00026 AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
00027 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00028 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00029 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00030 PERFORMANCE OF THIS SOFTWARE.
00031
00032 Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
00033
00034 Permission to use, copy, modify, and distribute this software for any
00035 purpose with or without fee is hereby granted, provided that the above
00036 copyright notice and this permission notice appear in all copies.
00037
00038 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
00039 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00040 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
00041 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00042 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00043 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00044 PERFORMANCE OF THIS SOFTWARE.
00045 """
00046
00047 import datetime as pydatetime
00048 import re
00049
00050 def now():
00051 """Get current timestamp."""
00052 return datetime.utcnow()
00053
00054 class datetime(pydatetime.datetime):
00055 """
00056 RPKI extensions to standard datetime.datetime class. All work here
00057 is in UTC, so we use naive datetime objects.
00058 """
00059
00060 def totimestamp(self):
00061 """
00062 Convert to seconds from epoch (like time.time()). Conversion
00063 method is a bit silly, but avoids time module timezone whackiness.
00064 """
00065 return int(self.strftime("%s"))
00066
00067 @classmethod
00068 def fromUTCTime(cls, x):
00069 """Convert from ASN.1 UTCTime."""
00070 x = str(x)
00071 return cls.fromGeneralizedTime(("19" if x[0] >= "5" else "20") + x)
00072
00073 def toUTCTime(self):
00074 """Convert to ASN.1 UTCTime."""
00075 return self.strftime("%y%m%d%H%M%SZ")
00076
00077 @classmethod
00078 def fromGeneralizedTime(cls, x):
00079 """Convert from ASN.1 GeneralizedTime."""
00080 return cls.strptime(x, "%Y%m%d%H%M%SZ")
00081
00082 def toGeneralizedTime(self):
00083 """Convert to ASN.1 GeneralizedTime."""
00084 return self.strftime("%Y%m%d%H%M%SZ")
00085
00086 @classmethod
00087 def fromASN1tuple(cls, x):
00088 """
00089 Convert from ASN.1 tuple representation.
00090 """
00091 assert isinstance(x, tuple) and len(x) == 2 and x[0] in ("utcTime", "generalTime")
00092 if x[0] == "utcTime":
00093 return cls.fromUTCTime(x[1])
00094 else:
00095 return cls.fromGeneralizedTime(x[1])
00096
00097
00098
00099
00100 PKIX_threshhold = pydatetime.datetime(2050, 1, 1)
00101
00102 def toASN1tuple(self):
00103 """
00104 Convert to ASN.1 tuple representation.
00105 """
00106 if self < self.PKIX_threshhold:
00107 return "utcTime", self.toUTCTime()
00108 else:
00109 return "generalTime", self.toGeneralizedTime()
00110
00111 @classmethod
00112 def fromXMLtime(cls, x):
00113 """
00114 Convert from XML time representation.
00115 """
00116 if x is None:
00117 return None
00118 else:
00119 return cls.strptime(x, "%Y-%m-%dT%H:%M:%SZ")
00120
00121 def toXMLtime(self):
00122 """Convert to XML time representation."""
00123 return self.strftime("%Y-%m-%dT%H:%M:%SZ")
00124
00125 def __str__(self):
00126 return self.toXMLtime()
00127
00128 @classmethod
00129 def fromdatetime(cls, x):
00130 """
00131 Convert a datetime.datetime object into this subclass. This is
00132 whacky due to the weird constructors for datetime.
00133 """
00134 return cls.combine(x.date(), x.time())
00135
00136 @classmethod
00137 def from_sql(cls, x):
00138 """Convert from SQL storage format."""
00139 return cls.fromdatetime(x)
00140
00141 def to_sql(self):
00142 """
00143 Convert to SQL storage format.
00144
00145 There's something whacky going on in the MySQLdb module, it throws
00146 range errors when storing a derived type into a DATETIME column.
00147 Investigate some day, but for now brute force this by copying the
00148 relevant fields into a datetime.datetime for MySQLdb's
00149 consumption.
00150
00151 """
00152 return pydatetime.datetime(year = self.year, month = self.month, day = self.day,
00153 hour = self.hour, minute = self.minute, second = self.second,
00154 microsecond = 0, tzinfo = None)
00155
00156 def later(self, other):
00157 """Return the later of two timestamps."""
00158 return other if other > self else self
00159
00160 def earlier(self, other):
00161 """Return the earlier of two timestamps."""
00162 return other if other < self else self
00163
00164 def __add__(self, y): return _cast(pydatetime.datetime.__add__(self, y))
00165 def __radd__(self, y): return _cast(pydatetime.datetime.__radd__(self, y))
00166 def __rsub__(self, y): return _cast(pydatetime.datetime.__rsub__(self, y))
00167 def __sub__(self, y): return _cast(pydatetime.datetime.__sub__(self, y))
00168
00169 class timedelta(pydatetime.timedelta):
00170 """
00171 Timedelta with text parsing. This accepts two input formats:
00172
00173 - A simple integer, indicating a number of seconds.
00174
00175 - A string of the form "uY vW wD xH yM zS" where u, v, w, x, y, and z
00176 are integers and Y, W, D, H, M, and S indicate years, weeks, days,
00177 hours, minutes, and seconds. All of the fields are optional, but
00178 at least one must be specified. Eg,"3D4H" means "three days plus
00179 four hours".
00180
00181 There is no "months" format, because the definition of a month is too
00182 fuzzy to be useful (what day is six months from August 30th?)
00183
00184 Similarly, the "years" conversion may produce surprising results, as
00185 "one year" in conventional English does not refer to a fixed interval
00186 but rather a fixed (and in some cases undefined) offset within the
00187 Gregorian calendar (what day is one year from February 29th?) 1Y as
00188 implemented by this code refers to a specific number of seconds.
00189 If you mean 365 days or 52 weeks, say that instead.
00190 """
00191
00192
00193
00194
00195
00196
00197 regexp = re.compile("\\s*".join(("^",
00198 "(?:(?P<years>\\d+)Y)?",
00199 "(?:(?P<weeks>\\d+)W)?",
00200 "(?:(?P<days>\\d+)D)?",
00201 "(?:(?P<hours>\\d+)H)?",
00202 "(?:(?P<minutes>\\d+)M)?",
00203 "(?:(?P<seconds>\\d+)S)?",
00204 "$")),
00205 re.I)
00206
00207
00208
00209
00210
00211 years_to_seconds = 31556926
00212
00213 @classmethod
00214 def parse(cls, arg):
00215 """
00216 Parse text into a timedelta object.
00217 """
00218 if not isinstance(arg, str):
00219 return cls(seconds = arg)
00220 elif arg.isdigit():
00221 return cls(seconds = int(arg))
00222 else:
00223 match = cls.regexp.match(arg)
00224 if match:
00225
00226 d = match.groupdict("0")
00227 for k, v in d.iteritems():
00228 d[k] = int(v)
00229 d["days"] += d.pop("weeks") * 7
00230 d["seconds"] += d.pop("years") * cls.years_to_seconds
00231 return cls(**d)
00232 else:
00233 raise RuntimeError, "Couldn't parse timedelta %r" % (arg,)
00234
00235 def convert_to_seconds(self):
00236 """Convert a timedelta interval to seconds."""
00237 return self.days * 24 * 60 * 60 + self.seconds
00238
00239 @classmethod
00240 def fromtimedelta(cls, x):
00241 """Convert a datetime.timedelta object into this subclass."""
00242 return cls(days = x.days, seconds = x.seconds, microseconds = x.microseconds)
00243
00244 def __abs__(self): return _cast(pydatetime.timedelta.__abs__(self))
00245 def __add__(self, x): return _cast(pydatetime.timedelta.__add__(self, x))
00246 def __div__(self, x): return _cast(pydatetime.timedelta.__div__(self, x))
00247 def __floordiv__(self, x): return _cast(pydatetime.timedelta.__floordiv__(self, x))
00248 def __mul__(self, x): return _cast(pydatetime.timedelta.__mul__(self, x))
00249 def __neg__(self): return _cast(pydatetime.timedelta.__neg__(self))
00250 def __pos__(self): return _cast(pydatetime.timedelta.__pos__(self))
00251 def __radd__(self, x): return _cast(pydatetime.timedelta.__radd__(self, x))
00252 def __rdiv__(self, x): return _cast(pydatetime.timedelta.__rdiv__(self, x))
00253 def __rfloordiv__(self, x): return _cast(pydatetime.timedelta.__rfloordiv__(self, x))
00254 def __rmul__(self, x): return _cast(pydatetime.timedelta.__rmul__(self, x))
00255 def __rsub__(self, x): return _cast(pydatetime.timedelta.__rsub__(self, x))
00256 def __sub__(self, x): return _cast(pydatetime.timedelta.__sub__(self, x))
00257
00258 def _cast(x):
00259 """
00260 Cast result of arithmetic operations back into correct subtype.
00261 """
00262 if isinstance(x, pydatetime.datetime):
00263 return datetime.fromdatetime(x)
00264 if isinstance(x, pydatetime.timedelta):
00265 return timedelta.fromtimedelta(x)
00266 return x
00267
00268 if __name__ == "__main__":
00269
00270 def test(t):
00271 print
00272 print "str: ", t
00273 print "repr: ", repr(t)
00274 print "seconds since epoch:", t.strftime("%s")
00275 print "UTCTime: ", t.toUTCTime()
00276 print "GeneralizedTime: ", t.toGeneralizedTime()
00277 print "ASN1tuple: ", t.toASN1tuple()
00278 print "XMLtime: ", t.toXMLtime()
00279 print
00280
00281 print
00282 print "Testing time conversion routines"
00283 test(now())
00284 test(now() + timedelta(days = 30))
00285 test(now() + timedelta.parse("3d5s"))
00286 test(now() + timedelta.parse(" 3d 5s "))
00287 test(now() + timedelta.parse("1y3d5h"))