RPKI Engine 1.0

sundial.py (3783)

Go to the documentation of this file.
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 3783 2011-04-21 22:01:02Z sra $
00017 
00018 Copyright (C) 2009--2011  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   """
00052   Get current timestamp.
00053   """
00054   return datetime.utcnow()
00055 
00056 class datetime(pydatetime.datetime):
00057   """
00058   RPKI extensions to standard datetime.datetime class.  All work here
00059   is in UTC, so we use naive datetime objects.
00060   """
00061 
00062   def totimestamp(self):
00063     """
00064     Convert to seconds from epoch (like time.time()).  Conversion
00065     method is a bit silly, but avoids time module timezone whackiness.
00066     """
00067     return int(self.strftime("%s"))
00068 
00069   @classmethod
00070   def fromUTCTime(cls, x):
00071     """
00072     Convert from ASN.1 UTCTime.
00073     """
00074     x = str(x)
00075     return cls.fromGeneralizedTime(("19" if x[0] >= "5" else "20") + x)
00076 
00077   def toUTCTime(self):
00078     """
00079     Convert to ASN.1 UTCTime.
00080     """
00081     return self.strftime("%y%m%d%H%M%SZ")
00082 
00083   @classmethod
00084   def fromGeneralizedTime(cls, x):
00085     """
00086     Convert from ASN.1 GeneralizedTime.
00087     """
00088     return cls.strptime(x, "%Y%m%d%H%M%SZ")
00089 
00090   def toGeneralizedTime(self):
00091     """
00092     Convert to ASN.1 GeneralizedTime.
00093     """
00094     return self.strftime("%Y%m%d%H%M%SZ")
00095 
00096   @classmethod
00097   def fromASN1tuple(cls, x):
00098     """
00099     Convert from ASN.1 tuple representation.
00100     """
00101     assert isinstance(x, tuple) and len(x) == 2 and x[0] in ("utcTime", "generalTime")
00102     if x[0] == "utcTime":
00103       return cls.fromUTCTime(x[1])
00104     else:
00105       return cls.fromGeneralizedTime(x[1])
00106 
00107   ## @var PKIX_threshhold
00108   # Threshold specified in RFC 3280 for switchover from UTCTime to GeneralizedTime.
00109 
00110   PKIX_threshhold = pydatetime.datetime(2050, 1, 1)
00111 
00112   def toASN1tuple(self):
00113     """
00114     Convert to ASN.1 tuple representation.
00115     """
00116     if self < self.PKIX_threshhold:
00117       return "utcTime", self.toUTCTime()
00118     else:
00119       return "generalTime", self.toGeneralizedTime()
00120 
00121   @classmethod
00122   def fromXMLtime(cls, x):
00123     """
00124     Convert from XML time representation.
00125     """
00126     if x is None:
00127       return None
00128     else:
00129       return cls.strptime(x, "%Y-%m-%dT%H:%M:%SZ")
00130 
00131   def toXMLtime(self):
00132     """
00133     Convert to XML time representation.
00134     """
00135     return self.strftime("%Y-%m-%dT%H:%M:%SZ")
00136 
00137   def __str__(self):
00138     return self.toXMLtime()
00139 
00140   @classmethod
00141   def fromdatetime(cls, x):
00142     """
00143     Convert a datetime.datetime object into this subclass.  This is
00144     whacky due to the weird constructors for datetime.
00145     """
00146     return cls.combine(x.date(), x.time())
00147 
00148   @classmethod
00149   def fromOpenSSL(cls, x):
00150     """
00151     Convert from the format OpenSSL's command line tool uses into this
00152     subclass.  May require rewriting if we run into locale problems.
00153     """
00154     if x.startswith("notBefore=") or x.startswith("notAfter="):
00155       x = x.partition("=")[2]
00156     return cls.strptime(x, "%b %d %H:%M:%S %Y GMT")
00157 
00158   @classmethod
00159   def from_sql(cls, x):
00160     """
00161     Convert from SQL storage format.
00162     """
00163     return cls.fromdatetime(x)
00164 
00165   def to_sql(self):
00166     """
00167     Convert to SQL storage format.
00168 
00169     There's something whacky going on in the MySQLdb module, it throws
00170     range errors when storing a derived type into a DATETIME column.
00171     Investigate some day, but for now brute force this by copying the
00172     relevant fields into a datetime.datetime for MySQLdb's
00173     consumption.
00174 
00175     """
00176     return pydatetime.datetime(year = self.year, month = self.month, day = self.day,
00177                                hour = self.hour, minute = self.minute, second = self.second,
00178                                microsecond = 0, tzinfo = None)
00179 
00180   def later(self, other):
00181     """
00182     Return the later of two timestamps.
00183     """
00184     return other if other > self else self
00185 
00186   def earlier(self, other):
00187     """
00188     Return the earlier of two timestamps.
00189     """
00190     return other if other < self else self
00191 
00192   def __add__(self, y):  return _cast(pydatetime.datetime.__add__(self, y))
00193   def __radd__(self, y): return _cast(pydatetime.datetime.__radd__(self, y))
00194   def __rsub__(self, y): return _cast(pydatetime.datetime.__rsub__(self, y))
00195   def __sub__(self, y):  return _cast(pydatetime.datetime.__sub__(self, y))
00196 
00197 class timedelta(pydatetime.timedelta):
00198   """
00199   Timedelta with text parsing.  This accepts two input formats:
00200 
00201   - A simple integer, indicating a number of seconds.
00202 
00203   - A string of the form "uY vW wD xH yM zS" where u, v, w, x, y, and z
00204     are integers and Y, W, D, H, M, and S indicate years, weeks, days,
00205     hours, minutes, and seconds.  All of the fields are optional, but
00206     at least one must be specified.  Eg,"3D4H" means "three days plus
00207     four hours".
00208 
00209   There is no "months" format, because the definition of a month is too
00210   fuzzy to be useful (what day is six months from August 30th?)
00211 
00212   Similarly, the "years" conversion may produce surprising results, as
00213   "one year" in conventional English does not refer to a fixed interval
00214   but rather a fixed (and in some cases undefined) offset within the
00215   Gregorian calendar (what day is one year from February 29th?)  1Y as
00216   implemented by this code refers to a specific number of seconds.
00217   If you mean 365 days or 52 weeks, say that instead.
00218   """
00219 
00220   ## @var regexp
00221   # Hideously ugly regular expression to parse the complex text form.
00222   # Tags are intended for use with re.MatchObject.groupdict() and map
00223   # directly to the keywords expected by the timedelta constructor.
00224 
00225   regexp = re.compile("\\s*".join(("^",
00226                                    "(?:(?P<years>\\d+)Y)?",
00227                                    "(?:(?P<weeks>\\d+)W)?",
00228                                    "(?:(?P<days>\\d+)D)?",
00229                                    "(?:(?P<hours>\\d+)H)?",
00230                                    "(?:(?P<minutes>\\d+)M)?",
00231                                    "(?:(?P<seconds>\\d+)S)?",
00232                                    "$")),
00233                       re.I)
00234 
00235   ## @var years_to_seconds
00236   # Conversion factor from years to seconds (value furnished by the
00237   # "units" program).
00238 
00239   years_to_seconds = 31556926
00240 
00241   @classmethod
00242   def parse(cls, arg):
00243     """
00244     Parse text into a timedelta object.
00245     """
00246     if not isinstance(arg, str):
00247       return cls(seconds = arg)
00248     elif arg.isdigit():
00249       return cls(seconds = int(arg))
00250     else:
00251       match = cls.regexp.match(arg)
00252       if match:
00253         #return cls(**dict((k, int(v)) for (k, v) in match.groupdict().items() if v is not None))
00254         d = match.groupdict("0")
00255         for k, v in d.iteritems():
00256           d[k] = int(v)
00257         d["days"]    += d.pop("weeks") * 7
00258         d["seconds"] += d.pop("years") * cls.years_to_seconds
00259         return cls(**d)
00260       else:
00261         raise RuntimeError, "Couldn't parse timedelta %r" % (arg,)
00262 
00263   def convert_to_seconds(self):
00264     """
00265     Convert a timedelta interval to seconds.
00266     """
00267     return self.days * 24 * 60 * 60 + self.seconds
00268 
00269   @classmethod
00270   def fromtimedelta(cls, x):
00271     """
00272     Convert a datetime.timedelta object into this subclass.
00273     """
00274     return cls(days = x.days, seconds = x.seconds, microseconds = x.microseconds)
00275 
00276   def __abs__(self):          return _cast(pydatetime.timedelta.__abs__(self))
00277   def __add__(self, x):       return _cast(pydatetime.timedelta.__add__(self, x))
00278   def __div__(self, x):       return _cast(pydatetime.timedelta.__div__(self, x))
00279   def __floordiv__(self, x):  return _cast(pydatetime.timedelta.__floordiv__(self, x))
00280   def __mul__(self, x):       return _cast(pydatetime.timedelta.__mul__(self, x))
00281   def __neg__(self):          return _cast(pydatetime.timedelta.__neg__(self))
00282   def __pos__(self):          return _cast(pydatetime.timedelta.__pos__(self))
00283   def __radd__(self, x):      return _cast(pydatetime.timedelta.__radd__(self, x))
00284   def __rdiv__(self, x):      return _cast(pydatetime.timedelta.__rdiv__(self, x))
00285   def __rfloordiv__(self, x): return _cast(pydatetime.timedelta.__rfloordiv__(self, x))
00286   def __rmul__(self, x):      return _cast(pydatetime.timedelta.__rmul__(self, x))
00287   def __rsub__(self, x):      return _cast(pydatetime.timedelta.__rsub__(self, x))
00288   def __sub__(self, x):       return _cast(pydatetime.timedelta.__sub__(self, x))
00289 
00290 def _cast(x):
00291   """
00292   Cast result of arithmetic operations back into correct subtype.
00293   """
00294   if isinstance(x, pydatetime.datetime):
00295     return datetime.fromdatetime(x)
00296   if isinstance(x, pydatetime.timedelta):
00297     return timedelta.fromtimedelta(x)
00298   return x
00299 
00300 if __name__ == "__main__":
00301 
00302   def test(t):
00303     print
00304     print "str:                ", t
00305     print "repr:               ", repr(t)
00306     print "seconds since epoch:", t.strftime("%s")
00307     print "UTCTime:            ", t.toUTCTime()
00308     print "GeneralizedTime:    ", t.toGeneralizedTime()
00309     print "ASN1tuple:          ", t.toASN1tuple()
00310     print "XMLtime:            ", t.toXMLtime()
00311     print
00312 
00313   print
00314   print "Testing time conversion routines"
00315   test(now())
00316   test(now() + timedelta(days = 30))
00317   test(now() + timedelta.parse("3d5s"))
00318   test(now() + timedelta.parse(" 3d 5s "))
00319   test(now() + timedelta.parse("1y3d5h"))
 All Classes Namespaces Files Functions Variables