expressions.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. """This module contains the expressions applicable for CronTrigger's fields."""
  2. from calendar import monthrange
  3. import re
  4. from apscheduler.util import asint
  5. __all__ = ('AllExpression', 'RangeExpression', 'WeekdayRangeExpression',
  6. 'WeekdayPositionExpression', 'LastDayOfMonthExpression')
  7. WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
  8. MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
  9. class AllExpression(object):
  10. value_re = re.compile(r'\*(?:/(?P<step>\d+))?$')
  11. def __init__(self, step=None):
  12. self.step = asint(step)
  13. if self.step == 0:
  14. raise ValueError('Increment must be higher than 0')
  15. def validate_range(self, field_name):
  16. from apscheduler.triggers.cron.fields import MIN_VALUES, MAX_VALUES
  17. value_range = MAX_VALUES[field_name] - MIN_VALUES[field_name]
  18. if self.step and self.step > value_range:
  19. raise ValueError('the step value ({}) is higher than the total range of the '
  20. 'expression ({})'.format(self.step, value_range))
  21. def get_next_value(self, date, field):
  22. start = field.get_value(date)
  23. minval = field.get_min(date)
  24. maxval = field.get_max(date)
  25. start = max(start, minval)
  26. if not self.step:
  27. next = start
  28. else:
  29. distance_to_next = (self.step - (start - minval)) % self.step
  30. next = start + distance_to_next
  31. if next <= maxval:
  32. return next
  33. def __eq__(self, other):
  34. return isinstance(other, self.__class__) and self.step == other.step
  35. def __str__(self):
  36. if self.step:
  37. return '*/%d' % self.step
  38. return '*'
  39. def __repr__(self):
  40. return "%s(%s)" % (self.__class__.__name__, self.step)
  41. class RangeExpression(AllExpression):
  42. value_re = re.compile(
  43. r'(?P<first>\d+)(?:-(?P<last>\d+))?(?:/(?P<step>\d+))?$')
  44. def __init__(self, first, last=None, step=None):
  45. super(RangeExpression, self).__init__(step)
  46. first = asint(first)
  47. last = asint(last)
  48. if last is None and step is None:
  49. last = first
  50. if last is not None and first > last:
  51. raise ValueError('The minimum value in a range must not be higher than the maximum')
  52. self.first = first
  53. self.last = last
  54. def validate_range(self, field_name):
  55. from apscheduler.triggers.cron.fields import MIN_VALUES, MAX_VALUES
  56. super(RangeExpression, self).validate_range(field_name)
  57. if self.first < MIN_VALUES[field_name]:
  58. raise ValueError('the first value ({}) is lower than the minimum value ({})'
  59. .format(self.first, MIN_VALUES[field_name]))
  60. if self.last is not None and self.last > MAX_VALUES[field_name]:
  61. raise ValueError('the last value ({}) is higher than the maximum value ({})'
  62. .format(self.last, MAX_VALUES[field_name]))
  63. value_range = (self.last or MAX_VALUES[field_name]) - self.first
  64. if self.step and self.step > value_range:
  65. raise ValueError('the step value ({}) is higher than the total range of the '
  66. 'expression ({})'.format(self.step, value_range))
  67. def get_next_value(self, date, field):
  68. startval = field.get_value(date)
  69. minval = field.get_min(date)
  70. maxval = field.get_max(date)
  71. # Apply range limits
  72. minval = max(minval, self.first)
  73. maxval = min(maxval, self.last) if self.last is not None else maxval
  74. nextval = max(minval, startval)
  75. # Apply the step if defined
  76. if self.step:
  77. distance_to_next = (self.step - (nextval - minval)) % self.step
  78. nextval += distance_to_next
  79. return nextval if nextval <= maxval else None
  80. def __eq__(self, other):
  81. return (isinstance(other, self.__class__) and self.first == other.first and
  82. self.last == other.last)
  83. def __str__(self):
  84. if self.last != self.first and self.last is not None:
  85. range = '%d-%d' % (self.first, self.last)
  86. else:
  87. range = str(self.first)
  88. if self.step:
  89. return '%s/%d' % (range, self.step)
  90. return range
  91. def __repr__(self):
  92. args = [str(self.first)]
  93. if self.last != self.first and self.last is not None or self.step:
  94. args.append(str(self.last))
  95. if self.step:
  96. args.append(str(self.step))
  97. return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
  98. class MonthRangeExpression(RangeExpression):
  99. value_re = re.compile(r'(?P<first>[a-z]+)(?:-(?P<last>[a-z]+))?', re.IGNORECASE)
  100. def __init__(self, first, last=None):
  101. try:
  102. first_num = MONTHS.index(first.lower()) + 1
  103. except ValueError:
  104. raise ValueError('Invalid month name "%s"' % first)
  105. if last:
  106. try:
  107. last_num = MONTHS.index(last.lower()) + 1
  108. except ValueError:
  109. raise ValueError('Invalid month name "%s"' % last)
  110. else:
  111. last_num = None
  112. super(MonthRangeExpression, self).__init__(first_num, last_num)
  113. def __str__(self):
  114. if self.last != self.first and self.last is not None:
  115. return '%s-%s' % (MONTHS[self.first - 1], MONTHS[self.last - 1])
  116. return MONTHS[self.first - 1]
  117. def __repr__(self):
  118. args = ["'%s'" % MONTHS[self.first]]
  119. if self.last != self.first and self.last is not None:
  120. args.append("'%s'" % MONTHS[self.last - 1])
  121. return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
  122. class WeekdayRangeExpression(RangeExpression):
  123. value_re = re.compile(r'(?P<first>[a-z]+)(?:-(?P<last>[a-z]+))?', re.IGNORECASE)
  124. def __init__(self, first, last=None):
  125. try:
  126. first_num = WEEKDAYS.index(first.lower())
  127. except ValueError:
  128. raise ValueError('Invalid weekday name "%s"' % first)
  129. if last:
  130. try:
  131. last_num = WEEKDAYS.index(last.lower())
  132. except ValueError:
  133. raise ValueError('Invalid weekday name "%s"' % last)
  134. else:
  135. last_num = None
  136. super(WeekdayRangeExpression, self).__init__(first_num, last_num)
  137. def __str__(self):
  138. if self.last != self.first and self.last is not None:
  139. return '%s-%s' % (WEEKDAYS[self.first], WEEKDAYS[self.last])
  140. return WEEKDAYS[self.first]
  141. def __repr__(self):
  142. args = ["'%s'" % WEEKDAYS[self.first]]
  143. if self.last != self.first and self.last is not None:
  144. args.append("'%s'" % WEEKDAYS[self.last])
  145. return "%s(%s)" % (self.__class__.__name__, ', '.join(args))
  146. class WeekdayPositionExpression(AllExpression):
  147. options = ['1st', '2nd', '3rd', '4th', '5th', 'last']
  148. value_re = re.compile(r'(?P<option_name>%s) +(?P<weekday_name>(?:\d+|\w+))' %
  149. '|'.join(options), re.IGNORECASE)
  150. def __init__(self, option_name, weekday_name):
  151. super(WeekdayPositionExpression, self).__init__(None)
  152. try:
  153. self.option_num = self.options.index(option_name.lower())
  154. except ValueError:
  155. raise ValueError('Invalid weekday position "%s"' % option_name)
  156. try:
  157. self.weekday = WEEKDAYS.index(weekday_name.lower())
  158. except ValueError:
  159. raise ValueError('Invalid weekday name "%s"' % weekday_name)
  160. def get_next_value(self, date, field):
  161. # Figure out the weekday of the month's first day and the number of days in that month
  162. first_day_wday, last_day = monthrange(date.year, date.month)
  163. # Calculate which day of the month is the first of the target weekdays
  164. first_hit_day = self.weekday - first_day_wday + 1
  165. if first_hit_day <= 0:
  166. first_hit_day += 7
  167. # Calculate what day of the month the target weekday would be
  168. if self.option_num < 5:
  169. target_day = first_hit_day + self.option_num * 7
  170. else:
  171. target_day = first_hit_day + ((last_day - first_hit_day) // 7) * 7
  172. if target_day <= last_day and target_day >= date.day:
  173. return target_day
  174. def __eq__(self, other):
  175. return (super(WeekdayPositionExpression, self).__eq__(other) and
  176. self.option_num == other.option_num and self.weekday == other.weekday)
  177. def __str__(self):
  178. return '%s %s' % (self.options[self.option_num], WEEKDAYS[self.weekday])
  179. def __repr__(self):
  180. return "%s('%s', '%s')" % (self.__class__.__name__, self.options[self.option_num],
  181. WEEKDAYS[self.weekday])
  182. class LastDayOfMonthExpression(AllExpression):
  183. value_re = re.compile(r'last', re.IGNORECASE)
  184. def __init__(self):
  185. super(LastDayOfMonthExpression, self).__init__(None)
  186. def get_next_value(self, date, field):
  187. return monthrange(date.year, date.month)[1]
  188. def __str__(self):
  189. return 'last'
  190. def __repr__(self):
  191. return "%s()" % self.__class__.__name__