commit ff5980fe8bab178bc113964a0e6c31a531387c2f Author: Isis Lovecruft isis@torproject.org Date: Tue May 13 23:17:01 2014 +0000
Rewrite bridgedb.Time module. --- lib/bridgedb/Time.py | 308 +++++++++++++++++++++++++++++++++----------------- 1 file changed, 204 insertions(+), 104 deletions(-)
diff --git a/lib/bridgedb/Time.py b/lib/bridgedb/Time.py index 707bf9b..4c4a2e2 100644 --- a/lib/bridgedb/Time.py +++ b/lib/bridgedb/Time.py @@ -10,157 +10,257 @@ """This module implements functions for dividing time into chunks."""
import calendar -import time + +from datetime import datetime
from zope import interface from zope.interface import implements +from zope.interface import Attribute + + +#: The known time intervals (or *periods*) for dividing time by. +KNOWN_INTERVALS = ["second", "minute", "hour", "day", "week", "month"] + + +class UnknownInterval(ValueError): + """Raised if an interval isn't one of the :data:`KNOWN_INTERVALS`.""" +
-KNOWN_INTERVALS = [ "hour", "day", "week", "month" ] +def toUnixSeconds(timestruct): + """Convert a datetime struct to a Unix timestamp in seconds. + + :param timestruct: A ``datetime.datetime`` object to convert into a + timestamp in Unix Era seconds. + :rtype: int + """ + return calendar.timegm(timestruct) + +def fromUnixSeconds(timestamp): + """Convert a Unix timestamp to a datetime struct. + + :param int timestamp: A timestamp in Unix Era seconds. + :rtype: :type:`datetime.datetime` + """ + return datetime.fromtimestamp(timestamp)
class ISchedule(interface.Interface): """A ``Interface`` specification for a Schedule."""
- def intervalStart(when): - """Set the start time of the current interval to **when**.""" + intervalPeriod = Attribute( + "The type of period which this Schedule's intervals will rotate by.") + intervalCount = Attribute( + "Number of **intervalPeriod**s before rotation to the next interval") + + def intervalStart(when=None): + """Get the start time of the interval that contains **when**."""
def getInterval(when=None): """Get the interval which includes an arbitrary **when**."""
- def nextIntervalStarts(): - """Get the start time for the next interval.""" + def nextIntervalStarts(when=None): + """Get the start of the interval after the one containing **when**."""
-class ScheduleBase(object): - """Base class for all ``Schedule`` classes.""" +class Unscheduled(object): + """A base ``Schedule`` that has only one period that contains all time."""
implements(ISchedule)
- def intervalStart(self, when): - pass + def __init__(self, period=None, count=None): + """Create a schedule for dividing time into intervals.
- def getInterval(self, when=None): - pass + :param str period: One of the periods in :data:`KNOWN_INTERVALS`. + :param int count: The number of **period**s in an interval. + """ + self.intervalCount = count + self.intervalPeriod = period
- def nextIntervalStarts(self): - pass + def intervalStart(self, when=0): + """Get the start time of the interval that contains **when**.
+ :param int when: The time which we're trying to find the corresponding + interval for. + :rtype: int + :returns: The Unix epoch timestamp for the start time of the interval + that contains **when**. + """ + return toUnixSeconds(datetime.min.timetuple()) + + def getInterval(self, when=0): + """Get the interval that contains the time **when**.
-class IntervalSchedule(ScheduleBase): - """An IntervalSchedule splits time into somewhat natural periods, based on + :param int when: The time which we're trying to find the corresponding + interval for. + :rtype: str + :returns: A timestamp in the form ``YEAR-MONTH[-DAY[-HOUR]]``. It's + specificity depends on what type of interval we're using. For + example, if using ``"month"``, the return value would be something + like ``"2013-12"``. + """ + return fromUnixSeconds(when).strftime('%04Y-%02m-%02d %02H:%02M:%02S') + + def nextIntervalStarts(self, when=0): + """Return the start time of the interval starting _after_ when. + + :rtype: int + :returns: Return the Y10K bug. + """ + return toUnixSeconds(datetime.max.timetuple()) + + +class ScheduledInterval(Unscheduled): + """An class that splits time into periods, based on seconds, minutes, hours, days, weeks, or months.
- :ivar str itype: One of "month", "day", "hour". - :ivar int count: How many of the units in :ivar:`itype` belong to each period. + :ivar str intervalPeriod: One of the :data:`KNOWN_INTERVALS`. + :ivar int intervalCount: The number of times **intervalPeriod** should be + repeated within an interval. """ + implements(ISchedule) + + def __init__(self, period=None, count=None): + """Create a schedule for dividing time into intervals. + + :param str period: One of the periods in :data:`KNOWN_INTERVALS`. + :param int count: The number of **period**s in an interval. + """ + super(ScheduledInterval, self).__init__(period, count) + self._setIntervalCount(count) + self._setIntervalPeriod(period)
- def __init__(self, intervaltype, count): - """Divide time into intervals of **count** number of **intervaltype**. + def _setIntervalCount(self, count=None): + """Set our :ivar:`intervalCount`.
- :param str intervaltype: One of ``'month'``, ``'week'``, ``'day'``, - or ``'hour'``. + .. attention:: This method should be called _before_ + :meth:`_setIntervalPeriod`, because the latter may change the + count, if it decides to change the period (for example, to + simplify things by changing weeks into days).
- :param int count: How many of the units in **intervaltype** belong to - each period. + :param int count: The number of times the :ivar:`intervalPeriod` + should be repeated during the interval. Defaults to ``1``. + :raises UnknownInterval: if the specified **count** was invalid. + """ + try: + if not count > 0: + count = 1 + count = int(count) + except (TypeError, ValueError): + raise UnknownInterval("%s.intervalCount: %r ist not an integer." + % (self.__class__.__name__, count)) + self.intervalCount = count + + def _setIntervalPeriod(self, period=None): + """Set our :ivar:`intervalPeriod`. + + :param str period: One of the :data:`KNOWN_INTERVALS`, or its + plural. Defaults to ``'hour'``. + :raises UnknownInterval: if the specified **period** is unknown. """ - it = intervaltype.lower() - if it.endswith("s"): it = it[:-1] - if it not in KNOWN_INTERVALS: - raise TypeError("What's a %s?"%it) - assert count > 0 - if it == 'week': - it = 'day' - count *= 7 - self.itype = it - self.count = count - - def intervalStart(self, when): + if not period: + period = 'hour' + try: + period = period.lower() + # Depluralise the period if necessary, i.e., "months" -> "month". + if period.endswith('s'): + period = period[:-1] + + if not period in KNOWN_INTERVALS: + raise ValueError + except (TypeError, AttributeError, ValueError): + raise UnknownInterval("%s doesn't know about the %r interval type." + % (self.__class__.__name__, period)) + self.intervalPeriod = period + + if period == 'week': + self.intervalPeriod = 'day' + self.intervalCount *= 7 + + def intervalStart(self, when=0): """Get the start time of the interval that contains **when**.
+ :param int when: The time which we're trying to determine the start of + interval that contains it. This should be given in Unix seconds, + for example, taken from :func:`calendar.timegm`. :rtype: int :returns: The Unix epoch timestamp for the start time of the interval that contains **when**. """ - if self.itype == 'month': + if self.intervalPeriod == 'month': # For months, we always start at the beginning of the month. - tm = time.gmtime(when) - n = tm.tm_year * 12 + tm.tm_mon - 1 - n -= (n % self.count) - month = n%12 + 1 - return calendar.timegm((n//12, month, 1, 0, 0, 0)) - elif self.itype == 'day': + date = fromUnixSeconds(when) + months = (date.year * 12) + (date.month - 1) + months -= (months % self.intervalCount) + month = months % 12 + 1 + return toUnixSeconds((months // 12, month, 1, 0, 0, 0)) + elif self.intervalPeriod == 'day': # For days, we start at the beginning of a day. - when -= when % (86400 * self.count) + when -= when % (86400 * self.intervalCount) return when - elif self.itype == 'hour': + elif self.intervalPeriod == 'hour': # For hours, we start at the beginning of an hour. - when -= when % (3600 * self.count) + when -= when % (3600 * self.intervalCount) + return when + elif self.intervalPeriod == 'minute': + when -= when % (60 * self.intervalCount) + return when + elif self.intervalPeriod == 'second': + when -= when % self.intervalCount return when - else: - assert False
- def getInterval(self, when): + def getInterval(self, when=0): """Get the interval that contains the time **when**.
>>> import calendar - >>> from bridgedb.Time import IntervalSchedule - >>> t = calendar.timegm((2007, 12, 12, 0, 0, 0)) - >>> I = IntervalSchedule('month', 1) - >>> I.getInterval(t) + >>> from bridgedb.Time import ScheduledInterval + >>> sched = ScheduledInterval('month', 1) + >>> when = calendar.timegm((2007, 12, 12, 0, 0, 0)) + >>> sched.getInterval(when) '2007-12' + >>> then = calendar.timegm((2014, 05, 13, 20, 25, 13)) + >>> sched.getInterval(then) + '2014-05'
:param int when: The time which we're trying to find the corresponding - interval for. + interval for. Given in Unix seconds, for example, taken from + :func:`calendar.timegm`. :rtype: str :returns: A timestamp in the form ``YEAR-MONTH[-DAY[-HOUR]]``. It's - specificity depends on what type of interval we're - using. For example, if using ``"month"``, the return value - would be something like ``"2013-12"``. + specificity depends on what type of interval we're using. For + example, if using ``"month"``, the return value would be something + like ``"2013-12"``. + """ + date = fromUnixSeconds(self.intervalStart(when)) + + fstr = "%04Y-%02m" + if self.intervalPeriod != 'month': + fstr += "-%02d" + if self.intervalPeriod != 'day': + fstr += " %02H" + if self.intervalPeriod != 'hour': + fstr += ":%02M" + if self.intervalPeriod == 'minute': + fstr += ":%02S" + + return date.strftime(fstr) + + def nextIntervalStarts(self, when=0): + """Return the start time of the interval starting _after_ when. + + :returns: The Unix epoch timestamp for the start time of the interval + that contains **when**. """ - if self.itype == 'month': - tm = time.gmtime(when) - n = tm.tm_year * 12 + tm.tm_mon - 1 - n -= (n % self.count) - month = n%12 + 1 - return "%04d-%02d" % (n // 12, month) - elif self.itype == 'day': - when = self.intervalStart(when) + 7200 #slop - tm = time.gmtime(when) - return "%04d-%02d-%02d" % (tm.tm_year, tm.tm_mon, tm.tm_mday) - elif self.itype == 'hour': - when = self.intervalStart(when) + 120 #slop - tm = time.gmtime(when) - return "%04d-%02d-%02d %02d" % (tm.tm_year, tm.tm_mon, tm.tm_mday, - tm.tm_hour) - else: - assert False - - def nextIntervalStarts(self, when): - """Return the start time of the interval starting _after_ when.""" - if self.itype == 'month': - tm = time.gmtime(when) - n = tm.tm_year * 12 + tm.tm_mon - 1 - n -= (n % self.count) - month = n%12 + 1 - tm = (n // 12, month+self.count, 1, 0,0,0) - return calendar.timegm(tm) - elif self.itype == 'day': - return self.intervalStart(when) + 86400 * self.count - elif self.itype == 'hour': - return self.intervalStart(when) + 3600 * self.count - - -class NoSchedule(ScheduleBase): - """A Schedule that has only one period for all time.""" - - def __init__(self): - pass - - def intervalStart(self, when): - return 0 - - def getInterval(self, when): - return "1970" - - def nextIntervalStarts(self, when): - return 2147483647L # INT32_MAX + seconds = self.intervalStart(when) + + if self.intervalPeriod == 'month': + date = fromUnixSeconds(seconds) + months = date.month + self.intervalCount + return toUnixSeconds((date.year, months, 1, 0, 0, 0)) + elif self.intervalPeriod == 'day': + return seconds + (86400 * self.intervalCount) + elif self.intervalPeriod == 'hour': + return seconds + (3600 * self.intervalCount) + elif self.intervalPeriod == 'minute': + return seconds + (60 * self.intervalCount) + elif self.intervalPeriod == 'second': + return seconds + self.intervalCount
tor-commits@lists.torproject.org