Skip to content

angee.scheduling.recurrence

Recurrence: the RFC-5545 rule value object for the scheduling addon.

:class:Recurrence wraps the RRULE string a :class:~angee.scheduling.fields.RecurrenceField stores and answers the two questions a caller has about it: is this rule well-formed? (:meth:Recurrence.validate) and which concrete moments does it produce in a window? (:meth:Recurrence.occurrences). It holds no state beyond the rule and touches no database — recurrence is a value, not a record.

Parsing and expansion are delegated to python-dateutil (rrulestr), the stack's owner of RFC-5545. The timezone/window contract is documented on :meth:Recurrence.occurrences and on SchedulingConfig.

Recurrence

python
class Recurrence()

An RFC-5545 RRULE and its bounded, timezone-aware expansion.

Construct from the stored rule string; an empty string means "no recurrence" (a single event). The object is immutable-by-convention — it only reads the rule it was given.

Deviation kept for v1 — DTSTART must synchronize with the rule. dateutil follows the interpretation in which a BY* part that excludes the dtstart slot drops dtstart from the set: a Tuesday dtstart with FREQ=WEEKLY;BYDAY=MO yields Mondays and never the stored Tuesday. Some readings of RFC-5545 instead force DTSTART to be the first instance. We keep dateutil's native semantics for v1 — recurrence is the python-dateutil lock's shape and we do not post-process the occurrence set — so authors should write a rule whose BY* parts include the start. The behaviour is a chosen default, pinned by a test (DtstartExclusionTests), not an accident.

__init__

python
def __init__(rule: str = "") -> None

Wrap rule; a blank/absent rule is a valid, non-recurring recurrence.

validate

python
def validate() -> None

Raise :class:~django.core.exceptions.ValidationError unless the rule parses.

A blank rule is valid (non-recurring). Otherwise the rule is parsed for its grammar alone, anchored at a fixed date and the result discarded — validity is dtstart-agnostic, so no start date is required to reject a malformed rule.

occurrences

python
def occurrences(dtstart: datetime, window_start: datetime,
                window_end: datetime) -> list[datetime]

Return the occurrences within [window_start, window_end) as aware UTC.

dtstart and the window bounds are timezone-aware datetimes (canonically UTC); the returned occurrences are aware UTC. A naive dtstart or window bound is rejected with :class:~django.core.exceptions.ValidationError rather than silently reinterpreted in the project timezone — "what day is it" has one owner and it will not guess an offset. Expansion runs in the project's TIME_ZONE: the rule is unrolled against the local wall-clock projection of dtstart, so a 09:00 event stays 09:00 local across a DST shift and an all-day event (local-midnight dtstart) steps by calendar date in that timezone. The window is half-openwindow_start is included, window_end excluded. An empty rule yields the single dtstart when it falls in the window.

Released under the AGPL-3.0 License.