1c8a86594f
Dos funciones nuevas del grupo de capacidad `dav`: - expand_rrule_py_infra (pure): expande una RRULE iCalendar a las fechas de cada ocurrencia dentro de un rango [from, to]. Solo stdlib (datetime, re). Soporta FREQ DAILY/WEEKLY/MONTHLY/YEARLY, INTERVAL, COUNT, UNTIL, BYDAY. 9 tests. - dav_make_calendar_py_infra (impure): crea una coleccion de calendario nueva via MKCALENDAR + PROPPATCH de nombre/color. Idempotente si ya existe. 11 tests. Consumidas por la app osint_web (eventos recurrentes + creacion de agendas). Pagina del grupo dav actualizada con ambas. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
90 lines
4.3 KiB
Markdown
90 lines
4.3 KiB
Markdown
---
|
|
name: expand_rrule
|
|
kind: function
|
|
lang: py
|
|
domain: infra
|
|
version: "1.0.0"
|
|
purity: pure
|
|
signature: "def expand_rrule(dtstart_ical: str, rrule: str, range_start: str, range_end: str, all_day: bool = False) -> list[str]"
|
|
description: "Expande una RRULE iCalendar a la lista ordenada de fechas DTSTART de cada ocurrencia que cae dentro de un rango [range_start, range_end]. Pura, determinista, solo stdlib (sin python-dateutil). Soporta FREQ DAILY/WEEKLY/MONTHLY/YEARLY, INTERVAL, COUNT, UNTIL y BYDAY (para WEEKLY)."
|
|
tags: [dav, calendar, ical, rrule, recurrence, caldav]
|
|
uses_functions: []
|
|
uses_types: []
|
|
returns: []
|
|
returns_optional: false
|
|
error_type: ""
|
|
imports: []
|
|
tested: true
|
|
tests: ["test_golden_weekly_count_4", "test_edge_monthly_interval_2", "test_edge_weekly_byday_two_days", "test_edge_all_day_vs_with_time", "test_until_recorta", "test_filtro_por_rango_excluye_fuera", "test_dtstart_anterior_al_rango_pero_serie_entra", "test_componente_no_soportado_se_ignora", "test_sin_count_ni_until_acota_a_range_end"]
|
|
test_file_path: "python/functions/infra/expand_rrule_test.py"
|
|
file_path: "python/functions/infra/expand_rrule.py"
|
|
params:
|
|
- name: dtstart_ical
|
|
desc: "Fecha de inicio del evento maestro en formato iCal crudo: YYYYMMDD (all-day), YYYYMMDDTHHMMSS o YYYYMMDDTHHMMSSZ. Es la primera ocurrencia (la serie la incluye si cae en rango)."
|
|
- name: rrule
|
|
desc: "Cuerpo de la RRULE SIN el prefijo 'RRULE:', p.ej. 'FREQ=WEEKLY;INTERVAL=1;COUNT=10' o 'FREQ=MONTHLY;UNTIL=20261231;BYDAY=MO,WE'."
|
|
- name: range_start
|
|
desc: "Limite inferior del rango como YYYYMMDD (inclusive). Solo se devuelven ocurrencias cuya fecha YYYYMMDD del DTSTART cae en [range_start, range_end]."
|
|
- name: range_end
|
|
desc: "Limite superior del rango como YYYYMMDD (inclusive). Tambien acota la generacion cuando faltan COUNT y UNTIL en la RRULE."
|
|
- name: all_day
|
|
desc: "Si True las ocurrencias se devuelven como YYYYMMDD; si False conservan la parte de hora del dtstart original (misma hora local en cada ocurrencia) y devuelven YYYYMMDDTHHMMSS sin sufijo Z. Default False."
|
|
output: "Lista ordenada de strings DTSTART iCal, una por ocurrencia en rango. Lista vacia si la RRULE no produce ninguna en [range_start, range_end]."
|
|
---
|
|
|
|
## Ejemplo
|
|
|
|
```python
|
|
import sys, os
|
|
sys.path.insert(0, os.path.join("python", "functions"))
|
|
from infra.expand_rrule import expand_rrule
|
|
|
|
# Reunion semanal los lunes a las 09:00, 4 ocurrencias, ventana de enero 2026.
|
|
fechas = expand_rrule(
|
|
"20260105T090000",
|
|
"FREQ=WEEKLY;COUNT=4",
|
|
"20260101",
|
|
"20261231",
|
|
)
|
|
print(fechas)
|
|
# ['20260105T090000', '20260112T090000', '20260119T090000', '20260126T090000']
|
|
|
|
# Evento all-day mensual cada 2 meses, solo las que caen en el primer semestre.
|
|
fechas = expand_rrule(
|
|
"20260115",
|
|
"FREQ=MONTHLY;INTERVAL=2;COUNT=4",
|
|
"20260101",
|
|
"20260630",
|
|
all_day=True,
|
|
)
|
|
print(fechas)
|
|
# ['20260115', '20260315', '20260515']
|
|
```
|
|
|
|
## Cuando usarla
|
|
|
|
Cuando un cliente CalDAV necesita mostrar las ocurrencias de un evento
|
|
recurrente dentro de la ventana visible del calendario: tienes el DTSTART y la
|
|
RRULE del VEVENT maestro y quieres la lista concreta de fechas de inicio que
|
|
caen entre dos limites para pintarlas en la agenda. Tambien para contar o
|
|
iterar instancias de una serie sin instanciar todo el iCal.
|
|
|
|
## Gotchas
|
|
|
|
- **No implementa el RFC 5545 completo.** Componentes soportados:
|
|
- `FREQ` (obligatorio): `DAILY`, `WEEKLY`, `MONTHLY`, `YEARLY`.
|
|
- `INTERVAL` (default 1).
|
|
- `COUNT` (incluye la primera ocurrencia = dtstart).
|
|
- `UNTIL` (`YYYYMMDD` o `YYYYMMDDTHHMMSSZ`, inclusive).
|
|
- `BYDAY` solo para `FREQ=WEEKLY` (`MO,TU,WE,TH,FR,SA,SU`).
|
|
- Cualquier otro componente (`BYMONTHDAY`, `BYSETPOS`, `BYMONTH`, `WKST`
|
|
avanzado, EXDATE, RDATE, etc.) se **ignora silenciosamente** — no falla, pero
|
|
el resultado puede diferir del esperado por el RFC en esos casos.
|
|
- Si faltan **COUNT y UNTIL** a la vez, la generacion se acota por `range_end`
|
|
con un tope de seguridad duro de 1000 ocurrencias para no colgar.
|
|
- En `FREQ=MONTHLY`/`YEARLY` con dia 29/30/31, los meses sin ese dia recortan al
|
|
ultimo dia valido del mes destino.
|
|
- No gestiona zonas horarias: con `all_day=False` conserva la hora local del
|
|
dtstart sin sufijo `Z`; el llamador es responsable de la tz (TZID/VTIMEZONE).
|
|
- El filtro de rango compara solo la parte `YYYYMMDD` del DTSTART, no la hora.
|