__init__.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. from datetime import datetime, timedelta
  2. from tzlocal import get_localzone
  3. import six
  4. from apscheduler.triggers.base import BaseTrigger
  5. from apscheduler.triggers.cron.fields import (
  6. BaseField, WeekField, DayOfMonthField, DayOfWeekField, DEFAULT_VALUES)
  7. from apscheduler.util import datetime_ceil, convert_to_datetime, datetime_repr, astimezone
  8. class CronTrigger(BaseTrigger):
  9. """
  10. Triggers when current time matches all specified time constraints,
  11. similarly to how the UNIX cron scheduler works.
  12. :param int|str year: 4-digit year
  13. :param int|str month: month (1-12)
  14. :param int|str day: day of the (1-31)
  15. :param int|str week: ISO week (1-53)
  16. :param int|str day_of_week: number or name of weekday (0-6 or mon,tue,wed,thu,fri,sat,sun)
  17. :param int|str hour: hour (0-23)
  18. :param int|str minute: minute (0-59)
  19. :param int|str second: second (0-59)
  20. :param datetime|str start_date: earliest possible date/time to trigger on (inclusive)
  21. :param datetime|str end_date: latest possible date/time to trigger on (inclusive)
  22. :param datetime.tzinfo|str timezone: time zone to use for the date/time calculations (defaults
  23. to scheduler timezone)
  24. .. note:: The first weekday is always **monday**.
  25. """
  26. FIELD_NAMES = ('year', 'month', 'day', 'week', 'day_of_week', 'hour', 'minute', 'second')
  27. FIELDS_MAP = {
  28. 'year': BaseField,
  29. 'month': BaseField,
  30. 'week': WeekField,
  31. 'day': DayOfMonthField,
  32. 'day_of_week': DayOfWeekField,
  33. 'hour': BaseField,
  34. 'minute': BaseField,
  35. 'second': BaseField
  36. }
  37. __slots__ = 'timezone', 'start_date', 'end_date', 'fields'
  38. def __init__(self, year=None, month=None, day=None, week=None, day_of_week=None, hour=None,
  39. minute=None, second=None, start_date=None, end_date=None, timezone=None):
  40. if timezone:
  41. self.timezone = astimezone(timezone)
  42. elif start_date and start_date.tzinfo:
  43. self.timezone = start_date.tzinfo
  44. elif end_date and end_date.tzinfo:
  45. self.timezone = end_date.tzinfo
  46. else:
  47. self.timezone = get_localzone()
  48. self.start_date = convert_to_datetime(start_date, self.timezone, 'start_date')
  49. self.end_date = convert_to_datetime(end_date, self.timezone, 'end_date')
  50. values = dict((key, value) for (key, value) in six.iteritems(locals())
  51. if key in self.FIELD_NAMES and value is not None)
  52. self.fields = []
  53. assign_defaults = False
  54. for field_name in self.FIELD_NAMES:
  55. if field_name in values:
  56. exprs = values.pop(field_name)
  57. is_default = False
  58. assign_defaults = not values
  59. elif assign_defaults:
  60. exprs = DEFAULT_VALUES[field_name]
  61. is_default = True
  62. else:
  63. exprs = '*'
  64. is_default = True
  65. field_class = self.FIELDS_MAP[field_name]
  66. field = field_class(field_name, exprs, is_default)
  67. self.fields.append(field)
  68. def _increment_field_value(self, dateval, fieldnum):
  69. """
  70. Increments the designated field and resets all less significant fields to their minimum
  71. values.
  72. :type dateval: datetime
  73. :type fieldnum: int
  74. :return: a tuple containing the new date, and the number of the field that was actually
  75. incremented
  76. :rtype: tuple
  77. """
  78. values = {}
  79. i = 0
  80. while i < len(self.fields):
  81. field = self.fields[i]
  82. if not field.REAL:
  83. if i == fieldnum:
  84. fieldnum -= 1
  85. i -= 1
  86. else:
  87. i += 1
  88. continue
  89. if i < fieldnum:
  90. values[field.name] = field.get_value(dateval)
  91. i += 1
  92. elif i > fieldnum:
  93. values[field.name] = field.get_min(dateval)
  94. i += 1
  95. else:
  96. value = field.get_value(dateval)
  97. maxval = field.get_max(dateval)
  98. if value == maxval:
  99. fieldnum -= 1
  100. i -= 1
  101. else:
  102. values[field.name] = value + 1
  103. i += 1
  104. difference = datetime(**values) - dateval.replace(tzinfo=None)
  105. return self.timezone.normalize(dateval + difference), fieldnum
  106. def _set_field_value(self, dateval, fieldnum, new_value):
  107. values = {}
  108. for i, field in enumerate(self.fields):
  109. if field.REAL:
  110. if i < fieldnum:
  111. values[field.name] = field.get_value(dateval)
  112. elif i > fieldnum:
  113. values[field.name] = field.get_min(dateval)
  114. else:
  115. values[field.name] = new_value
  116. return self.timezone.localize(datetime(**values))
  117. def get_next_fire_time(self, previous_fire_time, now):
  118. if previous_fire_time:
  119. start_date = min(now, previous_fire_time + timedelta(microseconds=1))
  120. if start_date == previous_fire_time:
  121. start_date += timedelta(microseconds=1)
  122. else:
  123. start_date = max(now, self.start_date) if self.start_date else now
  124. fieldnum = 0
  125. next_date = datetime_ceil(start_date).astimezone(self.timezone)
  126. while 0 <= fieldnum < len(self.fields):
  127. field = self.fields[fieldnum]
  128. curr_value = field.get_value(next_date)
  129. next_value = field.get_next_value(next_date)
  130. if next_value is None:
  131. # No valid value was found
  132. next_date, fieldnum = self._increment_field_value(next_date, fieldnum - 1)
  133. elif next_value > curr_value:
  134. # A valid, but higher than the starting value, was found
  135. if field.REAL:
  136. next_date = self._set_field_value(next_date, fieldnum, next_value)
  137. fieldnum += 1
  138. else:
  139. next_date, fieldnum = self._increment_field_value(next_date, fieldnum)
  140. else:
  141. # A valid value was found, no changes necessary
  142. fieldnum += 1
  143. # Return if the date has rolled past the end date
  144. if self.end_date and next_date > self.end_date:
  145. return None
  146. if fieldnum >= 0:
  147. return next_date
  148. def __getstate__(self):
  149. return {
  150. 'version': 1,
  151. 'timezone': self.timezone,
  152. 'start_date': self.start_date,
  153. 'end_date': self.end_date,
  154. 'fields': self.fields
  155. }
  156. def __setstate__(self, state):
  157. # This is for compatibility with APScheduler 3.0.x
  158. if isinstance(state, tuple):
  159. state = state[1]
  160. if state.get('version', 1) > 1:
  161. raise ValueError(
  162. 'Got serialized data for version %s of %s, but only version 1 can be handled' %
  163. (state['version'], self.__class__.__name__))
  164. self.timezone = state['timezone']
  165. self.start_date = state['start_date']
  166. self.end_date = state['end_date']
  167. self.fields = state['fields']
  168. def __str__(self):
  169. options = ["%s='%s'" % (f.name, f) for f in self.fields if not f.is_default]
  170. return 'cron[%s]' % (', '.join(options))
  171. def __repr__(self):
  172. options = ["%s='%s'" % (f.name, f) for f in self.fields if not f.is_default]
  173. if self.start_date:
  174. options.append("start_date='%s'" % datetime_repr(self.start_date))
  175. return "<%s (%s, timezone='%s')>" % (
  176. self.__class__.__name__, ', '.join(options), self.timezone)