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 2785 2009-09-25 20:21:53Z sra $
00017
00018 Copyright (C) 2009 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__(x, y): return _cast(pydatetime.datetime.__add__(x, y))
00164 def __radd__(x, y): return _cast(pydatetime.datetime.__radd__(x, y))
00165 def __rsub__(x, y): return _cast(pydatetime.datetime.__rsub__(x, y))
00166 def __sub__(x, y): return _cast(pydatetime.datetime.__sub__(x, 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 "wD xH yM zS" where w, x, y, and z are integers
00175 and D, H, M, and S indicate days, hours, minutes, and seconds.
00176 All of the fields are optional, but at least one must be specified.
00177 Eg, "3D4H" means "three days plus four hours".
00178 """
00179
00180
00181
00182
00183
00184
00185 regexp = re.compile("\\s*".join(("^",
00186 "(?:(?P<days>\\d+)D)?",
00187 "(?:(?P<hours>\\d+)H)?",
00188 "(?:(?P<minutes>\\d+)M)?",
00189 "(?:(?P<seconds>\\d+)S)?",
00190 "$")),
00191 re.I)
00192
00193 @classmethod
00194 def parse(cls, arg):
00195 """
00196 Parse text into a timedelta object.
00197 """
00198 if not isinstance(arg, str):
00199 return cls(seconds = arg)
00200 elif arg.isdigit():
00201 return cls(seconds = int(arg))
00202 else:
00203 match = cls.regexp.match(arg)
00204 if match:
00205 return cls(**dict((k, int(v)) for (k, v) in match.groupdict().items() if v is not None))
00206 else:
00207 raise RuntimeError, "Couldn't parse timedelta %r" % (arg,)
00208
00209 def convert_to_seconds(self):
00210 """Convert a timedelta interval to seconds."""
00211 return self.days * 24 * 60 * 60 + self.seconds
00212
00213 @classmethod
00214 def fromtimedelta(cls, x):
00215 """Convert a datetime.timedelta object into this subclass."""
00216 return cls(days = x.days, seconds = x.seconds, microseconds = x.microseconds)
00217
00218 def __abs__(x): return _cast(pydatetime.timedelta.__abs__(x))
00219 def __add__(x, y): return _cast(pydatetime.timedelta.__add__(x, y))
00220 def __div__(x, y): return _cast(pydatetime.timedelta.__div__(x, y))
00221 def __floordiv__(x, y): return _cast(pydatetime.timedelta.__floordiv__(x, y))
00222 def __mul__(x, y): return _cast(pydatetime.timedelta.__mul__(x, y))
00223 def __neg__(x): return _cast(pydatetime.timedelta.__neg__(x))
00224 def __pos__(x): return _cast(pydatetime.timedelta.__pos__(x))
00225 def __radd__(x, y): return _cast(pydatetime.timedelta.__radd__(x, y))
00226 def __rdiv__(x, y): return _cast(pydatetime.timedelta.__rdiv__(x, y))
00227 def __rfloordiv__(x, y): return _cast(pydatetime.timedelta.__rfloordiv__(x, y))
00228 def __rmul__(x, y): return _cast(pydatetime.timedelta.__rmul__(x, y))
00229 def __rsub__(x, y): return _cast(pydatetime.timedelta.__rsub__(x, y))
00230 def __sub__(x, y): return _cast(pydatetime.timedelta.__sub__(x, y))
00231
00232 def _cast(x):
00233 """
00234 Cast result of arithmetic operations back into correct subtype.
00235 """
00236 if isinstance(x, pydatetime.datetime):
00237 return datetime.fromdatetime(x)
00238 if isinstance(x, pydatetime.timedelta):
00239 return timedelta.fromtimedelta(x)
00240 return x
00241
00242 if __name__ == "__main__":
00243
00244 def test(t):
00245 print
00246 print "str: ", t
00247 print "repr: ", repr(t)
00248 print "seconds since epoch:", t.strftime("%s")
00249 print "UTCTime: ", t.toUTCTime()
00250 print "GeneralizedTime: ", t.toGeneralizedTime()
00251 print "ASN1tuple: ", t.toASN1tuple()
00252 print "XMLtime: ", t.toXMLtime()
00253 print
00254
00255 print
00256 print "Testing time conversion routines"
00257 test(now())
00258 test(now() + timedelta(days = 30))
00259 test(now() + timedelta.parse("3d5s"))
00260 timedelta.parse(" 3d 5s ")