prefect-client 3.2.4__py3-none-any.whl → 3.2.5__py3-none-any.whl

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