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