Repository: ansible/awx
File: awx/main/models/schedules.py (Python)

Code before:
__all__ = ['Schedule']


UTC_TIMEZONES = {x: tzutc() for x in dateutil.parser.parserinfo().UTCZONE}


SECONDS_IN_WEEK = 7 * 24 * 60 * 60


def fast_forward_date(rrule):
    '''
    Utility to fast forward an rrule, maintaining consistency in the resulting
    occurrences
    Fast forwards the rrule to 7 days ago
    Returns a datetime object
    '''
    if not rrule._freq in (dateutil.rrule.HOURLY, dateutil.rrule.MINUTELY):
        raise RuntimeError("Cannot fast forward rrule, frequency must be HOURLY or MINUTELY")

    interval = rrule._interval if rrule._interval else 1
    if rrule._freq == dateutil.rrule.HOURLY:
        interval *= 60 * 60
    elif rrule._freq == dateutil.rrule.MINUTELY:
        interval *= 60

    if type(interval) == float and not interval.is_integer():
        raise RuntimeError("Cannot fast forward rule, interval is a fraction of a second")

    seconds_since_dtstart = (now() - rrule._dtstart).total_seconds()

    # fast forward to 7 days ago
    fast_forward_seconds = seconds_since_dtstart - SECONDS_IN_WEEK

    # make sure at least one chunk of interval can fit inside the time period we are trying to fast forward
    if interval > fast_forward_seconds:
        raise RuntimeError(f"Cannot fast forward rrule {rrule}, interval is greater than the fast forward amount of {fast_forward_seconds} seconds")

    # it is important to fast forward by a number that is divisible by
    # interval. For example, if interval is 7 hours, we fast forward by 7, 14, 21, etc. hours.
    # Otherwise, the occurrences after the fast forward might not match the ones before.
    # x // y is integer division, lopping off any remainder, so that we get the outcome we want.
    new_start = rrule._dtstart + datetime.timedelta(seconds=(fast_forward_seconds // interval) * interval)
    return new_start


class ScheduleFilterMethods(object):
    def enabled(self, enabled=True):
        return self.filter(enabled=enabled)

    def before(self, dt):
        return self.filter(next_run__lt=dt)

Review comment (performance):
Is it intentional that `isinstance(interval, float)` is not used here? Do we want to disallow subclasses?
Also, wouldn't it be more correct to do an identity check, as in `type(interval) is float`?

Looking at what's checked, I think that it should be possible to use `rrule._interval` instead and move it to the beginning of the function, having a chance to bail before the computations. This is probably not a huge performance benefit, but it would improve the structure by grouping all the “guard expression”-style checks together.

Additionally, once these checks are close, they could be moved into a helper called smth like `_assert_rrule_is_fast-forwardable()`, which might improve the structure even further.

Code after:
__all__ = ['Schedule']


UTC_TIMEZONES = {x: tzutc() for x in dateutil.parser.parserinfo().UTCZONE}


def _assert_timezone_id_is_valid(rrules) -> None:
    broken_rrules = [str(rrule) for rrule in rrules if rrule._dtstart and rrule._dtstart.tzinfo is None]
    if not broken_rrules:
        return
    raise ValueError(
        f'A valid TZID must be provided (e.g., America/New_York). Invalid: {broken_rrules}',
    ) from None


def _fast_forward_rrules(rrules, ref_dt=None):
    for i, rule in enumerate(rrules):
        rrules[i] = _fast_forward_rrule(rule, ref_dt=ref_dt)
    return rrules


def _fast_forward_rrule(rrule, ref_dt=None):
    '''
    Utility to fast forward an rrule, maintaining consistency in the resulting
    occurrences.

    Uses the .replace() method to update the rrule with a newer dtstart
    The operation ensures that the original occurrences (based on the original dtstart)
    will match the occurrences after changing the dtstart.

    Returns a new rrule with a new dtstart
    '''
    if rrule._freq not in {dateutil.rrule.HOURLY, dateutil.rrule.MINUTELY}:
        return rrule

    if ref_dt is None:
        ref_dt = now()

    if rrule._dtstart > ref_dt:
        return rrule

    interval = rrule._interval if rrule._interval else 1
    if rrule._freq == dateutil.rrule.HOURLY:
        interval *= 60 * 60
    elif rrule._freq == dateutil.rrule.MINUTELY:
        interval *= 60

    # if after converting to seconds the interval is still a fraction,
    # just return original rrule
    if isinstance(interval, float) and not interval.is_integer():
        return rrule