recurrence
iCal RRULE-compliant recurrence calculation engine
Recurrence provides pattern-based scheduling for recurring entities. It supports daily, weekly, monthly, and yearly patterns with flexible restrictions (specific days, months). Uses python-dateutil for iCal RRULE compliance. Components inject RecurrencePort to calculate next occurrences, and MaterializationPolicy to control when new instances are created.
Ports
Schemas
No external schemas used
Hooks
- on_recurrence_trigger
- before_materialize
Events
- RecurrenceTriggered v1
- RecurrenceEnded v1
Stories
Calculate daily occurrences from a start date
Weekly recurrence on specific days
Monthly recurrence on a specific day of month
Recurrence that ends after N occurrences
Recurrence that ends on a specific date
Examples
from datetime import datetime, UTC
from psp.platform.recurrence import RecurrenceEngine, RecurrenceSpec, Frequency
engine = RecurrenceEngine()
spec = RecurrenceSpec(freq=Frequency.DAILY)
now = datetime(2024, 6, 15, 12, 0, tzinfo=UTC)
next_date = engine.next_occurrence(spec, after=now, dtstart=now)
print(f"Next: {next_date}") # 2024-06-16 12:00:00+00:00
Use RecurrenceEngine to find the next occurrence.
from psp.platform.recurrence import (
RecurrenceEngine, RecurrenceSpec, Frequency, Weekday
)
engine = RecurrenceEngine()
spec = RecurrenceSpec(
freq=Frequency.WEEKLY,
byday=(Weekday.MO, Weekday.WE, Weekday.FR),
)
start = datetime(2024, 6, 10, 9, 0, tzinfo=UTC) # Monday
dates = engine.occurrences(spec, dtstart=start, count=5)
Configure weekly recurrence on specific days.
spec = RecurrenceSpec(
freq=Frequency.MONTHLY,
bymonthday=(15,),
)
start = datetime(2024, 1, 15, 9, 0, tzinfo=UTC)
dates = engine.occurrences(spec, dtstart=start, count=3)
# Jan 15, Feb 15, Mar 15
Recur on a specific day of each month.
class CompleteTaskUseCase:
def __init__(
self,
repo: TaskRepository,
recurrence: RecurrencePort,
clock: Clock,
):
self._repo = repo
self._recurrence = recurrence
self._clock = clock
def execute(self, task_id: UUID) -> Task:
task = self._repo.get(task_id)
now = self._clock.now()
task.complete(now)
# If task has recurrence, calculate next occurrence
if task.recurrence_state and task.recurrence_state.is_active:
next_date = self._recurrence.next_occurrence(
task.recurrence_state.spec,
after=now,
dtstart=task.recurrence_state.dtstart,
)
if next_date:
next_task = task.clone_for_recurrence(due_at=next_date)
self._repo.save(next_task)
task.recurrence_state.increment(now)
self._repo.save(task)
return task
Trigger recurrence when task is completed.
This component mounts routes under /v1/recurrence.
View OpenAPI specification