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 3191 2010-04-12 23:07:16Z 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 return cls.strptime(x, "%y%m%d%H%M%SZ")
00071
00072 def toUTCTime(self):
00073 """Convert to ASN.1 UTCTime."""
00074 return self.strftime("%y%m%d%H%M%SZ")
00075
00076 @classmethod
00077 def fromGeneralizedTime(cls, x):
00078 """Convert from ASN.1 GeneralizedTime."""
00079 return cls.strptime(x, "%Y%m%d%H%M%SZ")
00080
00081 def toGeneralizedTime(self):
00082 """Convert to ASN.1 GeneralizedTime."""
00083 return self.strftime("%Y%m%d%H%M%SZ")
00084
00085 @classmethod
00086 def fromASN1tuple(cls, x):
00087 """
00088 Convert from ASN.1 tuple representation.
00089 """
00090 assert isinstance(x, tuple) and len(x) == 2 and x[0] in ("utcTime", "generalTime")
00091 if x[0] == "utcTime":
00092 return cls.fromUTCTime(x[1])
00093 else:
00094 return cls.fromGeneralizedTime(x[1])
00095
00096
00097
00098
00099 PKIX_threshhold = pydatetime.datetime(2050, 1, 1)
00100
00101 def toASN1tuple(self):
00102 """
00103 Convert to ASN.1 tuple representation.
00104 """
00105 if self < self.PKIX_threshhold:
00106 return "utcTime", self.toUTCTime()
00107 else:
00108 return "generalTime", self.toGeneralizedTime()
00109
00110 @classmethod
00111 def fromXMLtime(cls, x):
00112 """
00113 Convert from XML time representation.
00114 """
00115 if x is None:
00116 return None
00117 else:
00118 return cls.strptime(x, "%Y-%m-%dT%H:%M:%SZ")
00119
00120 def toXMLtime(self):
00121 """Convert to XML time representation."""
00122 return self.strftime("%Y-%m-%dT%H:%M:%SZ")
00123
00124 def __str__(self):
00125 return self.toXMLtime()
00126
00127 @classmethod
00128 def fromdatetime(cls, x):
00129 """
00130 Convert a datetime.datetime object into this subclass. This is
00131 whacky due to the weird constructors for datetime.
00132 """
00133 return cls.combine(x.date(), x.time())
00134
00135 @classmethod
00136 def from_sql(cls, x):
00137 """Convert from SQL storage format."""
00138 return cls.fromdatetime(x)
00139
00140 def to_sql(self):
00141 """
00142 Convert to SQL storage format.
00143
00144 There's something whacky going on in the MySQLdb module, it throws
00145 range errors when storing a derived type into a DATETIME column.
00146 Investigate some day, but for now brute force this by copying the
00147 relevant fields into a datetime.datetime for MySQLdb's
00148 consumption.
00149
00150 """
00151 return pydatetime.datetime(year = self.year, month = self.month, day = self.day,
00152 hour = self.hour, minute = self.minute, second = self.second,
00153 microsecond = 0, tzinfo = None)
00154
00155 def later(self, other):
00156 """Return the later of two timestamps."""
00157 return other if other > self else self
00158
00159 def earlier(self, other):
00160 """Return the earlier of two timestamps."""
00161 return other if other < self else self
00162
00163 def __add__(self, y): return _cast(pydatetime.datetime.__add__(self, y))
00164 def __radd__(self, y): return _cast(pydatetime.datetime.__radd__(self, y))
00165 def __rsub__(self, y): return _cast(pydatetime.datetime.__rsub__(self, y))
00166 def __sub__(self, y): return _cast(pydatetime.datetime.__sub__(self, y))
00167
00168 class timedelta(pydatetime.timedelta):
00169 """
00170 Timedelta with text parsing. This accepts two input formats:
00171
00172 - A simple integer, indicating a number of seconds.
00173
00174 - A string of the form "uY vW wD xH yM zS" where u, v, w, x, y, and z
00175 are integers and Y, W, D, H, M, and S indicate years, weeks, days,
00176 hours, minutes, and seconds. All of the fields are optional, but
00177 at least one must be specified. Eg,"3D4H" means "three days plus
00178 four hours".
00179
00180 There is no "months" format, because the definition of a month is too
00181 fuzzy to be useful (what day is six months from August 30th?)
00182
00183 Similarly, the "years" conversion may produce surprising results, as
00184 "one year" in conventional English does not refer to a fixed interval
00185 but rather a fixed (and in some cases undefined) offset within the
00186 Gregorian calendar (what day is one year from February 29th?) 1Y as
00187 implemented by this code refers to a specific number of seconds.
00188 If you mean 365 days or 52 weeks, say that instead.
00189 """
00190
00191
00192
00193
00194
00195
00196 regexp = re.compile("\\s*".join(("^",
00197 "(?:(?P<years>\\d+)Y)?",
00198 "(?:(?P<weeks>\\d+)W)?",
00199 "(?:(?P<days>\\d+)D)?",
00200 "(?:(?P<hours>\\d+)H)?",
00201 "(?:(?P<minutes>\\d+)M)?",
00202 "(?:(?P<seconds>\\d+)S)?",
00203 "$")),
00204 re.I)
00205
00206
00207
00208
00209
00210 years_to_seconds = 31556926
00211
00212 @classmethod
00213 def parse(cls, arg):
00214 """
00215 Parse text into a timedelta object.
00216 """
00217 if not isinstance(arg, str):
00218 return cls(seconds = arg)
00219 elif arg.isdigit():
00220 return cls(seconds = int(arg))
00221 else:
00222 match = cls.regexp.match(arg)
00223 if match:
00224
00225 d = match.groupdict("0")
00226 for k, v in d.iteritems():
00227 d[k] = int(v)
00228 d["days"] += d.pop("weeks") * 7
00229 d["seconds"] += d.pop("years") * cls.years_to_seconds
00230 return cls(**d)
00231 else:
00232 raise RuntimeError, "Couldn't parse timedelta %r" % (arg,)
00233
00234 def convert_to_seconds(self):
00235 """Convert a timedelta interval to seconds."""
00236 return self.days * 24 * 60 * 60 + self.seconds
00237
00238 @classmethod
00239 def fromtimedelta(cls, x):
00240 """Convert a datetime.timedelta object into this subclass."""
00241 return cls(days = x.days, seconds = x.seconds, microseconds = x.microseconds)
00242
00243 def __abs__(self): return _cast(pydatetime.timedelta.__abs__(self))
00244 def __add__(self, x): return _cast(pydatetime.timedelta.__add__(self, x))
00245 def __div__(self, x): return _cast(pydatetime.timedelta.__div__(self, x))
00246 def __floordiv__(self, x): return _cast(pydatetime.timedelta.__floordiv__(self, x))
00247 def __mul__(self, x): return _cast(pydatetime.timedelta.__mul__(self, x))
00248 def __neg__(self): return _cast(pydatetime.timedelta.__neg__(self))
00249 def __pos__(self): return _cast(pydatetime.timedelta.__pos__(self))
00250 def __radd__(self, x): return _cast(pydatetime.timedelta.__radd__(self, x))
00251 def __rdiv__(self, x): return _cast(pydatetime.timedelta.__rdiv__(self, x))
00252 def __rfloordiv__(self, x): return _cast(pydatetime.timedelta.__rfloordiv__(self, x))
00253 def __rmul__(self, x): return _cast(pydatetime.timedelta.__rmul__(self, x))
00254 def __rsub__(self, x): return _cast(pydatetime.timedelta.__rsub__(self, x))
00255 def __sub__(self, x): return _cast(pydatetime.timedelta.__sub__(self, x))
00256
00257 def _cast(x):
00258 """
00259 Cast result of arithmetic operations back into correct subtype.
00260 """
00261 if isinstance(x, pydatetime.datetime):
00262 return datetime.fromdatetime(x)
00263 if isinstance(x, pydatetime.timedelta):
00264 return timedelta.fromtimedelta(x)
00265 return x
00266
00267 if __name__ == "__main__":
00268
00269 def test(t):
00270 print
00271 print "str: ", t
00272 print "repr: ", repr(t)
00273 print "seconds since epoch:", t.strftime("%s")
00274 print "UTCTime: ", t.toUTCTime()
00275 print "GeneralizedTime: ", t.toGeneralizedTime()
00276 print "ASN1tuple: ", t.toASN1tuple()
00277 print "XMLtime: ", t.toXMLtime()
00278 print
00279
00280 print
00281 print "Testing time conversion routines"
00282 test(now())
00283 test(now() + timedelta(days = 30))
00284 test(now() + timedelta.parse("3d5s"))
00285 test(now() + timedelta.parse(" 3d 5s "))
00286 test(now() + timedelta.parse("1y3d5h"))