croniter.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. from __future__ import absolute_import, division, print_function
  4. import binascii
  5. import calendar
  6. import copy
  7. import datetime
  8. import math
  9. import platform
  10. import random
  11. import re
  12. import struct
  13. import sys
  14. import traceback as _traceback
  15. from time import time
  16. # as pytz is optional in thirdparty libs but we need it for good support under
  17. # python2, just test that it's well installed
  18. import pytz # noqa
  19. from dateutil.relativedelta import relativedelta
  20. from dateutil.tz import tzutc
  21. def is_32bit():
  22. """
  23. Detect if Python is running in 32-bit mode.
  24. Compatible with Python 2.6 and later versions.
  25. Returns True if running on 32-bit Python, False for 64-bit.
  26. """
  27. # Method 1: Check pointer size
  28. bits = struct.calcsize("P") * 8
  29. # Method 2: Check platform architecture string
  30. try:
  31. architecture = platform.architecture()[0]
  32. except RuntimeError:
  33. architecture = None
  34. # Method 3: Check maxsize (sys.maxint in Python 2)
  35. try:
  36. # Python 2
  37. is_small_maxsize = sys.maxint <= 2 ** 32
  38. except AttributeError:
  39. # Python 3
  40. is_small_maxsize = sys.maxsize <= 2 ** 32
  41. # Evaluate all available methods
  42. is_32 = False
  43. if bits == 32:
  44. is_32 = True
  45. elif architecture and "32" in architecture:
  46. is_32 = True
  47. elif is_small_maxsize:
  48. is_32 = True
  49. return is_32
  50. try:
  51. # https://github.com/python/cpython/issues/101069 detection
  52. if is_32bit():
  53. datetime.datetime.fromtimestamp(3999999999)
  54. OVERFLOW32B_MODE = False
  55. except OverflowError:
  56. OVERFLOW32B_MODE = True
  57. try:
  58. from collections import OrderedDict
  59. except ImportError:
  60. OrderedDict = dict # py26 degraded mode, expanders order will not be immutable
  61. try:
  62. # py3 recent
  63. UTC_DT = datetime.timezone.utc
  64. except AttributeError:
  65. UTC_DT = pytz.utc
  66. EPOCH = datetime.datetime.fromtimestamp(0, UTC_DT)
  67. # fmt: off
  68. M_ALPHAS = {
  69. "jan": 1, "feb": 2, "mar": 3, "apr": 4, # noqa: E241
  70. "may": 5, "jun": 6, "jul": 7, "aug": 8, # noqa: E241
  71. "sep": 9, "oct": 10, "nov": 11, "dec": 12,
  72. }
  73. DOW_ALPHAS = {
  74. "sun": 0, "mon": 1, "tue": 2, "wed": 3, "thu": 4, "fri": 5, "sat": 6
  75. }
  76. MINUTE_FIELD = 0
  77. HOUR_FIELD = 1
  78. DAY_FIELD = 2
  79. MONTH_FIELD = 3
  80. DOW_FIELD = 4
  81. SECOND_FIELD = 5
  82. YEAR_FIELD = 6
  83. UNIX_FIELDS = (MINUTE_FIELD, HOUR_FIELD, DAY_FIELD, MONTH_FIELD, DOW_FIELD) # noqa: E222
  84. SECOND_FIELDS = (MINUTE_FIELD, HOUR_FIELD, DAY_FIELD, MONTH_FIELD, DOW_FIELD, SECOND_FIELD) # noqa: E222
  85. YEAR_FIELDS = (MINUTE_FIELD, HOUR_FIELD, DAY_FIELD, MONTH_FIELD, DOW_FIELD, SECOND_FIELD, YEAR_FIELD) # noqa: E222
  86. # fmt: on
  87. step_search_re = re.compile(r"^([^-]+)-([^-/]+)(/(\d+))?$")
  88. only_int_re = re.compile(r"^\d+$")
  89. WEEKDAYS = "|".join(DOW_ALPHAS.keys())
  90. MONTHS = "|".join(M_ALPHAS.keys())
  91. star_or_int_re = re.compile(r"^(\d+|\*)$")
  92. special_dow_re = re.compile(
  93. (r"^(?P<pre>((?P<he>(({WEEKDAYS})(-({WEEKDAYS}))?)").format(WEEKDAYS=WEEKDAYS)
  94. + (r"|(({MONTHS})(-({MONTHS}))?)|\w+)#)|l)(?P<last>\d+)$").format(MONTHS=MONTHS)
  95. )
  96. re_star = re.compile("[*]")
  97. hash_expression_re = re.compile(
  98. r"^(?P<hash_type>h|r)(\((?P<range_begin>\d+)-(?P<range_end>\d+)\))?(\/(?P<divisor>\d+))?$"
  99. )
  100. CRON_FIELDS = {
  101. "unix": UNIX_FIELDS,
  102. "second": SECOND_FIELDS,
  103. "year": YEAR_FIELDS,
  104. len(UNIX_FIELDS): UNIX_FIELDS,
  105. len(SECOND_FIELDS): SECOND_FIELDS,
  106. len(YEAR_FIELDS): YEAR_FIELDS,
  107. }
  108. UNIX_CRON_LEN = len(UNIX_FIELDS)
  109. SECOND_CRON_LEN = len(SECOND_FIELDS)
  110. YEAR_CRON_LEN = len(YEAR_FIELDS)
  111. # retrocompat
  112. VALID_LEN_EXPRESSION = set(a for a in CRON_FIELDS if isinstance(a, int))
  113. TIMESTAMP_TO_DT_CACHE = {}
  114. EXPRESSIONS = {}
  115. MARKER = object()
  116. def timedelta_to_seconds(td):
  117. return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10 ** 6) / 10 ** 6
  118. def datetime_to_timestamp(d):
  119. if d.tzinfo is not None:
  120. d = d.replace(tzinfo=None) - d.utcoffset()
  121. return timedelta_to_seconds(d - datetime.datetime(1970, 1, 1))
  122. class CroniterError(ValueError):
  123. """General top-level Croniter base exception"""
  124. class CroniterBadTypeRangeError(TypeError):
  125. """."""
  126. class CroniterBadCronError(CroniterError):
  127. """Syntax, unknown value, or range error within a cron expression"""
  128. class CroniterUnsupportedSyntaxError(CroniterBadCronError):
  129. """Valid cron syntax, but likely to produce inaccurate results"""
  130. # Extending CroniterBadCronError, which may be contridatory, but this allows
  131. # catching both errors with a single exception. From a user perspective
  132. # these will likely be handled the same way.
  133. class CroniterBadDateError(CroniterError):
  134. """Unable to find next/prev timestamp match"""
  135. class CroniterNotAlphaError(CroniterBadCronError):
  136. """Cron syntax contains an invalid day or month abbreviation"""
  137. class croniter(object):
  138. MONTHS_IN_YEAR = 12
  139. # This helps with expanding `*` fields into `lower-upper` ranges. Each item
  140. # in this tuple maps to the corresponding field index
  141. RANGES = (
  142. (0, 59),
  143. (0, 23),
  144. (1, 31),
  145. (1, 12),
  146. (0, 6),
  147. (0, 59),
  148. (1970, 2099),
  149. )
  150. DAYS = (31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31)
  151. ALPHACONV = (
  152. {}, # 0: min
  153. {}, # 1: hour
  154. {"l": "l"}, # 2: dom
  155. # 3: mon
  156. copy.deepcopy(M_ALPHAS),
  157. # 4: dow
  158. copy.deepcopy(DOW_ALPHAS),
  159. # 5: second
  160. {},
  161. # 6: year
  162. {},
  163. )
  164. LOWMAP = (
  165. {},
  166. {},
  167. {0: 1},
  168. {0: 1},
  169. {7: 0},
  170. {},
  171. {},
  172. )
  173. LEN_MEANS_ALL = (
  174. 60,
  175. 24,
  176. 31,
  177. 12,
  178. 7,
  179. 60,
  180. 130,
  181. )
  182. def __init__(
  183. self,
  184. expr_format,
  185. start_time=None,
  186. ret_type=float,
  187. day_or=True,
  188. max_years_between_matches=None,
  189. is_prev=False,
  190. hash_id=None,
  191. implement_cron_bug=False,
  192. second_at_beginning=None,
  193. expand_from_start_time=False,
  194. ):
  195. self._ret_type = ret_type
  196. self._day_or = day_or
  197. self._implement_cron_bug = implement_cron_bug
  198. self.second_at_beginning = bool(second_at_beginning)
  199. self._expand_from_start_time = expand_from_start_time
  200. if hash_id:
  201. if not isinstance(hash_id, (bytes, str)):
  202. raise TypeError("hash_id must be bytes or UTF-8 string")
  203. if not isinstance(hash_id, bytes):
  204. hash_id = hash_id.encode("UTF-8")
  205. self._max_years_btw_matches_explicitly_set = max_years_between_matches is not None
  206. if not self._max_years_btw_matches_explicitly_set:
  207. max_years_between_matches = 50
  208. self._max_years_between_matches = max(int(max_years_between_matches), 1)
  209. if start_time is None:
  210. start_time = time()
  211. self.tzinfo = None
  212. self.start_time = None
  213. self.dst_start_time = None
  214. self.cur = None
  215. self.set_current(start_time, force=False)
  216. self.expanded, self.nth_weekday_of_month = self.expand(
  217. expr_format,
  218. hash_id=hash_id,
  219. from_timestamp=self.dst_start_time if self._expand_from_start_time else None,
  220. second_at_beginning=second_at_beginning,
  221. )
  222. self.fields = CRON_FIELDS[len(self.expanded)]
  223. self.expressions = EXPRESSIONS[(expr_format, hash_id, second_at_beginning)]
  224. self._is_prev = is_prev
  225. @classmethod
  226. def _alphaconv(cls, index, key, expressions):
  227. try:
  228. return cls.ALPHACONV[index][key]
  229. except KeyError:
  230. raise CroniterNotAlphaError("[{0}] is not acceptable".format(" ".join(expressions)))
  231. def get_next(self, ret_type=None, start_time=None, update_current=True):
  232. if start_time and self._expand_from_start_time:
  233. raise ValueError("start_time is not supported when using expand_from_start_time = True.")
  234. return self._get_next(
  235. ret_type=ret_type,
  236. start_time=start_time,
  237. is_prev=False,
  238. update_current=update_current,
  239. )
  240. def get_prev(self, ret_type=None, start_time=None, update_current=True):
  241. return self._get_next(
  242. ret_type=ret_type,
  243. start_time=start_time,
  244. is_prev=True,
  245. update_current=update_current,
  246. )
  247. def get_current(self, ret_type=None):
  248. ret_type = ret_type or self._ret_type
  249. if issubclass(ret_type, datetime.datetime):
  250. return self.timestamp_to_datetime(self.cur)
  251. return self.cur
  252. def set_current(self, start_time, force=True):
  253. if (force or (self.cur is None)) and start_time is not None:
  254. if isinstance(start_time, datetime.datetime):
  255. self.tzinfo = start_time.tzinfo
  256. start_time = self.datetime_to_timestamp(start_time)
  257. self.start_time = start_time
  258. self.dst_start_time = start_time
  259. self.cur = start_time
  260. return self.cur
  261. @staticmethod
  262. def datetime_to_timestamp(d):
  263. """
  264. Converts a `datetime` object `d` into a UNIX timestamp.
  265. """
  266. return datetime_to_timestamp(d)
  267. _datetime_to_timestamp = datetime_to_timestamp # retrocompat
  268. def timestamp_to_datetime(self, timestamp, tzinfo=MARKER):
  269. """
  270. Converts a UNIX `timestamp` into a `datetime` object.
  271. """
  272. if tzinfo is MARKER: # allow to give tzinfo=None even if self.tzinfo is set
  273. tzinfo = self.tzinfo
  274. k = timestamp
  275. if tzinfo:
  276. k = (timestamp, repr(tzinfo))
  277. try:
  278. return TIMESTAMP_TO_DT_CACHE[k]
  279. except KeyError:
  280. pass
  281. if OVERFLOW32B_MODE:
  282. # degraded mode to workaround Y2038
  283. # see https://github.com/python/cpython/issues/101069
  284. result = EPOCH.replace(tzinfo=None) + datetime.timedelta(seconds=timestamp)
  285. else:
  286. result = datetime.datetime.fromtimestamp(timestamp, tz=tzutc()).replace(tzinfo=None)
  287. if tzinfo:
  288. result = result.replace(tzinfo=UTC_DT).astimezone(tzinfo)
  289. TIMESTAMP_TO_DT_CACHE[(result, repr(result.tzinfo))] = result
  290. return result
  291. _timestamp_to_datetime = timestamp_to_datetime # retrocompat
  292. @staticmethod
  293. def timedelta_to_seconds(td):
  294. """
  295. Converts a 'datetime.timedelta' object `td` into seconds contained in
  296. the duration.
  297. Note: We cannot use `timedelta.total_seconds()` because this is not
  298. supported by Python 2.6.
  299. """
  300. return timedelta_to_seconds(td)
  301. _timedelta_to_seconds = timedelta_to_seconds # retrocompat
  302. def _get_next(
  303. self,
  304. ret_type=None,
  305. start_time=None,
  306. is_prev=None,
  307. update_current=None,
  308. ):
  309. if update_current is None:
  310. update_current = True
  311. self.set_current(start_time, force=True)
  312. if is_prev is None:
  313. is_prev = self._is_prev
  314. self._is_prev = is_prev
  315. expanded = self.expanded[:]
  316. nth_weekday_of_month = self.nth_weekday_of_month.copy()
  317. ret_type = ret_type or self._ret_type
  318. if not issubclass(ret_type, (float, datetime.datetime)):
  319. raise TypeError("Invalid ret_type, only 'float' or 'datetime' is acceptable.")
  320. # exception to support day of month and day of week as defined in cron
  321. dom_dow_exception_processed = False
  322. if (expanded[DAY_FIELD][0] != "*" and expanded[DOW_FIELD][0] != "*") and self._day_or:
  323. # If requested, handle a bug in vixie cron/ISC cron where day_of_month and day_of_week form
  324. # an intersection (AND) instead of a union (OR) if either field is an asterisk or starts with an asterisk
  325. # (https://crontab.guru/cron-bug.html)
  326. if self._implement_cron_bug and (
  327. re_star.match(self.expressions[DAY_FIELD]) or re_star.match(self.expressions[DOW_FIELD])
  328. ):
  329. # To produce a schedule identical to the cron bug, we'll bypass the code that
  330. # makes a union of DOM and DOW, and instead skip to the code that does an intersect instead
  331. pass
  332. else:
  333. bak = expanded[DOW_FIELD]
  334. expanded[DOW_FIELD] = ["*"]
  335. t1 = self._calc(self.cur, expanded, nth_weekday_of_month, is_prev)
  336. expanded[DOW_FIELD] = bak
  337. expanded[DAY_FIELD] = ["*"]
  338. t2 = self._calc(self.cur, expanded, nth_weekday_of_month, is_prev)
  339. if not is_prev:
  340. result = t1 if t1 < t2 else t2
  341. else:
  342. result = t1 if t1 > t2 else t2
  343. dom_dow_exception_processed = True
  344. if not dom_dow_exception_processed:
  345. result = self._calc(self.cur, expanded, nth_weekday_of_month, is_prev)
  346. # DST Handling for cron job spanning across days
  347. dtstarttime = self._timestamp_to_datetime(self.dst_start_time)
  348. dtstarttime_utcoffset = dtstarttime.utcoffset() or datetime.timedelta(0)
  349. dtresult = self.timestamp_to_datetime(result)
  350. lag = lag_hours = 0
  351. # do we trigger DST on next crontab (handle backward changes)
  352. dtresult_utcoffset = dtstarttime_utcoffset
  353. if dtresult and self.tzinfo:
  354. dtresult_utcoffset = dtresult.utcoffset()
  355. lag_hours = self._timedelta_to_seconds(dtresult - dtstarttime) / (60 * 60)
  356. lag = self._timedelta_to_seconds(dtresult_utcoffset - dtstarttime_utcoffset)
  357. hours_before_midnight = 24 - dtstarttime.hour
  358. if dtresult_utcoffset != dtstarttime_utcoffset:
  359. if (lag > 0 and abs(lag_hours) >= hours_before_midnight) or (
  360. lag < 0 and ((3600 * abs(lag_hours) + abs(lag)) >= hours_before_midnight * 3600)
  361. ):
  362. dtresult_adjusted = dtresult - datetime.timedelta(seconds=lag)
  363. result_adjusted = self._datetime_to_timestamp(dtresult_adjusted)
  364. # Do the actual adjust only if the result time actually exists
  365. if self._timestamp_to_datetime(result_adjusted).tzinfo == dtresult_adjusted.tzinfo:
  366. dtresult = dtresult_adjusted
  367. result = result_adjusted
  368. self.dst_start_time = result
  369. if update_current:
  370. self.cur = result
  371. if issubclass(ret_type, datetime.datetime):
  372. result = dtresult
  373. return result
  374. # iterator protocol, to enable direct use of croniter
  375. # objects in a loop, like "for dt in croniter("5 0 * * *'): ..."
  376. # or for combining multiple croniters into single
  377. # dates feed using 'itertools' module
  378. def all_next(self, ret_type=None, start_time=None, update_current=None):
  379. """
  380. Returns a generator yielding consecutive dates.
  381. May be used instead of an implicit call to __iter__ whenever a
  382. non-default `ret_type` needs to be specified.
  383. """
  384. # In a Python 3.7+ world: contextlib.suppress and contextlib.nullcontext could be used instead
  385. try:
  386. while True:
  387. self._is_prev = False
  388. yield self._get_next(
  389. ret_type=ret_type,
  390. start_time=start_time,
  391. update_current=update_current,
  392. )
  393. start_time = None
  394. except CroniterBadDateError:
  395. if self._max_years_btw_matches_explicitly_set:
  396. return
  397. raise
  398. def all_prev(self, ret_type=None, start_time=None, update_current=None):
  399. """
  400. Returns a generator yielding previous dates.
  401. """
  402. try:
  403. while True:
  404. self._is_prev = True
  405. yield self._get_next(
  406. ret_type=ret_type,
  407. start_time=start_time,
  408. update_current=update_current,
  409. )
  410. start_time = None
  411. except CroniterBadDateError:
  412. if self._max_years_btw_matches_explicitly_set:
  413. return
  414. raise
  415. def iter(self, *args, **kwargs):
  416. return self.all_prev if self._is_prev else self.all_next
  417. def __iter__(self):
  418. return self
  419. __next__ = next = _get_next
  420. def _calc(self, now, expanded, nth_weekday_of_month, is_prev):
  421. if is_prev:
  422. now = math.ceil(now)
  423. nearest_diff_method = self._get_prev_nearest_diff
  424. sign = -1
  425. offset = 1 if (len(expanded) > UNIX_CRON_LEN or now % 60 > 0) else 60
  426. else:
  427. now = math.floor(now)
  428. nearest_diff_method = self._get_next_nearest_diff
  429. sign = 1
  430. offset = 1 if (len(expanded) > UNIX_CRON_LEN) else 60
  431. dst = now = self.timestamp_to_datetime(now + sign * offset)
  432. month, year = dst.month, dst.year
  433. current_year = now.year
  434. DAYS = self.DAYS
  435. def proc_year(d):
  436. if len(expanded) == YEAR_CRON_LEN:
  437. try:
  438. expanded[YEAR_FIELD].index("*")
  439. except ValueError:
  440. # use None as range_val to indicate no loop
  441. diff_year = nearest_diff_method(d.year, expanded[YEAR_FIELD], None)
  442. if diff_year is None:
  443. return None, d
  444. if diff_year != 0:
  445. if is_prev:
  446. d += relativedelta(
  447. years=diff_year,
  448. month=12,
  449. day=31,
  450. hour=23,
  451. minute=59,
  452. second=59,
  453. )
  454. else:
  455. d += relativedelta(
  456. years=diff_year,
  457. month=1,
  458. day=1,
  459. hour=0,
  460. minute=0,
  461. second=0,
  462. )
  463. return True, d
  464. return False, d
  465. def proc_month(d):
  466. try:
  467. expanded[MONTH_FIELD].index("*")
  468. except ValueError:
  469. diff_month = nearest_diff_method(d.month, expanded[MONTH_FIELD], self.MONTHS_IN_YEAR)
  470. reset_day = 1
  471. if diff_month is not None and diff_month != 0:
  472. if is_prev:
  473. d += relativedelta(months=diff_month)
  474. reset_day = DAYS[d.month - 1]
  475. if d.month == 2 and self.is_leap(d.year) is True:
  476. reset_day += 1
  477. d += relativedelta(day=reset_day, hour=23, minute=59, second=59)
  478. else:
  479. d += relativedelta(months=diff_month, day=reset_day, hour=0, minute=0, second=0)
  480. return True, d
  481. return False, d
  482. def proc_day_of_month(d):
  483. try:
  484. expanded[DAY_FIELD].index("*")
  485. except ValueError:
  486. days = DAYS[month - 1]
  487. if month == 2 and self.is_leap(year) is True:
  488. days += 1
  489. if "l" in expanded[DAY_FIELD] and days == d.day:
  490. return False, d
  491. if is_prev:
  492. days_in_prev_month = DAYS[(month - 2) % self.MONTHS_IN_YEAR]
  493. diff_day = nearest_diff_method(d.day, expanded[DAY_FIELD], days_in_prev_month)
  494. else:
  495. diff_day = nearest_diff_method(d.day, expanded[DAY_FIELD], days)
  496. if diff_day is not None and diff_day != 0:
  497. if is_prev:
  498. d += relativedelta(days=diff_day, hour=23, minute=59, second=59)
  499. else:
  500. d += relativedelta(days=diff_day, hour=0, minute=0, second=0)
  501. return True, d
  502. return False, d
  503. def proc_day_of_week(d):
  504. try:
  505. expanded[DOW_FIELD].index("*")
  506. except ValueError:
  507. diff_day_of_week = nearest_diff_method(d.isoweekday() % 7, expanded[DOW_FIELD], 7)
  508. if diff_day_of_week is not None and diff_day_of_week != 0:
  509. if is_prev:
  510. d += relativedelta(days=diff_day_of_week, hour=23, minute=59, second=59)
  511. else:
  512. d += relativedelta(days=diff_day_of_week, hour=0, minute=0, second=0)
  513. return True, d
  514. return False, d
  515. def proc_day_of_week_nth(d):
  516. if "*" in nth_weekday_of_month:
  517. s = nth_weekday_of_month["*"]
  518. for i in range(0, 7):
  519. if i in nth_weekday_of_month:
  520. nth_weekday_of_month[i].update(s)
  521. else:
  522. nth_weekday_of_month[i] = s
  523. del nth_weekday_of_month["*"]
  524. candidates = []
  525. for wday, nth in nth_weekday_of_month.items():
  526. c = self._get_nth_weekday_of_month(d.year, d.month, wday)
  527. for n in nth:
  528. if n == "l":
  529. candidate = c[-1]
  530. elif len(c) < n:
  531. continue
  532. else:
  533. candidate = c[n - 1]
  534. if (is_prev and candidate <= d.day) or (not is_prev and d.day <= candidate):
  535. candidates.append(candidate)
  536. if not candidates:
  537. if is_prev:
  538. d += relativedelta(days=-d.day, hour=23, minute=59, second=59)
  539. else:
  540. days = DAYS[month - 1]
  541. if month == 2 and self.is_leap(year) is True:
  542. days += 1
  543. d += relativedelta(days=(days - d.day + 1), hour=0, minute=0, second=0)
  544. return True, d
  545. candidates.sort()
  546. diff_day = (candidates[-1] if is_prev else candidates[0]) - d.day
  547. if diff_day != 0:
  548. if is_prev:
  549. d += relativedelta(days=diff_day, hour=23, minute=59, second=59)
  550. else:
  551. d += relativedelta(days=diff_day, hour=0, minute=0, second=0)
  552. return True, d
  553. return False, d
  554. def proc_hour(d):
  555. try:
  556. expanded[HOUR_FIELD].index("*")
  557. except ValueError:
  558. diff_hour = nearest_diff_method(d.hour, expanded[HOUR_FIELD], 24)
  559. if diff_hour is not None and diff_hour != 0:
  560. if is_prev:
  561. d += relativedelta(hours=diff_hour, minute=59, second=59)
  562. else:
  563. d += relativedelta(hours=diff_hour, minute=0, second=0)
  564. return True, d
  565. return False, d
  566. def proc_minute(d):
  567. try:
  568. expanded[MINUTE_FIELD].index("*")
  569. except ValueError:
  570. diff_min = nearest_diff_method(d.minute, expanded[MINUTE_FIELD], 60)
  571. if diff_min is not None and diff_min != 0:
  572. if is_prev:
  573. d += relativedelta(minutes=diff_min, second=59)
  574. else:
  575. d += relativedelta(minutes=diff_min, second=0)
  576. return True, d
  577. return False, d
  578. def proc_second(d):
  579. if len(expanded) > UNIX_CRON_LEN:
  580. try:
  581. expanded[SECOND_FIELD].index("*")
  582. except ValueError:
  583. diff_sec = nearest_diff_method(d.second, expanded[SECOND_FIELD], 60)
  584. if diff_sec is not None and diff_sec != 0:
  585. d += relativedelta(seconds=diff_sec)
  586. return True, d
  587. else:
  588. d += relativedelta(second=0)
  589. return False, d
  590. procs = [
  591. proc_year,
  592. proc_month,
  593. proc_day_of_month,
  594. (proc_day_of_week_nth if nth_weekday_of_month else proc_day_of_week),
  595. proc_hour,
  596. proc_minute,
  597. proc_second,
  598. ]
  599. while abs(year - current_year) <= self._max_years_between_matches:
  600. next = False
  601. stop = False
  602. for proc in procs:
  603. (changed, dst) = proc(dst)
  604. # `None` can be set mostly for year processing
  605. # so please see proc_year / _get_prev_nearest_diff / _get_next_nearest_diff
  606. if changed is None:
  607. stop = True
  608. break
  609. if changed:
  610. month, year = dst.month, dst.year
  611. next = True
  612. break
  613. if stop:
  614. break
  615. if next:
  616. continue
  617. return self.datetime_to_timestamp(dst.replace(microsecond=0))
  618. if is_prev:
  619. raise CroniterBadDateError("failed to find prev date")
  620. raise CroniterBadDateError("failed to find next date")
  621. @staticmethod
  622. def _get_next_nearest(x, to_check):
  623. small = [item for item in to_check if item < x]
  624. large = [item for item in to_check if item >= x]
  625. large.extend(small)
  626. return large[0]
  627. @staticmethod
  628. def _get_prev_nearest(x, to_check):
  629. small = [item for item in to_check if item <= x]
  630. large = [item for item in to_check if item > x]
  631. small.reverse()
  632. large.reverse()
  633. small.extend(large)
  634. return small[0]
  635. @staticmethod
  636. def _get_next_nearest_diff(x, to_check, range_val):
  637. """
  638. `range_val` is the range of a field.
  639. If no available time, we can move to next loop(like next month).
  640. `range_val` can also be set to `None` to indicate that there is no loop.
  641. ( Currently, should only used for `year` field )
  642. """
  643. for i, d in enumerate(to_check):
  644. if d == "l" and range_val is not None:
  645. # if 'l' then it is the last day of month
  646. # => its value of range_val
  647. d = range_val
  648. if d >= x:
  649. return d - x
  650. # When range_val is None and x not exists in to_check,
  651. # `None` will be returned to suggest no more available time
  652. if range_val is None:
  653. return None
  654. return to_check[0] - x + range_val
  655. @staticmethod
  656. def _get_prev_nearest_diff(x, to_check, range_val):
  657. """
  658. `range_val` is the range of a field.
  659. If no available time, we can move to previous loop(like previous month).
  660. Range_val can also be set to `None` to indicate that there is no loop.
  661. ( Currently should only used for `year` field )
  662. """
  663. candidates = to_check[:]
  664. candidates.reverse()
  665. for d in candidates:
  666. if d != "l" and d <= x:
  667. return d - x
  668. if "l" in candidates:
  669. return -x
  670. # When range_val is None and x not exists in to_check,
  671. # `None` will be returned to suggest no more available time
  672. if range_val is None:
  673. return None
  674. candidate = candidates[0]
  675. for c in candidates:
  676. # fixed: c < range_val
  677. # this code will reject all 31 day of month, 12 month, 59 second,
  678. # 23 hour and so on.
  679. # if candidates has just a element, this will not harmful.
  680. # but candidates have multiple elements, then values equal to
  681. # range_val will rejected.
  682. if c <= range_val:
  683. candidate = c
  684. break
  685. # fix crontab "0 6 30 3 *" condidates only a element, then get_prev error return 2021-03-02 06:00:00
  686. if candidate > range_val:
  687. return -range_val
  688. return candidate - x - range_val
  689. @staticmethod
  690. def _get_nth_weekday_of_month(year, month, day_of_week):
  691. """For a given year/month return a list of days in nth-day-of-month order.
  692. The last weekday of the month is always [-1].
  693. """
  694. w = (day_of_week + 6) % 7
  695. c = calendar.Calendar(w).monthdayscalendar(year, month)
  696. if c[0][0] == 0:
  697. c.pop(0)
  698. return tuple(i[0] for i in c)
  699. @staticmethod
  700. def is_leap(year):
  701. return bool(year % 400 == 0 or (year % 4 == 0 and year % 100 != 0))
  702. @classmethod
  703. def value_alias(cls, val, field_index, len_expressions=UNIX_CRON_LEN):
  704. if isinstance(len_expressions, (list, dict, tuple, set)):
  705. len_expressions = len(len_expressions)
  706. if val in cls.LOWMAP[field_index] and not (
  707. # do not support 0 as a month either for classical 5 fields cron,
  708. # 6fields second repeat form or 7 fields year form
  709. # but still let conversion happen if day field is shifted
  710. (field_index in [DAY_FIELD, MONTH_FIELD] and len_expressions == UNIX_CRON_LEN)
  711. or (field_index in [MONTH_FIELD, DOW_FIELD] and len_expressions == SECOND_CRON_LEN)
  712. or (field_index in [DAY_FIELD, MONTH_FIELD, DOW_FIELD] and len_expressions == YEAR_CRON_LEN)
  713. ):
  714. val = cls.LOWMAP[field_index][val]
  715. return val
  716. @classmethod
  717. def _expand(
  718. cls,
  719. expr_format,
  720. hash_id=None,
  721. second_at_beginning=False,
  722. from_timestamp=None,
  723. ):
  724. # Split the expression in components, and normalize L -> l, MON -> mon,
  725. # etc. Keep expr_format untouched so we can use it in the exception
  726. # messages.
  727. expr_aliases = {
  728. "@midnight": ("0 0 * * *", "h h(0-2) * * * h"),
  729. "@hourly": ("0 * * * *", "h * * * * h"),
  730. "@daily": ("0 0 * * *", "h h * * * h"),
  731. "@weekly": ("0 0 * * 0", "h h * * h h"),
  732. "@monthly": ("0 0 1 * *", "h h h * * h"),
  733. "@yearly": ("0 0 1 1 *", "h h h h * h"),
  734. "@annually": ("0 0 1 1 *", "h h h h * h"),
  735. }
  736. efl = expr_format.lower()
  737. hash_id_expr = 1 if hash_id is not None else 0
  738. try:
  739. efl = expr_aliases[efl][hash_id_expr]
  740. except KeyError:
  741. pass
  742. expressions = efl.split()
  743. if len(expressions) not in VALID_LEN_EXPRESSION:
  744. raise CroniterBadCronError("Exactly 5, 6 or 7 columns has to be specified for iterator expression.")
  745. if len(expressions) > UNIX_CRON_LEN and second_at_beginning:
  746. # move second to it's own(6th) field to process by same logical
  747. expressions.insert(SECOND_FIELD, expressions.pop(0))
  748. expanded = []
  749. nth_weekday_of_month = {}
  750. for field_index, expr in enumerate(expressions):
  751. for expanderid, expander in EXPANDERS.items():
  752. expr = expander(cls).expand(
  753. efl,
  754. field_index,
  755. expr,
  756. hash_id=hash_id,
  757. from_timestamp=from_timestamp,
  758. )
  759. if "?" in expr:
  760. if expr != "?":
  761. raise CroniterBadCronError(
  762. "[{0}] is not acceptable. Question mark can not used with other characters".format(expr_format)
  763. )
  764. if field_index not in [DAY_FIELD, DOW_FIELD]:
  765. raise CroniterBadCronError(
  766. "[{0}] is not acceptable. Question mark can only used in day_of_month or day_of_week".format(
  767. expr_format
  768. )
  769. )
  770. # currently just trade `?` as `*`
  771. expr = "*"
  772. e_list = expr.split(",")
  773. res = []
  774. while len(e_list) > 0:
  775. e = e_list.pop()
  776. nth = None
  777. if field_index == DOW_FIELD:
  778. # Handle special case in the dow expression: 2#3, l3
  779. special_dow_rem = special_dow_re.match(str(e))
  780. if special_dow_rem:
  781. g = special_dow_rem.groupdict()
  782. he, last = g.get("he", ""), g.get("last", "")
  783. if he:
  784. e = he
  785. try:
  786. nth = int(last)
  787. assert 5 >= nth >= 1
  788. except (KeyError, ValueError, AssertionError):
  789. raise CroniterBadCronError(
  790. "[{0}] is not acceptable. Invalid day_of_week value: '{1}'".format(
  791. expr_format, nth
  792. )
  793. )
  794. elif last:
  795. e = last
  796. nth = g["pre"] # 'l'
  797. # Before matching step_search_re, normalize "*" to "{min}-{max}".
  798. # Example: in the minute field, "*/5" normalizes to "0-59/5"
  799. t = re.sub(
  800. r"^\*(\/.+)$",
  801. r"%d-%d\1" % (cls.RANGES[field_index][0], cls.RANGES[field_index][1]),
  802. str(e),
  803. )
  804. m = step_search_re.search(t)
  805. if not m:
  806. # Before matching step_search_re,
  807. # normalize "{start}/{step}" to "{start}-{max}/{step}".
  808. # Example: in the minute field, "10/5" normalizes to "10-59/5"
  809. t = re.sub(
  810. r"^(.+)\/(.+)$",
  811. r"\1-%d/\2" % (cls.RANGES[field_index][1]),
  812. str(e),
  813. )
  814. m = step_search_re.search(t)
  815. if m:
  816. # early abort if low/high are out of bounds
  817. (low, high, step) = m.group(1), m.group(2), m.group(4) or 1
  818. if field_index == DAY_FIELD and high == "l":
  819. high = "31"
  820. if not only_int_re.search(low):
  821. low = "{0}".format(cls._alphaconv(field_index, low, expressions))
  822. if not only_int_re.search(high):
  823. high = "{0}".format(cls._alphaconv(field_index, high, expressions))
  824. # normally, it's already guarded by the RE that should not accept not-int values.
  825. if not only_int_re.search(str(step)):
  826. raise CroniterBadCronError(
  827. "[{0}] step '{2}' in field {1} is not acceptable".format(expr_format, field_index, step)
  828. )
  829. step = int(step)
  830. for band in low, high:
  831. if not only_int_re.search(str(band)):
  832. raise CroniterBadCronError(
  833. "[{0}] bands '{2}-{3}' in field {1} are not acceptable".format(
  834. expr_format, field_index, low, high
  835. )
  836. )
  837. low, high = [cls.value_alias(int(_val), field_index, expressions) for _val in (low, high)]
  838. if max(low, high) > max(cls.RANGES[field_index][0], cls.RANGES[field_index][1]):
  839. raise CroniterBadCronError("{0} is out of bands".format(expr_format))
  840. if from_timestamp:
  841. low = cls._get_low_from_current_date_number(field_index, int(step), int(from_timestamp))
  842. # Handle when the second bound of the range is in backtracking order:
  843. # eg: X-Sun or X-7 (Sat-Sun) in DOW, or X-Jan (Apr-Jan) in MONTH
  844. if low > high:
  845. whole_field_range = list(
  846. range(
  847. cls.RANGES[field_index][0],
  848. cls.RANGES[field_index][1] + 1,
  849. 1,
  850. )
  851. )
  852. # Add FirstBound -> ENDRANGE, respecting step
  853. rng = list(range(low, cls.RANGES[field_index][1] + 1, step))
  854. # Then 0 -> SecondBound, but skipping n first occurences according to step
  855. # EG to respect such expressions : Apr-Jan/3
  856. to_skip = 0
  857. if rng:
  858. already_skipped = list(reversed(whole_field_range)).index(rng[-1])
  859. curpos = whole_field_range.index(rng[-1])
  860. if ((curpos + step) > len(whole_field_range)) and (already_skipped < step):
  861. to_skip = step - already_skipped
  862. rng += list(range(cls.RANGES[field_index][0] + to_skip, high + 1, step))
  863. # if we include a range type: Jan-Jan, or Sun-Sun,
  864. # it means the whole cycle (all days of week, # all monthes of year, etc)
  865. elif low == high:
  866. rng = list(
  867. range(
  868. cls.RANGES[field_index][0],
  869. cls.RANGES[field_index][1] + 1,
  870. step,
  871. )
  872. )
  873. else:
  874. try:
  875. rng = list(range(low, high + 1, step))
  876. except ValueError as exc:
  877. raise CroniterBadCronError("invalid range: {0}".format(exc))
  878. rng = (
  879. ["{0}#{1}".format(item, nth) for item in rng]
  880. if field_index == DOW_FIELD and nth and nth != "l"
  881. else rng
  882. )
  883. e_list += [a for a in rng if a not in e_list]
  884. else:
  885. if t.startswith("-"):
  886. raise CroniterBadCronError(
  887. "[{0}] is not acceptable," "negative numbers not allowed".format(expr_format)
  888. )
  889. if not star_or_int_re.search(t):
  890. t = cls._alphaconv(field_index, t, expressions)
  891. try:
  892. t = int(t)
  893. except ValueError:
  894. pass
  895. t = cls.value_alias(t, field_index, expressions)
  896. if t not in ["*", "l"] and (
  897. int(t) < cls.RANGES[field_index][0] or int(t) > cls.RANGES[field_index][1]
  898. ):
  899. raise CroniterBadCronError("[{0}] is not acceptable, out of range".format(expr_format))
  900. res.append(t)
  901. if field_index == DOW_FIELD and nth:
  902. if t not in nth_weekday_of_month:
  903. nth_weekday_of_month[t] = set()
  904. nth_weekday_of_month[t].add(nth)
  905. res = set(res)
  906. res = sorted(res, key=lambda i: "{:02}".format(i) if isinstance(i, int) else i)
  907. if len(res) == cls.LEN_MEANS_ALL[field_index]:
  908. # Make sure the wildcard is used in the correct way (avoid over-optimization)
  909. if (field_index == DAY_FIELD and "*" not in expressions[DOW_FIELD]) or (
  910. field_index == DOW_FIELD and "*" not in expressions[DAY_FIELD]
  911. ):
  912. pass
  913. else:
  914. res = ["*"]
  915. expanded.append(["*"] if (len(res) == 1 and res[0] == "*") else res)
  916. # Check to make sure the dow combo in use is supported
  917. if nth_weekday_of_month:
  918. dow_expanded_set = set(expanded[DOW_FIELD])
  919. dow_expanded_set = dow_expanded_set.difference(nth_weekday_of_month.keys())
  920. dow_expanded_set.discard("*")
  921. # Skip: if it's all weeks instead of wildcard
  922. if dow_expanded_set and len(set(expanded[DOW_FIELD])) != cls.LEN_MEANS_ALL[DOW_FIELD]:
  923. raise CroniterUnsupportedSyntaxError(
  924. "day-of-week field does not support mixing literal values and nth day of week syntax. "
  925. "Cron: '{}' dow={} vs nth={}".format(expr_format, dow_expanded_set, nth_weekday_of_month)
  926. )
  927. EXPRESSIONS[(expr_format, hash_id, second_at_beginning)] = expressions
  928. return expanded, nth_weekday_of_month
  929. @classmethod
  930. def expand(
  931. cls,
  932. expr_format,
  933. hash_id=None,
  934. second_at_beginning=False,
  935. from_timestamp=None,
  936. ):
  937. """
  938. Expand a cron expression format into a noramlized format of
  939. list[list[int | 'l' | '*']]. The first list representing each element
  940. of the epxression, and each sub-list representing the allowed values
  941. for that expression component.
  942. A tuple is returned, the first value being the expanded epxression
  943. list, and the second being a `nth_weekday_of_month` mapping.
  944. Examples:
  945. # Every minute
  946. >>> croniter.expand('* * * * *')
  947. ([['*'], ['*'], ['*'], ['*'], ['*']], {})
  948. # On the hour
  949. >>> croniter.expand('0 0 * * *')
  950. ([[0], [0], ['*'], ['*'], ['*']], {})
  951. # Hours 0-5 and 10 monday through friday
  952. >>> croniter.expand('0-5,10 * * * mon-fri')
  953. ([[0, 1, 2, 3, 4, 5, 10], ['*'], ['*'], ['*'], [1, 2, 3, 4, 5]], {})
  954. Note that some special values such as nth day of week are expanded to a
  955. special mapping format for later processing:
  956. # Every minute on the 3rd tuesday of the month
  957. >>> croniter.expand('* * * * 2#3')
  958. ([['*'], ['*'], ['*'], ['*'], [2]], {2: {3}})
  959. # Every hour on the last day of the month
  960. >>> croniter.expand('0 * l * *')
  961. ([[0], ['*'], ['l'], ['*'], ['*']], {})
  962. # On the hour every 15 seconds
  963. >>> croniter.expand('0 0 * * * */15')
  964. ([[0], [0], ['*'], ['*'], ['*'], [0, 15, 30, 45]], {})
  965. """
  966. try:
  967. return cls._expand(
  968. expr_format,
  969. hash_id=hash_id,
  970. second_at_beginning=second_at_beginning,
  971. from_timestamp=from_timestamp,
  972. )
  973. except (ValueError,) as exc:
  974. if isinstance(exc, CroniterError):
  975. raise
  976. if int(sys.version[0]) >= 3:
  977. trace = _traceback.format_exc()
  978. raise CroniterBadCronError(trace)
  979. raise CroniterBadCronError("{0}".format(exc))
  980. @classmethod
  981. def _get_low_from_current_date_number(cls, field_index, step, from_timestamp):
  982. dt = datetime.datetime.fromtimestamp(from_timestamp, tz=UTC_DT)
  983. if field_index == MINUTE_FIELD:
  984. return dt.minute % step
  985. if field_index == HOUR_FIELD:
  986. return dt.hour % step
  987. if field_index == DAY_FIELD:
  988. return ((dt.day - 1) % step) + 1
  989. if field_index == MONTH_FIELD:
  990. return dt.month % step
  991. if field_index == DOW_FIELD:
  992. return (dt.weekday() + 1) % step
  993. raise ValueError("Can't get current date number for index larger than 4")
  994. @classmethod
  995. def is_valid(
  996. cls,
  997. expression,
  998. hash_id=None,
  999. encoding="UTF-8",
  1000. second_at_beginning=False,
  1001. ):
  1002. if hash_id:
  1003. if not isinstance(hash_id, (bytes, str)):
  1004. raise TypeError("hash_id must be bytes or UTF-8 string")
  1005. if not isinstance(hash_id, bytes):
  1006. hash_id = hash_id.encode(encoding)
  1007. try:
  1008. cls.expand(expression, hash_id=hash_id, second_at_beginning=second_at_beginning)
  1009. except CroniterError:
  1010. return False
  1011. return True
  1012. @classmethod
  1013. def match(cls, cron_expression, testdate, day_or=True, second_at_beginning=False):
  1014. return cls.match_range(cron_expression, testdate, testdate, day_or, second_at_beginning)
  1015. @classmethod
  1016. def match_range(
  1017. cls,
  1018. cron_expression,
  1019. from_datetime,
  1020. to_datetime,
  1021. day_or=True,
  1022. second_at_beginning=False,
  1023. ):
  1024. cron = cls(
  1025. cron_expression,
  1026. to_datetime,
  1027. ret_type=datetime.datetime,
  1028. day_or=day_or,
  1029. second_at_beginning=second_at_beginning,
  1030. )
  1031. tdp = cron.get_current(datetime.datetime)
  1032. if not tdp.microsecond:
  1033. tdp += relativedelta(microseconds=1)
  1034. cron.set_current(tdp, force=True)
  1035. try:
  1036. tdt = cron.get_prev()
  1037. except CroniterBadDateError:
  1038. return False
  1039. precision_in_seconds = 1 if len(cron.expanded) > UNIX_CRON_LEN else 60
  1040. duration_in_second = (to_datetime - from_datetime).total_seconds() + precision_in_seconds
  1041. return (max(tdp, tdt) - min(tdp, tdt)).total_seconds() < duration_in_second
  1042. def croniter_range(
  1043. start,
  1044. stop,
  1045. expr_format,
  1046. ret_type=None,
  1047. day_or=True,
  1048. exclude_ends=False,
  1049. _croniter=None,
  1050. second_at_beginning=False,
  1051. expand_from_start_time=False,
  1052. ):
  1053. """
  1054. Generator that provides all times from start to stop matching the given cron expression.
  1055. If the cron expression matches either 'start' and/or 'stop', those times will be returned as
  1056. well unless 'exclude_ends=True' is passed.
  1057. You can think of this function as sibling to the builtin range function for datetime objects.
  1058. Like range(start,stop,step), except that here 'step' is a cron expression.
  1059. """
  1060. _croniter = _croniter or croniter
  1061. auto_rt = datetime.datetime
  1062. # type is used in first if branch for perfs reasons
  1063. if type(start) is not type(stop) and not (isinstance(start, type(stop)) or isinstance(stop, type(start))):
  1064. raise CroniterBadTypeRangeError(
  1065. "The start and stop must be same type. {0} != {1}".format(type(start), type(stop))
  1066. )
  1067. if isinstance(start, (float, int)):
  1068. start, stop = (datetime.datetime.fromtimestamp(t, tzutc()).replace(tzinfo=None) for t in (start, stop))
  1069. auto_rt = float
  1070. if ret_type is None:
  1071. ret_type = auto_rt
  1072. if not exclude_ends:
  1073. ms1 = relativedelta(microseconds=1)
  1074. if start < stop: # Forward (normal) time order
  1075. start -= ms1
  1076. stop += ms1
  1077. else: # Reverse time order
  1078. start += ms1
  1079. stop -= ms1
  1080. year_span = math.floor(abs(stop.year - start.year)) + 1
  1081. ic = _croniter(
  1082. expr_format,
  1083. start,
  1084. ret_type=datetime.datetime,
  1085. day_or=day_or,
  1086. max_years_between_matches=year_span,
  1087. second_at_beginning=second_at_beginning,
  1088. expand_from_start_time=expand_from_start_time,
  1089. )
  1090. # define a continue (cont) condition function and step function for the main while loop
  1091. if start < stop: # Forward
  1092. def cont(v):
  1093. return v < stop
  1094. step = ic.get_next
  1095. else: # Reverse
  1096. def cont(v):
  1097. return v > stop
  1098. step = ic.get_prev
  1099. try:
  1100. dt = step()
  1101. while cont(dt):
  1102. if ret_type is float:
  1103. yield ic.get_current(float)
  1104. else:
  1105. yield dt
  1106. dt = step()
  1107. except CroniterBadDateError:
  1108. # Stop iteration when this exception is raised; no match found within the given year range
  1109. return
  1110. class HashExpander:
  1111. def __init__(self, cronit):
  1112. self.cron = cronit
  1113. def do(self, idx, hash_type="h", hash_id=None, range_end=None, range_begin=None):
  1114. """Return a hashed/random integer given range/hash information"""
  1115. if range_end is None:
  1116. range_end = self.cron.RANGES[idx][1]
  1117. if range_begin is None:
  1118. range_begin = self.cron.RANGES[idx][0]
  1119. if hash_type == "r":
  1120. crc = random.randint(0, 0xFFFFFFFF)
  1121. else:
  1122. crc = binascii.crc32(hash_id) & 0xFFFFFFFF
  1123. return ((crc >> idx) % (range_end - range_begin + 1)) + range_begin
  1124. def match(self, efl, idx, expr, hash_id=None, **kw):
  1125. return hash_expression_re.match(expr)
  1126. def expand(self, efl, idx, expr, hash_id=None, match="", **kw):
  1127. """Expand a hashed/random expression to its normal representation"""
  1128. if match == "":
  1129. match = self.match(efl, idx, expr, hash_id, **kw)
  1130. if not match:
  1131. return expr
  1132. m = match.groupdict()
  1133. if m["hash_type"] == "h" and hash_id is None:
  1134. raise CroniterBadCronError("Hashed definitions must include hash_id")
  1135. if m["range_begin"] and m["range_end"]:
  1136. if int(m["range_begin"]) >= int(m["range_end"]):
  1137. raise CroniterBadCronError("Range end must be greater than range begin")
  1138. if m["range_begin"] and m["range_end"] and m["divisor"]:
  1139. # Example: H(30-59)/10 -> 34-59/10 (i.e. 34,44,54)
  1140. if int(m["divisor"]) == 0:
  1141. raise CroniterBadCronError("Bad expression: {0}".format(expr))
  1142. return "{0}-{1}/{2}".format(
  1143. self.do(
  1144. idx,
  1145. hash_type=m["hash_type"],
  1146. hash_id=hash_id,
  1147. range_begin=int(m["range_begin"]),
  1148. range_end=int(m["divisor"]) - 1 + int(m["range_begin"]),
  1149. ),
  1150. int(m["range_end"]),
  1151. int(m["divisor"]),
  1152. )
  1153. elif m["range_begin"] and m["range_end"]:
  1154. # Example: H(0-29) -> 12
  1155. return str(
  1156. self.do(
  1157. idx,
  1158. hash_type=m["hash_type"],
  1159. hash_id=hash_id,
  1160. range_end=int(m["range_end"]),
  1161. range_begin=int(m["range_begin"]),
  1162. )
  1163. )
  1164. elif m["divisor"]:
  1165. # Example: H/15 -> 7-59/15 (i.e. 7,22,37,52)
  1166. if int(m["divisor"]) == 0:
  1167. raise CroniterBadCronError("Bad expression: {0}".format(expr))
  1168. return "{0}-{1}/{2}".format(
  1169. self.do(
  1170. idx,
  1171. hash_type=m["hash_type"],
  1172. hash_id=hash_id,
  1173. range_begin=self.cron.RANGES[idx][0],
  1174. range_end=int(m["divisor"]) - 1 + self.cron.RANGES[idx][0],
  1175. ),
  1176. self.cron.RANGES[idx][1],
  1177. int(m["divisor"]),
  1178. )
  1179. else:
  1180. # Example: H -> 32
  1181. return str(
  1182. self.do(
  1183. idx,
  1184. hash_type=m["hash_type"],
  1185. hash_id=hash_id,
  1186. )
  1187. )
  1188. EXPANDERS = OrderedDict(
  1189. [
  1190. ("hash", HashExpander),
  1191. ]
  1192. )