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:
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: import_ics_to_caldav
|
||||
kind: pipeline
|
||||
lang: py
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def import_ics_to_caldav(ics_path: str, base_url: str, username: str, password: str, collection_path: str, *, timeout_s: float = 20.0, verify_tls: bool = True, uid_prefix: str = 'goog-') -> dict"
|
||||
description: "Pipeline que importa un archivo .ics completo a una coleccion CalDAV. Lee el .ics del disco, parte el VCALENDAR en N VCALENDARs independientes con un VEVENT cada uno (split_vevents_to_vcalendars, replicando las VTIMEZONE), por cada uno extrae o sintetiza el UID (extract_or_make_uid) y lo sube por HTTP PUT (caldav_put_event). Devuelve {ok, fail, total, errors}. Idempotente por UID: re-importar el mismo .ics sobrescribe en vez de duplicar. Formaliza el heredoc ad-hoc usado para migrar 98 eventos de Google Calendar a Xandikos. Solo stdlib."
|
||||
tags: [dav, caldav, ical, ics, vevent, import, calendar, migration, pipelines]
|
||||
uses_functions: [split_vevents_to_vcalendars_py_infra, extract_or_make_uid_py_infra, caldav_put_event_py_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [os, sys, json]
|
||||
params:
|
||||
- name: ics_path
|
||||
desc: "ruta en disco del archivo .ics a importar (export de Google Calendar: un VCALENDAR con N VEVENT)."
|
||||
- name: base_url
|
||||
desc: "URL base del servidor DAV (p.ej. 'https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com')."
|
||||
- name: username
|
||||
desc: "usuario para HTTP Basic auth (p.ej. 'enmanuel')."
|
||||
- name: password
|
||||
desc: "contrasena para HTTP Basic auth. Resolver desde pass con pass_get_secret, nunca hardcodear."
|
||||
- name: collection_path
|
||||
desc: "ruta de la coleccion CalDAV destino (p.ej. '/enmanuel/calendars/calendar/')."
|
||||
- name: timeout_s
|
||||
desc: "timeout por PUT individual en segundos. Default 20.0."
|
||||
- name: verify_tls
|
||||
desc: "si True (default) verifica el certificado TLS. No desactivar salvo entorno de prueba."
|
||||
- name: uid_prefix
|
||||
desc: "prefijo del UID sintetico para eventos sin UID. Default 'goog-'."
|
||||
output: "dict: {ok:int, fail:int, total:int, errors:[{uid, error}, ...]}. ok=eventos subidos con exito, fail=eventos que fallaron, total=VEVENT en el .ics, errors=detalle de los fallos (uid + mensaje, sin datos sensibles)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/pipelines/import_ics_to_caldav.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys
|
||||
sys.path.insert(0, "python/functions")
|
||||
from infra.pass_get_secret import pass_get_secret
|
||||
from pipelines.import_ics_to_caldav import import_ics_to_caldav
|
||||
|
||||
pw = pass_get_secret("dav/xandikos-enmanuel")["value"] # NO logear
|
||||
|
||||
summary = import_ics_to_caldav(
|
||||
ics_path="/home/enmanuel/Descargas/calendar.ics",
|
||||
base_url="https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com",
|
||||
username="enmanuel",
|
||||
password=pw,
|
||||
collection_path="/enmanuel/calendars/calendar/",
|
||||
)
|
||||
print(summary["ok"], summary["fail"], summary["total"]) # 98 0 98
|
||||
```
|
||||
|
||||
Desde la CLI del registry (resuelve la pass tu mismo y pasala como arg):
|
||||
|
||||
```bash
|
||||
PW=$(pass show dav/xandikos-enmanuel | head -n1)
|
||||
./fn run import_ics_to_caldav /home/enmanuel/Descargas/calendar.ics \
|
||||
https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com \
|
||||
enmanuel "$PW" /enmanuel/calendars/calendar/
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando tienes un `.ics` exportado (Google Calendar, otro CalDAV) con todos los
|
||||
eventos en un unico VCALENDAR y quieres volcarlo entero a Xandikos en una sola
|
||||
llamada. Reemplaza el flujo ad-hoc hecho a mano en la migracion. Re-ejecutable:
|
||||
por idempotencia de UID, correrlo dos veces no duplica eventos.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Solo importa VEVENT. Si el .ics trae VTODO o VJOURNAL, esos componentes se
|
||||
ignoran (split_vevents_to_vcalendars solo extrae VEVENT).
|
||||
- Las VTIMEZONE del original se replican en CADA evento subido (conservador):
|
||||
garantiza que cualquier TZID referenciado este definido.
|
||||
- Escritura remota masiva secuencial: una request por evento. Cada PUT respeta
|
||||
`timeout_s`.
|
||||
- Password por HTTP Basic sobre TLS; leela de `pass`, no la hardcodees ni la
|
||||
dejes en el historial del shell sin cuidado.
|
||||
- `errors` lista solo uid + mensaje, nunca el contenido del evento.
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Pipeline: importa un archivo .ics completo a una coleccion CalDAV.
|
||||
|
||||
Compone funciones del registry: lee el .ics del disco, parte el VCALENDAR en N
|
||||
VCALENDARs independientes con un VEVENT cada uno (split_vevents_to_vcalendars),
|
||||
por cada uno extrae o sintetiza el UID (extract_or_make_uid) y lo sube via HTTP
|
||||
PUT (caldav_put_event). Devuelve un resumen {ok, fail, total, errors}. Impuro
|
||||
(I/O de disco + red). Solo stdlib.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from infra.split_vevents_to_vcalendars import split_vevents_to_vcalendars
|
||||
from infra.extract_or_make_uid import extract_or_make_uid
|
||||
from infra.caldav_put_event import caldav_put_event
|
||||
|
||||
|
||||
def import_ics_to_caldav(
|
||||
ics_path: str,
|
||||
base_url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
collection_path: str,
|
||||
*,
|
||||
timeout_s: float = 20.0,
|
||||
verify_tls: bool = True,
|
||||
uid_prefix: str = "goog-",
|
||||
) -> dict:
|
||||
"""Importa todos los eventos de un .ics a una coleccion CalDAV.
|
||||
|
||||
Args:
|
||||
ics_path: ruta en disco del archivo .ics a importar.
|
||||
base_url: URL base del servidor DAV.
|
||||
username: usuario para HTTP Basic auth.
|
||||
password: contrasena para HTTP Basic auth.
|
||||
collection_path: ruta de la coleccion CalDAV destino.
|
||||
timeout_s: timeout por PUT en segundos. Default 20.0.
|
||||
verify_tls: si True (default) verifica el certificado TLS.
|
||||
uid_prefix: prefijo del UID sintetico cuando un evento no trae UID.
|
||||
|
||||
Returns:
|
||||
dict: {ok:int, fail:int, total:int, errors:[{uid:str, error:str}, ...]}.
|
||||
ok = eventos subidos con exito; fail = eventos que fallaron; total =
|
||||
eventos (VEVENT) encontrados en el .ics.
|
||||
"""
|
||||
with open(ics_path, "r", encoding="utf-8", errors="replace") as fh:
|
||||
data = fh.read()
|
||||
|
||||
vcalendars = split_vevents_to_vcalendars(data)
|
||||
ok = 0
|
||||
fail = 0
|
||||
errors = []
|
||||
for cal in vcalendars:
|
||||
uid = extract_or_make_uid(cal, prefix=uid_prefix)
|
||||
res = caldav_put_event(
|
||||
base_url, username, password, collection_path, uid, cal,
|
||||
timeout_s=timeout_s, verify_tls=verify_tls,
|
||||
)
|
||||
if res.get("status") == "ok":
|
||||
ok += 1
|
||||
else:
|
||||
fail += 1
|
||||
errors.append({"uid": uid, "error": res.get("error", "unknown")})
|
||||
|
||||
return {"ok": ok, "fail": fail, "total": len(vcalendars), "errors": errors}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
if len(sys.argv) < 6:
|
||||
print(
|
||||
"uso: import_ics_to_caldav.py <ics_path> <base_url> <username> "
|
||||
"<password> <collection_path>",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
summary = import_ics_to_caldav(
|
||||
sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]
|
||||
)
|
||||
print(json.dumps({k: summary[k] for k in ("ok", "fail", "total")}))
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
name: import_vcf_to_carddav
|
||||
kind: pipeline
|
||||
lang: py
|
||||
domain: pipelines
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def import_vcf_to_carddav(vcf_path: str, base_url: str, username: str, password: str, collection_path: str, *, timeout_s: float = 20.0, verify_tls: bool = True, uid_prefix: str = 'goog-') -> dict"
|
||||
description: "Pipeline que importa un archivo .vcf completo a una coleccion CardDAV. Lee el .vcf del disco, lo parte en VCARDs (split_vcards), por cada tarjeta extrae o sintetiza el UID (extract_or_make_uid), inyecta el UID en la tarjeta si faltaba, y la sube por HTTP PUT (carddav_put_vcard). Devuelve {ok, fail, total, errors}. Idempotente por UID: re-importar el mismo .vcf sobrescribe en vez de duplicar. Formaliza el heredoc ad-hoc usado para migrar 820 contactos de Google a Xandikos. Solo stdlib."
|
||||
tags: [dav, carddav, vcard, import, contacts, migration, pipelines]
|
||||
uses_functions: [split_vcards_py_infra, extract_or_make_uid_py_infra, carddav_put_vcard_py_infra]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: [os, sys, json]
|
||||
params:
|
||||
- name: vcf_path
|
||||
desc: "ruta en disco del archivo .vcf a importar (export de Google Contacts con N tarjetas concatenadas)."
|
||||
- name: base_url
|
||||
desc: "URL base del servidor DAV (p.ej. 'https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com')."
|
||||
- name: username
|
||||
desc: "usuario para HTTP Basic auth (p.ej. 'enmanuel')."
|
||||
- name: password
|
||||
desc: "contrasena para HTTP Basic auth. Resolver desde pass con pass_get_secret, nunca hardcodear."
|
||||
- name: collection_path
|
||||
desc: "ruta de la coleccion CardDAV destino (p.ej. '/enmanuel/contacts/addressbook/')."
|
||||
- name: timeout_s
|
||||
desc: "timeout por PUT individual en segundos. Default 20.0."
|
||||
- name: verify_tls
|
||||
desc: "si True (default) verifica el certificado TLS. No desactivar salvo entorno de prueba."
|
||||
- name: uid_prefix
|
||||
desc: "prefijo del UID sintetico para tarjetas sin UID. Default 'goog-'."
|
||||
output: "dict: {ok:int, fail:int, total:int, errors:[{uid, error}, ...]}. ok=tarjetas subidas con exito, fail=tarjetas que fallaron, total=tarjetas en el .vcf, errors=detalle de los fallos (uid + mensaje, sin datos sensibles)."
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "python/functions/pipelines/import_vcf_to_carddav.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys
|
||||
sys.path.insert(0, "python/functions")
|
||||
from infra.pass_get_secret import pass_get_secret
|
||||
from pipelines.import_vcf_to_carddav import import_vcf_to_carddav
|
||||
|
||||
pw = pass_get_secret("dav/xandikos-enmanuel")["value"] # NO logear
|
||||
|
||||
summary = import_vcf_to_carddav(
|
||||
vcf_path="/home/enmanuel/Descargas/contacts.vcf",
|
||||
base_url="https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com",
|
||||
username="enmanuel",
|
||||
password=pw,
|
||||
collection_path="/enmanuel/contacts/addressbook/",
|
||||
)
|
||||
print(summary["ok"], summary["fail"], summary["total"]) # 820 0 820
|
||||
```
|
||||
|
||||
Desde la CLI del registry (resuelve la pass tu mismo y pasala como arg):
|
||||
|
||||
```bash
|
||||
PW=$(pass show dav/xandikos-enmanuel | head -n1)
|
||||
./fn run import_vcf_to_carddav /home/enmanuel/Descargas/contacts.vcf \
|
||||
https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com \
|
||||
enmanuel "$PW" /enmanuel/contacts/addressbook/
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando tienes un `.vcf` exportado (Google Contacts, iCloud, otro CardDAV) y
|
||||
quieres volcarlo entero a Xandikos en una sola llamada en vez de subir tarjeta a
|
||||
tarjeta con heredocs. Reemplaza el flujo ad-hoc que se hizo a mano para la
|
||||
migracion. Re-ejecutable: por idempotencia de UID, correrlo dos veces no
|
||||
duplica contactos.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Escritura remota masiva: sube una request por tarjeta secuencialmente. Para
|
||||
miles de contactos puede tardar; cada PUT respeta `timeout_s`.
|
||||
- Lee TODO el .vcf en memoria; para archivos de cientos de MB considera trocear.
|
||||
- La password va por HTTP Basic sobre TLS; leela de `pass`, no la hardcodees ni
|
||||
la pongas en el historial del shell sin cuidado (usa una variable como en el
|
||||
ejemplo CLI).
|
||||
- `errors` lista solo uid + mensaje, nunca el contenido de la tarjeta.
|
||||
- Si una tarjeta no traia UID, el pipeline inyecta `UID:<goog-md5>` antes del
|
||||
END:VCARD para que el campo UID: y el nombre del recurso queden consistentes.
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Pipeline: importa un archivo .vcf completo a una coleccion CardDAV.
|
||||
|
||||
Compone funciones del registry: lee el .vcf del disco, lo parte en VCARDs
|
||||
individuales (split_vcards), por cada tarjeta extrae o sintetiza el UID
|
||||
(extract_or_make_uid) y la sube via HTTP PUT (carddav_put_vcard). Devuelve un
|
||||
resumen {ok, fail, total, errors}. Impuro (I/O de disco + red). Solo stdlib.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from infra.split_vcards import split_vcards
|
||||
from infra.extract_or_make_uid import extract_or_make_uid
|
||||
from infra.carddav_put_vcard import carddav_put_vcard
|
||||
|
||||
|
||||
def import_vcf_to_carddav(
|
||||
vcf_path: str,
|
||||
base_url: str,
|
||||
username: str,
|
||||
password: str,
|
||||
collection_path: str,
|
||||
*,
|
||||
timeout_s: float = 20.0,
|
||||
verify_tls: bool = True,
|
||||
uid_prefix: str = "goog-",
|
||||
) -> dict:
|
||||
"""Importa todas las tarjetas de un .vcf a una coleccion CardDAV.
|
||||
|
||||
Args:
|
||||
vcf_path: ruta en disco del archivo .vcf a importar.
|
||||
base_url: URL base del servidor DAV.
|
||||
username: usuario para HTTP Basic auth.
|
||||
password: contrasena para HTTP Basic auth.
|
||||
collection_path: ruta de la coleccion CardDAV destino.
|
||||
timeout_s: timeout por PUT en segundos. Default 20.0.
|
||||
verify_tls: si True (default) verifica el certificado TLS.
|
||||
uid_prefix: prefijo del UID sintetico cuando una tarjeta no trae UID.
|
||||
|
||||
Returns:
|
||||
dict: {ok:int, fail:int, total:int, errors:[{uid:str, error:str}, ...]}.
|
||||
ok = tarjetas subidas con exito; fail = tarjetas que fallaron; total =
|
||||
tarjetas encontradas en el .vcf.
|
||||
"""
|
||||
with open(vcf_path, "r", encoding="utf-8", errors="replace") as fh:
|
||||
data = fh.read()
|
||||
|
||||
cards = split_vcards(data)
|
||||
ok = 0
|
||||
fail = 0
|
||||
errors = []
|
||||
for card in cards:
|
||||
uid = extract_or_make_uid(card, prefix=uid_prefix)
|
||||
# Si la tarjeta no declaraba UID, inyectarlo antes del END:VCARD para que
|
||||
# el campo UID: y el nombre del recurso queden consistentes.
|
||||
if "UID:" not in card:
|
||||
card = card.replace("END:VCARD", "UID:%s\r\nEND:VCARD" % uid)
|
||||
res = carddav_put_vcard(
|
||||
base_url, username, password, collection_path, uid, card,
|
||||
timeout_s=timeout_s, verify_tls=verify_tls,
|
||||
)
|
||||
if res.get("status") == "ok":
|
||||
ok += 1
|
||||
else:
|
||||
fail += 1
|
||||
errors.append({"uid": uid, "error": res.get("error", "unknown")})
|
||||
|
||||
return {"ok": ok, "fail": fail, "total": len(cards), "errors": errors}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import json
|
||||
|
||||
if len(sys.argv) < 6:
|
||||
print(
|
||||
"uso: import_vcf_to_carddav.py <vcf_path> <base_url> <username> "
|
||||
"<password> <collection_path>",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(2)
|
||||
summary = import_vcf_to_carddav(
|
||||
sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]
|
||||
)
|
||||
# No imprimir errores con datos sensibles; solo conteos + uids.
|
||||
print(json.dumps({k: summary[k] for k in ("ok", "fail", "total")}))
|
||||
Reference in New Issue
Block a user