commit e35194ecc67f40e69635fa70adee7331471de337 Author: Isis Lovecruft isis@torproject.org Date: Tue May 13 23:19:06 2014 +0000
Move lib/bridgedb/Time.py → lib/bridgedb/schedule.py. --- lib/bridgedb/HTTPServer.py | 6 +- lib/bridgedb/Main.py | 10 +- lib/bridgedb/Tests.py | 4 +- lib/bridgedb/Time.py | 266 ------------------------------- lib/bridgedb/email/server.py | 4 +- lib/bridgedb/schedule.py | 267 ++++++++++++++++++++++++++++++++ lib/bridgedb/test/test_HTTPServer.py | 4 +- lib/bridgedb/test/test_email_server.py | 6 +- 8 files changed, 284 insertions(+), 283 deletions(-)
diff --git a/lib/bridgedb/HTTPServer.py b/lib/bridgedb/HTTPServer.py index b436df2..8289be3 100644 --- a/lib/bridgedb/HTTPServer.py +++ b/lib/bridgedb/HTTPServer.py @@ -481,7 +481,7 @@ class ReCaptchaProtectedResource(CaptchaProtectedResource): ``'captcha_response_field'``. These POST arguments should be obtained from :meth:`render_GET`. :rtupe: :api:`twisted.internet.defer.Deferred` - :returns: A deferred wich will callback with a tuple in the following + :returns: A deferred which will callback with a tuple in the following form: (:type:`bool`, :api:`twisted.web.server.Request`) If the CAPTCHA solution was valid, a tuple will contain:: @@ -619,7 +619,7 @@ class WebResourceBridges(resource.Resource): :type distributor: :class:`IPBasedDistributor` :param distributor: The mechanism to retrieve bridges for this distributor. - :type schedule: :class:`IntervalSchedule` + :type schedule: :class:`~bridgedb.schedule.ScheduledInterval` :param schedule: The time period used to tweak the bridge selection procedure. :param int N: The number of bridges to hand out per query. @@ -669,7 +669,7 @@ class WebResourceBridges(resource.Resource): :returns: A plaintext or HTML response to serve. """ # XXX why are we getting the interval if our distributor might be - # using bridgedb.Time.NoSchedule? + # using bridgedb.schedule.Unscheduled? interval = self.schedule.getInterval(time.time()) bridges = ( ) ip = None diff --git a/lib/bridgedb/Main.py b/lib/bridgedb/Main.py index 1b4420e..ccc844d 100644 --- a/lib/bridgedb/Main.py +++ b/lib/bridgedb/Main.py @@ -20,12 +20,12 @@ from twisted.internet import reactor from bridgedb import crypto from bridgedb import persistent from bridgedb import safelog +from bridgedb import schedule from bridgedb import util from bridgedb.parse import options
import bridgedb.Bridges as Bridges import bridgedb.Dist as Dist -import bridgedb.Time as Time import bridgedb.Storage
@@ -590,12 +590,12 @@ def startup(options):
# Configure all servers: if config.HTTPS_DIST and config.HTTPS_SHARE: - #webSchedule = Time.IntervalSchedule("day", 2) - webSchedule = Time.NoSchedule() + #webSchedule = schedule.ScheduledInterval("day", 2) + webSchedule = schedule.Unscheduled() HTTPServer.addWebServer(config, ipDistributor, webSchedule) if config.EMAIL_DIST and config.EMAIL_SHARE: - #emailSchedule = Time.IntervalSchedule("day", 1) - emailSchedule = Time.NoSchedule() + #emailSchedule = schedule.ScheduledInterval("day", 1) + emailSchedule = schedule.Unscheduled() addSMTPServer(config, emailDistributor, emailSchedule)
# Actually run the servers. diff --git a/lib/bridgedb/Tests.py b/lib/bridgedb/Tests.py index 6fcdbc6..219802b 100644 --- a/lib/bridgedb/Tests.py +++ b/lib/bridgedb/Tests.py @@ -17,7 +17,7 @@ from datetime import datetime import bridgedb.Bridges import bridgedb.Main import bridgedb.Dist -import bridgedb.Time +import bridgedb.schedule import bridgedb.Storage import re import ipaddr @@ -768,7 +768,7 @@ def testSuite(): for module in [ bridgedb.Bridges, bridgedb.Main, bridgedb.Dist, - bridgedb.Time ]: + bridgedb.schedule ]: suite.addTest(doctest.DocTestSuite(module))
return suite diff --git a/lib/bridgedb/Time.py b/lib/bridgedb/Time.py deleted file mode 100644 index 4c4a2e2..0000000 --- a/lib/bridgedb/Time.py +++ /dev/null @@ -1,266 +0,0 @@ -# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_Time -*- -# -# This file is part of BridgeDB, a Tor bridge distribution system. -# -# :authors: Nick Mathewson -# :copyright: (c) 2007-2014, The Tor Project, Inc. -# :license: see LICENSE for licensing information - - -"""This module implements functions for dividing time into chunks.""" - -import calendar - -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`.""" - - -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.""" - - 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(when=None): - """Get the start of the interval after the one containing **when**.""" - - -class Unscheduled(object): - """A base ``Schedule`` that has only one period that contains all time.""" - - 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. - """ - self.intervalCount = count - self.intervalPeriod = period - - 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**. - - :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 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 _setIntervalCount(self, count=None): - """Set our :ivar:`intervalCount`. - - .. 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: 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. - """ - 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.intervalPeriod == 'month': - # For months, we always start at the beginning of the month. - 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.intervalCount) - return when - elif self.intervalPeriod == 'hour': - # For hours, we start at the beginning of an hour. - 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 - - def getInterval(self, when=0): - """Get the interval that contains the time **when**. - - >>> import calendar - >>> 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. 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"``. - """ - 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**. - """ - 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 diff --git a/lib/bridgedb/email/server.py b/lib/bridgedb/email/server.py index 678c154..e963e61 100644 --- a/lib/bridgedb/email/server.py +++ b/lib/bridgedb/email/server.py @@ -212,7 +212,7 @@ class MailContext(object): :type config: :class:`bridgedb.persistent.Conf` :type distributor: :class:`bridgedb.Dist.EmailBasedDistributor`. :param distributor: DOCDOC - :type schedule: :class:`bridgedb.Time.IntervalSchedule`. + :type schedule: :class:`bridgedb.schedule.ScheduledInterval`. :param schedule: DOCDOC """ self.config = config @@ -740,7 +740,7 @@ def addServer(config, distributor, schedule): :type distributor: :class:`bridgedb.Dist.EmailBasedDistributor` :param dist: A distributor which will handle database interactions, and will decide which bridges to give to who and when. - :type schedule: :class:`bridgedb.Time.IntervalSchedule` + :type schedule: :class:`bridgedb.schedule.ScheduledInterval` :param schedule: The schedule. XXX: Is this even used? """ context = MailContext(config, distributor, schedule) diff --git a/lib/bridgedb/schedule.py b/lib/bridgedb/schedule.py new file mode 100644 index 0000000..6cef1a6 --- /dev/null +++ b/lib/bridgedb/schedule.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 ; test-case-name: bridgedb.test.test_schedule -*- +# +# This file is part of BridgeDB, a Tor bridge distribution system. +# +# :authors: Nick Mathewson +# Isis Lovecruft isis@torproject.org 0xa3adb67a2cdb8b35 +# :copyright: (c) 2007-2014, The Tor Project, Inc. +# (c) 2014, Isis Lovecruft +# :license: see LICENSE for licensing information + +"""This module implements functions for dividing time into chunks.""" + +import calendar + +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`.""" + + +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.""" + + 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(when=None): + """Get the start of the interval after the one containing **when**.""" + + +class Unscheduled(object): + """A base ``Schedule`` that has only one period that contains all time.""" + + 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. + """ + self.intervalCount = count + self.intervalPeriod = period + + 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**. + + :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 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 _setIntervalCount(self, count=None): + """Set our :ivar:`intervalCount`. + + .. 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: 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. + """ + 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.intervalPeriod == 'month': + # For months, we always start at the beginning of the month. + 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.intervalCount) + return when + elif self.intervalPeriod == 'hour': + # For hours, we start at the beginning of an hour. + 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 + + def getInterval(self, when=0): + """Get the interval that contains the time **when**. + + >>> import calendar + >>> from bridgedb.schedule 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. 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"``. + """ + 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**. + """ + 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 diff --git a/lib/bridgedb/test/test_HTTPServer.py b/lib/bridgedb/test/test_HTTPServer.py index 8bc1da7..91ab17e 100644 --- a/lib/bridgedb/test/test_HTTPServer.py +++ b/lib/bridgedb/test/test_HTTPServer.py @@ -29,7 +29,7 @@ from twisted.web.resource import Resource from twisted.web.test import requesthelper
from bridgedb import HTTPServer -from bridgedb.Time import IntervalSchedule +from bridgedb.schedule import ScheduledInterval
# For additional logger output for debugging, comment out the following: @@ -518,7 +518,7 @@ class WebResourceBridgesTests(unittest.TestCase): self.root = Resource()
self.dist = DummyIPBasedDistributor() - self.sched = IntervalSchedule('hour', 1) + self.sched = ScheduledInterval('hour', 1) self.nBridgesPerRequest = 2 self.bridgesResource = HTTPServer.WebResourceBridges( self.dist, self.sched, N=2, diff --git a/lib/bridgedb/test/test_email_server.py b/lib/bridgedb/test/test_email_server.py index b7e12c6..23c65b8 100644 --- a/lib/bridgedb/test/test_email_server.py +++ b/lib/bridgedb/test/test_email_server.py @@ -21,9 +21,9 @@ import types
from bridgedb.Dist import EmailBasedDistributor from bridgedb.email import server -from bridgedb.Time import NoSchedule from bridgedb.parse.addr import BadEmail from bridgedb.persistent import Conf +from bridgedb.schedule import Unscheduled from bridgedb.test.test_HTTPServer import DummyBridge from bridgedb.test.util import fileCheckDecorator
@@ -73,7 +73,7 @@ def _createMailContext(config=None, distributor=None): domainmap=config.EMAIL_DOMAIN_MAP, domainrules=config.EMAIL_DOMAIN_RULES)
- context = server.MailContext(config, distributor, NoSchedule()) + context = server.MailContext(config, distributor, Unscheduled()) return context
@@ -426,4 +426,4 @@ class EmailServerServiceTests(unittest.TestCase): self.skip = True raise unittest.SkipTest("Not finished yet") from twisted.internet import reactor - server.addServer(self.config, self.distributor, NoSchedule) + server.addServer(self.config, self.distributor, Unscheduled)
tor-commits@lists.torproject.org