feat(dav,obsidian): grupo dav completo (CardDAV/CalDAV client + split vcf/ics + import pipelines) + build_obsidian_graph + dav_list_calendars

Funciones reutilizables creadas esta sesion para el sistema self-hosted de contactos/calendario (Xandikos) y la app osint_web:
- grupo dav (infra): split_vcards, split_vevents_to_vcalendars, extract_or_make_uid, carddav_put_vcard, caldav_put_event, dav_list_resources, dav_get_resource, dav_list_calendars
- pipelines: import_vcf_to_carddav, import_ics_to_caldav
- obsidian: build_obsidian_graph (grafo agregado del vault)
This commit is contained in:
2026-06-12 00:43:59 +02:00
parent 4a0f0e9dc0
commit a76760edba
32 changed files with 2814 additions and 0 deletions
@@ -0,0 +1,68 @@
---
name: split_vevents_to_vcalendars
kind: function
lang: py
domain: infra
version: "1.0.0"
purity: pure
signature: "def split_vevents_to_vcalendars(ics_text: str, prodid: str = '-//xandikos-migracion//google-export//EN') -> list"
description: "Divide un .ics (un VCALENDAR con N VEVENT) en N VCALENDARs independientes, cada uno con un unico VEVENT, header VERSION/PRODID/CALSCALE y las VTIMEZONE del original. Pura, solo stdlib (re). Util para importar a CalDAV un .ics exportado de Google Calendar que mete todos los eventos en un solo VCALENDAR: cada salida se sube como recurso .ics independiente. Normaliza saltos de linea a CRLF (RFC 5545)."
tags: [dav, caldav, ical, ics, vevent, vcalendar, calendar, infra, split]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [re]
params:
- name: ics_text
desc: "contenido completo del .ics: un VCALENDAR con uno o varios VEVENT. Tolera LF o CRLF."
- name: prodid
desc: "valor del campo PRODID del header de cada VCALENDAR de salida. Default identifica la migracion a Xandikos."
output: "list[str]. Cada elemento es un VCALENDAR completo y autonomo ('BEGIN:VCALENDAR'..'END:VCALENDAR' terminado en CRLF) con header VERSION:2.0 / PRODID / CALSCALE:GREGORIAN, las VTIMEZONE del original (si las habia, replicadas en cada salida) y un unico VEVENT. Lista vacia si no hay ningun VEVENT."
tested: true
tests:
- "test_dos_vevents_devuelve_dos_vcalendars"
- "test_cada_salida_tiene_un_solo_vevent"
- "test_header_vcalendar_correcto"
- "test_vtimezone_se_replica_en_cada_salida"
- "test_salida_termina_en_crlf"
- "test_input_vacio_devuelve_lista_vacia"
test_file_path: "python/functions/infra/split_vevents_to_vcalendars_test.py"
file_path: "python/functions/infra/split_vevents_to_vcalendars.py"
---
## Ejemplo
```python
import sys
sys.path.insert(0, "python/functions")
from infra.split_vevents_to_vcalendars import split_vevents_to_vcalendars
ics = (
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//Google//EN\r\n"
"BEGIN:VEVENT\r\nUID:a@x\r\nSUMMARY:Reunion\r\nEND:VEVENT\r\n"
"BEGIN:VEVENT\r\nUID:b@x\r\nSUMMARY:Comida\r\nEND:VEVENT\r\n"
"END:VCALENDAR\r\n"
)
cals = split_vevents_to_vcalendars(ics)
print(len(cals)) # 2
print(cals[0].count("BEGIN:VEVENT")) # 1 (un evento por VCALENDAR)
```
## Cuando usarla
Cuando exportas tu calendario de Google a un unico `.ics` (un VCALENDAR con
todos los eventos dentro) y necesitas subir cada evento como recurso CalDAV
separado a Xandikos. Es el primer paso del pipeline `import_ics_to_caldav`:
split → extraer UID por evento → `caldav_put_event`. Cada salida es un `.ics`
valido y autonomo que un cliente de calendario puede consumir por si solo.
## Gotchas
Funcion pura. Replica TODAS las VTIMEZONE del VCALENDAR original en cada salida
(conservador: garantiza que cualquier TZID referenciado por el VEVENT este
definido, aunque algun evento no use ninguna). No deduplica ni filtra
timezones por evento. No valida que el VEVENT este completo ni reescribe DTSTART
/DTEND. Si el .ics no contiene VEVENT (p.ej. solo VTODO o VJOURNAL) devuelve
lista vacia.