def calculate_recurrence()

in adjust_schedule_function/lib/recurrence.py [0:0]


    def calculate_recurrence(self, current_recurrence, expected_time, timezone, start_time=None):
        """Calculates the correct recurrence expression for the given
        recurrence, expected local time and local timezone.

        The recurrence must be defined as a cron expression.

        If the recurrence is not selective on the hour, or if the start date
        will occur in more than a day in the future, this method will return
        the current recurrence.

        Args:
            current_recurrence: A string with the current recurrence, as a cron
                expression (e..g, '0 0 * * *')
            expected_time: A string with the local time at which the action is
                expected to run.
            timezone: The name of the timezone (e.g., 'Europe/Madrid') of the
                local time.
            start_date: A datetime object with the start date and time of the
                event. If not provided, None is assumed.

        Returns:
            A string with the appropriate recurrence, formatted as a cron
            expression.

        Raises:
            NotImplementedError: The original recurrence contains anything
            other than single hours or minutes (e.g., ranges). These are
            currently not supported by this implementation."""

        parsed_recurrence = parse_cron_expression(current_recurrence)

        # For the time being, we don't handle cron expressions which specify
        # anything but a specific hour and minute (i.e., ranges, multiple
        # hours, etc.). This is a feature that will be implemented in the
        # future.
        if not re.match(r'^\d+$', parsed_recurrence['hour']):
            raise NotImplementedError("This script cannot yet handle multiple hours in cron expressions: '{}'".format(current_recurrence))
        if not re.match(r'^\d+$', parsed_recurrence['minute']):
            raise NotImplementedError("This script cannot yet handle multiple minutes in cron expressions: '{}'".format(current_recurrence))

        # If the cron expression is not selective on the hour, it does not make
        # sense to keep going.
        if parsed_recurrence['hour'] == '*':
            print("Recurrence's cron expression ('{}') is not selective on the hour. Leaving recurrence as is.".format(current_recurrence))
            return current_recurrence

        utc_now = self._time_source.get_current_utc_datetime()

        # If the start date is over a day in the future, skip it. (We need to
        # reevaluate whether this logic belongs to this class.)
        if start_time and (start_time - utc_now).days > 1:
            print("Start date is over a day away. Leaving recurrence as is.")
            return current_recurrence

        # Determine when the event will run next, and compare the time with the
        # expected local time at the specified timezone. If they match, then
        # we're all good. If they don't, we need to update the recurrence.
        #
        # Note that we're adding one extra second to the time delta, to account
        # for precision errors which might produce incorrect results. (See for
        # example when the expected time is 14:00:00 and the delta causes us to
        # see 13:59:59.998). This is pretty hacky and I should revisit this,
        # for sure.

        recurrence = CronTab(current_recurrence)
        delta = timedelta(seconds = recurrence.next(default_utc=True) + 1)
        utc_next_run = utc_now + delta
        local_next_run = utc_next_run.astimezone(pytz.timezone(timezone))
        local_next_run_time = local_next_run.strftime('%H:%M')

        print("This event should run at '{}' local time. The next run will occur at '{}', which is '{}' at specified local timezone '{}'.".format(expected_time, utc_next_run.isoformat(), local_next_run_time, timezone))

        if local_next_run_time == expected_time:
            print("Times match. Current recurrence is correct.")
            return current_recurrence

        print("Times don't match. Current recurrence must be recalculated.")
        local_expected_run = local_next_run.replace(hour=parser.parse(expected_time).hour,
                                                    minute=parser.parse(expected_time).minute)
        utc_expected_run = local_expected_run.astimezone(pytz.timezone('UTC'))

        # We should only change the hour and minute parts of the cron
        # expression. The rest should be left as it was originally. The reason
        # we're changing the minutes too is because some timezones don't have
        # whole offsets. E.g., see "Indian Standard Time".
        new_recurrence = '{} {} {}'.format(utc_expected_run.minute,
                                           utc_expected_run.hour,
                                           parsed_recurrence['rest'])

        return new_recurrence