def do_crondtc(self, author, channel, serv, message): """Cron feature for DTC printing, max 1 minute per loop with no end start : lanch cron for DTC, if is not stop : cancel cron for DTC, if is start next : print time for next cron DTC loop update <expression> : change cron DTC frequency, if <expression> is valid""" items = message.split() if channel in self._cronDTC: if message == 'start': if self._cronDTC[channel].is_alive(): serv.privmsg(channel, 'cronDTC : already start !') self.__prompt('host', '{%alert%}cronDTC : already start !{%default%}') else: serv.privmsg(channel, 'cronDTC : start !') self.__prompt('host', '{%alert%}cronDTC : start !{%default%}') elif message == 'stop': if self._cronDTC[channel].is_alive(): serv.privmsg(channel, 'cronDTC : stopped !') self.__prompt('host', '{%alert%}cronDTC : stopped !{%default%}') else: serv.privmsg(channel, 'cronDTC : already stopped !') self.__prompt('host', '{%alert%}cronDTC : already stopped !{%default%}') elif items[0] == 'update': items.pop(0) if len(items) > 5: serv.privmsg(channel, 'cronDTC : update refuse ! (possible abuse frequency)') self.__prompt('host', '{%alert%}cronDTC : update refuse ! (possible abuse frequency){%default%}') elif croniter.is_valid(' '.join(items[:5])): self._cronDTC[channel].expanded, self._cronDTC[channel].nth_weekday_of_month = croniter.expand(' '.join(items[:5])) else: serv.privmsg(channel, 'cronDTC : update refuse !') self.__prompt('host', '{%alert%}cronDTC : update refuse !{%default%}') elif message == 'next': if self._cronDTC[channel].is_alive(): _next = delai(datetime.fromtimestamp(self._cronDTC[channel].get_current())) serv.privmsg(channel, 'cronDTC : next in {next} !'.format(next = _next)) self.__prompt('host', '{%alert%}cronDTC : next in {next} !{%default%}', next = _next) else: return False else: if not message: self._cronDTC[channel] = cron(target = self.do_dtc, expr_format = '0 * * * *', args = (self._name, channel, serv, '')) elif croniter.is_valid(' '.join(items[:5])): self._cronDTC[channel] = cron(target = self.do_dtc, expr_format = ' '.join(items[:5]), args = (self._name, channel, serv, '')) else: return False self._cronDTC[channel].start() serv.privmsg(channel, 'cronDTC : start !') self.__prompt('host', '{%alert%}cronDTC : start !{%default%}') return True
def schedule_execution_time_iterator( start_timestamp: float, cron_schedule: str, execution_timezone: Optional[str]) -> Iterator[datetime.datetime]: timezone_str = execution_timezone if execution_timezone else "UTC" utc_datetime = pytz.utc.localize( datetime.datetime.utcfromtimestamp(start_timestamp)) start_datetime = utc_datetime.astimezone(pytz.timezone(timezone_str)) date_iter = croniter(cron_schedule, start_datetime) # Go back one iteration so that the next iteration is the first time that is >= start_datetime # and matches the cron schedule next_date = date_iter.get_prev(datetime.datetime) check.invariant(is_valid_cron_string(cron_schedule)) cron_parts, _ = croniter.expand(cron_schedule) is_numeric = [len(part) == 1 and part[0] != "*" for part in cron_parts] is_wildcard = [len(part) == 1 and part[0] == "*" for part in cron_parts] delta_fn = None should_hour_change = False # Special-case common intervals (hourly/daily/weekly/monthly) since croniter iteration can be # much slower than adding a fixed interval if all(is_numeric[0:3]) and all(is_wildcard[3:]): # monthly delta_fn = lambda d, num: d.add(months=num) should_hour_change = False elif all(is_numeric[0:2]) and is_numeric[4] and all( is_wildcard[2:4]): # weekly delta_fn = lambda d, num: d.add(weeks=num) should_hour_change = False elif all(is_numeric[0:2]) and all(is_wildcard[2:]): # daily delta_fn = lambda d, num: d.add(days=num) should_hour_change = False elif is_numeric[0] and all(is_wildcard[1:]): # hourly delta_fn = lambda d, num: d.add(hours=num) should_hour_change = True else: delta_fn = None should_hour_change = False if delta_fn: # Use pendulums for intervals when possible next_date = to_timezone(pendulum.instance(next_date), timezone_str) while True: curr_hour = next_date.hour next_date_cand = delta_fn(next_date, 1) new_hour = next_date_cand.hour if not should_hour_change and new_hour != curr_hour: # If the hour changes during a daily/weekly/monthly schedule, it # indicates that the time shifted due to falling in a time that doesn't # exist due to a DST transition (for example, 2:30AM CST on 3/10/2019). # Instead, execute at the first time that does exist (the start of the hour), # but return to the original hour for all subsequent executions so that the # hour doesn't stay different permanently. check.invariant(new_hour == curr_hour + 1) yield next_date_cand.replace(minute=0) next_date_cand = delta_fn(next_date, 2) check.invariant(next_date_cand.hour == curr_hour) next_date = next_date_cand yield next_date else: # Otherwise fall back to croniter while True: next_date = to_timezone( pendulum.instance(date_iter.get_next(datetime.datetime)), timezone_str) yield next_date
def expand(cls, expr_format, *args, **kwargs): if len(expr_format.split()) == 6: raise CroniterBadCronError( "Expected 'min hour day mon dow'") return croniter.expand(expr_format, *args, **kwargs)
def is_valid_cron_string(cron_string: str) -> bool: if not croniter.is_valid(cron_string): return False expanded, _ = croniter.expand(cron_string) # dagster only recognizes cron strings that resolve to 5 parts (e.g. not seconds resolution) return len(expanded) == 5