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

Code before:
from awx.main.fields import OrderedManyToManyField
from awx.main.models.base import PrimordialModel
from awx.main.models.jobs import LaunchTimeConfig
from awx.main.utils import ignore_inventory_computed_fields
from awx.main.consumers import emit_channel_notification

import pytz


logger = logging.getLogger('awx.main.models.schedule')

__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.

Review comment (suggestion):
This seems to be semantically closer to a `ValueError` than to a `RuntimeError` as it is function input validation, not a runtime check. Perhaps, it's worth changing?
```suggestion
        raise ValueError(f"Cannot fast forward rrule, frequency must be HOURLY or MINUTELY, but got {rrule._freq !r}")
```

Code after:
from awx.main.fields import OrderedManyToManyField
from awx.main.models.base import PrimordialModel
from awx.main.models.jobs import LaunchTimeConfig
from awx.main.utils import ignore_inventory_computed_fields
from awx.main.consumers import emit_channel_notification

import pytz


logger = logging.getLogger('awx.main.models.schedule')

__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