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 $Id: sundial.py 2452 2009-05-27 02:54:24Z sra $
00008
00009 Copyright (C) 2009 Internet Systems Consortium ("ISC")
00010
00011 Permission to use, copy, modify, and distribute this software for any
00012 purpose with or without fee is hereby granted, provided that the above
00013 copyright notice and this permission notice appear in all copies.
00014
00015 THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
00016 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00017 AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
00018 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00019 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00020 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00021 PERFORMANCE OF THIS SOFTWARE.
00022
00023 Portions copyright (C) 2007--2008 American Registry for Internet Numbers ("ARIN")
00024
00025 Permission to use, copy, modify, and distribute this software for any
00026 purpose with or without fee is hereby granted, provided that the above
00027 copyright notice and this permission notice appear in all copies.
00028
00029 THE SOFTWARE IS PROVIDED "AS IS" AND ARIN DISCLAIMS ALL WARRANTIES WITH
00030 REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
00031 AND FITNESS. IN NO EVENT SHALL ARIN BE LIABLE FOR ANY SPECIAL, DIRECT,
00032 INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
00033 LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
00034 OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
00035 PERFORMANCE OF THIS SOFTWARE.
00036 """
00037
00038 import datetime as pydatetime
00039 import re
00040
00041 def now():
00042 """Get current timestamp."""
00043 return datetime.utcnow()
00044
00045 class datetime(pydatetime.datetime):
00046 """
00047 RPKI extensions to standard datetime.datetime class. All work here
00048 is in UTC, so we use naive datetime objects.
00049 """
00050
00051 def totimestamp(self):
00052 """
00053 Convert to seconds from epoch (like time.time()). Conversion
00054 method is a bit silly, but avoids time module timezone whackiness.
00055 """
00056 return int(self.strftime("%s"))
00057
00058 @classmethod
00059 def fromUTCTime(cls, x):
00060 """Convert from ASN.1 UTCTime."""
00061 return cls.strptime(x, "%y%m%d%H%M%SZ")
00062
00063 def toUTCTime(self):
00064 """Convert to ASN.1 UTCTime."""
00065 return self.strftime("%y%m%d%H%M%SZ")
00066
00067 @classmethod
00068 def fromGeneralizedTime(cls, x):
00069 """Convert from ASN.1 GeneralizedTime."""
00070 return cls.strptime(x, "%Y%m%d%H%M%SZ")
00071
00072 def toGeneralizedTime(self):
00073 """Convert to ASN.1 GeneralizedTime."""
00074 return self.strftime("%Y%m%d%H%M%SZ")
00075
00076 @classmethod
00077 def fromASN1tuple(cls, x):
00078 """
00079 Convert from ASN.1 tuple representation.
00080 """
00081 assert isinstance(x, tuple) and len(x) == 2 and x[0] in ("utcTime", "generalTime")
00082 if x[0] == "utcTime":
00083 return cls.fromUTCTime(x[1])
00084 else:
00085 return cls.fromGeneralizedTime(x[1])
00086
00087
00088
00089
00090 PKIX_threshhold = pydatetime.datetime(2050, 1, 1)
00091
00092 def toASN1tuple(self):
00093 """
00094 Convert to ASN.1 tuple representation.
00095 """
00096 if self < self.PKIX_threshhold:
00097 return "utcTime", self.toUTCTime()
00098 else:
00099 return "generalTime", self.toGeneralizedTime()
00100
00101 @classmethod
00102 def fromXMLtime(cls, x):
00103 """
00104 Convert from XML time representation.
00105 """
00106 if x is None:
00107 return None
00108 else:
00109 return cls.strptime(x, "%Y-%m-%dT%H:%M:%SZ")
00110
00111 def toXMLtime(self):
00112 """Convert to XML time representation."""
00113 return self.strftime("%Y-%m-%dT%H:%M:%SZ")
00114
00115 def __str__(self):
00116 return self.toXMLtime()
00117
00118 @classmethod
00119 def fromdatetime(cls, x):
00120 """
00121 Convert a datetime.datetime object into this subclass. This is
00122 whacky due to the weird constructors for datetime.
00123 """
00124 return cls.combine(x.date(), x.time())
00125
00126 def __add__(self, other):
00127 """
00128 Force correct class for timedelta results.
00129 """
00130 x = pydatetime.datetime.__add__(self, other)
00131 if isinstance(x, pydatetime.timedelta):
00132 return timedelta.fromtimedelta(x)
00133 else:
00134 return datetime.fromdatetime(x)
00135
00136 def __sub__(self, other):
00137 """
00138 Force correct class for timedelta results.
00139 """
00140 x = pydatetime.datetime.__sub__(self, other)
00141 if isinstance(x, pydatetime.timedelta):
00142 return timedelta.fromtimedelta(x)
00143 else:
00144 return datetime.fromdatetime(x)
00145
00146 @classmethod
00147 def from_sql(cls, x):
00148 """Convert from SQL storage format."""
00149 return cls.fromdatetime(x)
00150
00151 def to_sql(self):
00152 """
00153 Convert to SQL storage format.
00154
00155 There's something whacky going on in the MySQLdb module, it throws
00156 range errors when storing a derived type into a DATETIME column.
00157 Investigate some day, but for now brute force this by copying the
00158 relevant fields into a datetime.datetime for MySQLdb's
00159 consumption.
00160
00161 """
00162 return pydatetime.datetime(year = self.year, month = self.month, day = self.day,
00163 hour = self.hour, minute = self.minute, second = self.second,
00164 microsecond = 0, tzinfo = None)
00165
00166 def later(self, other):
00167 """Return the later of two timestamps."""
00168 return other if other > self else self
00169
00170 def earlier(self, other):
00171 """Return the earlier of two timestamps."""
00172 return other if other < self else self
00173
00174 class timedelta(pydatetime.timedelta):
00175 """
00176 Timedelta with text parsing. This accepts two input formats:
00177
00178 - A simple integer, indicating a number of seconds.
00179
00180 - A string of the form "wD xH yM zS" where w, x, y, and z are integers
00181 and D, H, M, and S indicate days, hours, minutes, and seconds.
00182 All of the fields are optional, but at least one must be specified.
00183 Eg, "3D4H" means "three days plus four hours".
00184 """
00185
00186
00187
00188
00189
00190
00191 regexp = re.compile("\\s*".join(("^",
00192 "(?:(?P<days>\\d+)D)?",
00193 "(?:(?P<hours>\\d+)H)?",
00194 "(?:(?P<minutes>\\d+)M)?",
00195 "(?:(?P<seconds>\\d+)S)?",
00196 "$")),
00197 re.I)
00198
00199 @classmethod
00200 def parse(cls, arg):
00201 """
00202 Parse text into a timedelta object.
00203 """
00204 if not isinstance(arg, str):
00205 return cls(seconds = arg)
00206 elif arg.isdigit():
00207 return cls(seconds = int(arg))
00208 else:
00209 match = cls.regexp.match(arg)
00210 if match:
00211 return cls(**dict((k, int(v)) for (k, v) in match.groupdict().items() if v is not None))
00212 else:
00213 raise RuntimeError, "Couldn't parse timedelta %s" % repr(arg)
00214
00215
00216 def convert_to_seconds(self):
00217 """Convert a timedelta interval to seconds."""
00218 return self.days * 24 * 60 * 60 + self.seconds
00219
00220 @classmethod
00221 def fromtimedelta(cls, x):
00222 """Convert a datetime.timedelta object into this subclass."""
00223 return cls(days = x.days, seconds = x.seconds, microseconds = x.microseconds)
00224
00225 if __name__ == "__main__":
00226
00227 def test(t):
00228 print
00229 print "str: ", t
00230 print "repr: ", repr(t)
00231 print "seconds since epoch:", t.strftime("%s")
00232 print "UTCTime: ", t.toUTCTime()
00233 print "GeneralizedTime: ", t.toGeneralizedTime()
00234 print "ASN1tuple: ", t.toASN1tuple()
00235 print "XMLtime: ", t.toXMLtime()
00236 print
00237
00238 print
00239 print "Testing time conversion routines"
00240 test(now())
00241 test(now() + timedelta(days = 30))
00242 test(now() + timedelta.parse("3d5s"))
00243 timedelta.parse(" 3d 5s ")