Skip to content

Schedules

schedules

Schedules implement the logic for calculating the scheduled dates for each schedule stored in the database.

The actual schedule database object is the Schedules, and it can be converted by loading the rules appropriately.

Classes:

Pattern

Pattern(
    value: int,
    type: (
        PatternType
        | Literal[
            "SU", "MO", "TU", "WE", "TH", "FR", "SA", "day"
        ]
        | str
    ) = DAY,
)

Bases: BaseModel

Implements a single pattern for a schedule.

The pattern controls individual inclusions on the schedule. For example, if you want to make a schedule that runs every month on a specific day, you could additionally add the a pattern for, for example, the 15th of the month. This translates to:

p = Pattern(15, PatternType.DAY)
# print will display the friendly string for the pattern
print(p)

You may also provide the pattern as in days of the week. For the first Tuesday, you would use:

Pattern(1, PatternType.TUESDAY)
# is also equivalent to
Pattern(1, "TU")

If you want to indicate the last day, you can use -1 for the value:

last_day = Pattern(-1, PatternType.DAY)
# last day of the schedule period
print(last_day)

Parameters:

  • value

    (int) –

    Day of the month or weekday. If set to -1, it will translate to the last day.

  • type

    (PatternType, default: <PatternType.DAY: 'day'> ) –

    Type of pattern. Can be set to a specific weekday (i.e. Monday) or day of the month.

Source code in actual/schedules.py
def __init__(
    self,
    value: int,
    type: PatternType | typing.Literal["SU", "MO", "TU", "WE", "TH", "FR", "SA", "day"] | str = PatternType.DAY,
):
    if isinstance(type, str):
        type = PatternType(type)
    super().__init__(value=value, type=type)

Schedule

Bases: BaseModel

Implements schedules object for calculation.

Schedules are a way to define recurring transactions in your budget.

On the database level, schedules are stored as part of a Rule, which then compares if the date found fits within the schedule by using the is_approx method. If it does fit, and the other conditions match (extra conditions are only available via the custom rule edit), the transaction will then be linked with the schedule id (stored in the database for the transaction).

This object is not a database level object, meaning that it needs to be converted first using create_schedule, that will create the correct rule.

Parameters:

  • start

    (date) –

    The date indicating the start date of the recurrence.

  • interval

    (int, default: 1 ) –

    The interval at which the recurrence happens. Defaults to 1 if omitted.

  • frequency

    (Frequency, default: <Frequency.MONTHLY: 'monthly'> ) –

    How often the schedule repeats.

  • patterns

    (list[Pattern], default: <dynamic> ) –

    Optional patterns to control specific dates for recurrence (e.g., certain weekdays or month days).

  • skip_weekend

    (bool, default: False ) –

    If true, skips weekends when calculating recurrence dates. This option can be further configured with the weekend_solve_mode parameter.

  • weekend_solve_mode

    (WeekendSolveMode, default: <WeekendSolveMode.AFTER: 'after'> ) –

    If a calculated date falls on a weekend and skip_weekend is true, this controls whether the date moves to the before or after weekday.

  • end_mode

    (EndMode, default: <EndMode.NEVER: 'never'> ) –

    Specifies how the recurrence ends: never ends, after a number of occurrences, or on a specific date.

  • end_occurrences

    (int | None, default: 1 ) –

    Used when end_mode is 'after_n_occurrences'. Indicates how many times it should repeat.

  • end_date

    (date | None, default: None ) –

    Used when end_mode is 'on_date'. The date object indicating when the recurrence should end.

Methods:

  • serialize_model

    Converts a schedule to a dict that can be used in a rule.

  • is_approx

    This function checks if the input date could fit in the schedule.

  • rruleset

    Returns the rruleset from the dateutil library. This is used internally to calculate the schedule dates.

serialize_model

serialize_model(handler) -> dict

Converts a schedule to a dict that can be used in a rule.

Source code in actual/schedules.py
@model_serializer(mode="wrap")
def serialize_model(self, handler) -> dict:
    """Converts a schedule to a dict that can be used in a rule."""
    ret = handler(self)
    return ret

is_approx

is_approx(
    date: date, interval: timedelta = timedelta(days=2)
) -> bool

This function checks if the input date could fit in the schedule.

It will use the interval as the maximum threshold before and after the specified date to look for. This defaults on Actual to +-2 days.

Source code in actual/schedules.py
def is_approx(self, date: datetime.date, interval: datetime.timedelta = datetime.timedelta(days=2)) -> bool:
    """
    This function checks if the input date could fit in the schedule.

    It will use the interval as the maximum threshold before and after the specified date to look for.
    This defaults on Actual to +-2 days.
    """
    if date < self.start or (
        self.end_mode == EndMode.ON_DATE and self.end_date is not None and self.end_date < date
    ):
        return False
    before = self.before(date)
    after = self.xafter(date, 1)
    if before and (before - interval <= date <= before + interval):
        return True
    if after and (after[0] - interval <= date <= after[0] + interval):
        return True
    return False

rruleset

rruleset() -> rruleset

Returns the rruleset from the dateutil library. This is used internally to calculate the schedule dates.

For information on how to use this object, check the official documentation.

Source code in actual/schedules.py
def rruleset(self) -> rruleset:
    """
    Returns the `rruleset` from the dateutil library. This is used internally to calculate the schedule dates.

    For information on how to use this object, check the [official documentation](https://dateutil.readthedocs.io).
    """
    freq = self.frequency.as_dateutil()
    dtstart = self.start
    interval = self.interval
    until = self.end_date if self.end_mode == EndMode.ON_DATE else None
    count = self.end_occurrences if self.end_mode == EndMode.AFTER_N_OCCURRENCES else None
    rules: list[rrule] = []
    if self.frequency == Frequency.MONTHLY and self.patterns:
        by_month_day, by_weekday = [], []
        for p in self.patterns:
            if p.type == PatternType.DAY:
                by_month_day.append(p.value)
            else:  # it's a weekday
                by_weekday.append(p.type.as_dateutil()(p.value))
        # for the month or weekday rules, add a different rrule to the ruleset. This is because otherwise the rule
        # would only look for, for example, days that are 15 that are also Fridays, and that is not desired
        if by_month_day:
            rules.append(
                rrule(freq, dtstart=dtstart, interval=interval, until=until, count=count, bymonthday=by_month_day)
            )
        if by_weekday:
            rules.append(
                rrule(freq, dtstart=dtstart, interval=interval, until=until, count=count, byweekday=by_weekday)
            )
    if not rules:
        rules.append(rrule(freq, dtstart=dtstart, interval=interval, until=until, count=count))
    rs = rruleset(cache=True)
    for r in rules:
        rs.rrule(r)
    return rs