---
name: build_vevent
kind: function
lang: py
domain: core
version: "1.0.0"
purity: pure
signature: "def build_vevent(event: dict) -> str"
description: "Serializa un evento (dict) a un texto VCALENDAR (RFC 5545) con un VEVENT dentro. Analoga de build_vcard pero para calendarios. Pura, solo compone texto (sin red, sin disco, sin reloj: nunca usa datetime.now). Acepta claves en espanol e ingles, normaliza fechas de varios formatos humanos a iCal compacto, sintetiza UID determinista si falta, soporta all_day, RRULE y VALARM. Salida CRLF terminando en END:VCALENDAR."
tags: [dav, caldav, ical, vevent, calendar, serialize]
params:
  - name: event
    desc: "dict del evento. Claves opcionales salvo lo indicado (acepta nombre ES o EN): uid (identificador; si falta se sintetiza determinista 'evt-'+md5(summary+start)[:16]), summary/titulo/resumen (-> SUMMARY, OBLIGATORIO), start/inicio (fecha/hora inicio -> DTSTART, OBLIGATORIO), end/fin (-> DTEND; si falta deriva +1h o dia siguiente si all_day), all_day/todo_el_dia (bool -> DTSTART;VALUE=DATE), location/ubicacion/lugar (-> LOCATION), description/descripcion/notas (-> DESCRIPTION), rrule/recurrencia (string RRULE -> linea RRULE), dtstamp (iCal opcional; fallback determinista a DTSTART), alarm_minutes/recordatorio_min (int -> bloque VALARM con TRIGGER:-PTnM). Fechas aceptadas: 'YYYY-MM-DDTHH:MM[:SS]', con sufijo 'Z' para UTC, 'YYYY-MM-DD', o iCal compacto ya formado."
output: "Texto VCALENDAR (RFC 5545) con lineas separadas por CRLF: BEGIN:VCALENDAR / VERSION:2.0 / PRODID / CALSCALE:GREGORIAN, un VEVENT con UID, DTSTAMP, DTSTART, DTEND, SUMMARY y campos opcionales, terminando en END:VCALENDAR\\r\\n. Valores de texto escapados segun RFC 5545; RRULE no se escapa (sus ';'/',' son separadores propios)."
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [hashlib, datetime]
tested: true
tests: ["test_golden_evento_con_hora", "test_all_day", "test_rrule", "test_uid_sintetico_determinista", "test_end_derivado_mas_una_hora", "test_utc_con_z", "test_escape_caracteres_especiales", "test_alarm", "test_claves_espanol_equivalentes", "test_falta_summary_lanza_valueerror", "test_falta_start_lanza_valueerror"]
test_file_path: "python/functions/core/build_vevent_test.py"
file_path: "python/functions/core/build_vevent.py"
---

## Ejemplo

```python
from core.build_vevent import build_vevent

vcal = build_vevent({
    "summary": "Cita dentista",
    "start": "2026-06-20T17:00",
    "end": "2026-06-20T18:00",
    "location": "Clinica",
    "description": "Revision anual",
    "alarm_minutes": 30,
})
print(vcal)
# BEGIN:VCALENDAR
# VERSION:2.0
# PRODID:-//fn_registry//build_vevent//ES
# CALSCALE:GREGORIAN
# BEGIN:VEVENT
# UID:evt-<md5(summary+start)>
# DTSTAMP:20260620T170000
# DTSTART:20260620T170000
# DTEND:20260620T180000
# SUMMARY:Cita dentista
# LOCATION:Clinica
# DESCRIPTION:Revision anual
# BEGIN:VALARM
# ACTION:DISPLAY
# DESCRIPTION:Cita dentista
# TRIGGER:-PT30M
# END:VALARM
# END:VEVENT
# END:VCALENDAR

# Evento de todo el dia recurrente:
build_vevent({"titulo": "Cumpleanos", "inicio": "2026-06-20", "all_day": True,
              "recurrencia": "FREQ=YEARLY"})
```

## Cuando usarla

Cuando hay que materializar un evento a texto iCalendar para subirlo a CalDAV.
Es el paso "componer el VCALENDAR" previo a `caldav_put_event_py_infra`: le pasas
el texto que devuelve y el UID. La usa el pipeline `add_event_dav_py_pipelines`
para anadir un evento de un tiro. Si no das `uid`, el UID sintetico determinista
hace que re-construir el mismo evento produzca el mismo recurso `<uid>.ics`
(idempotente al subir). Reserva `build_vcard_py_core` para contactos (vCard) y
esta para eventos (VEVENT).

## Gotchas

- **Pura salvo `ValueError`**: determinista, sin efectos (no red, no disco, no
  reloj). NUNCA llama `datetime.now()` — `datetime.strptime`/`timedelta` solo se
  usan para parsear y derivar fechas a partir de los inputs. La unica excepcion
  posible es `ValueError` cuando falta `summary` o `start` (sin ellos no hay
  evento) — validacion de entrada aceptable en una pura, en paridad con
  `build_vcard`.
- **DTSTAMP siempre presente**: RFC 5545 lo exige. Si no se pasa `dtstamp`, se
  usa el valor de `DTSTART` como fallback determinista (no la hora actual), para
  que la salida sea reproducible y la funcion siga siendo pura.
- **RRULE no se escapa**: el valor de `rrule` es un recurrence rule estructurado
  (`FREQ=...;BYDAY=...`) cuyos `;` y `,` son separadores propios. Se emite tal
  cual (stripeado). El resto de campos de texto (SUMMARY/LOCATION/DESCRIPTION) si
  se escapan (RFC 5545: `\`, `\n`, `,`, `;`; el `\r` crudo se elimina).
- **all_day usa VALUE=DATE**: con `all_day=True` el DTSTART/DTEND salen como
  `;VALUE=DATE:YYYYMMDD` y el DTEND por defecto es el dia siguiente (convencion
  iCal: fin exclusivo). Sin `all_day`, son datetime y el DTEND por defecto es
  start+1h.
- **Formatos de fecha**: acepta varios formatos humanos y los normaliza, pero un
  string mal formado (que `strptime` no entienda) lanza `ValueError` del propio
  `strptime` — valida tus inputs si vienen de fuera.
