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