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
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__
def __init__(rule: str = "") -> NoneWrap rule; a blank/absent rule is a valid, non-recurring recurrence.
validate
def validate() -> NoneRaise :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
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-open — window_start is included, window_end excluded. An empty rule yields the single dtstart when it falls in the window.