Cinco funciones nuevas para soportar DuckDB como fuente de verdad del project osint:
Grupo duckdb (escritura, complementan a duckdb_query_readonly):
- duckdb_execute_py_infra (impure): ejecuta INSERT/UPDATE/DELETE/DDL en read-write, commit, {status,rowcount}. 6 tests.
- duckdb_upsert_py_infra (impure): UPSERT ON CONFLICT actualizando solo update_cols → ownership selectivo (un re-upsert no pisa columnas excluidas). 7 tests.
Grupo dav (libretas de contactos + vCard multi-valor):
- dav_make_addressbook_py_infra (impure): crea una libreta CardDAV nueva via extended MKCOL (RFC 5689). Idempotente. 12 tests.
- dav_list_addressbooks_py_infra (impure): lista las libretas del contacts-home (PROPFIND Depth:1). 7 tests.
- build_vcard_py_core (pure): serializa un contacto a vCard 3.0 multi-valor (N TEL/EMAIL/ADR + X-OSINT-*). 5 tests.
Paginas de capacidad duckdb.md y dav.md actualizadas.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.0 KiB
dav — Cliente CardDAV/CalDAV (Python, solo stdlib)
Grupo de capacidad para operar un servidor CardDAV/CalDAV (Xandikos, git-backed,
en el VPS magnus) desde Python sin dependencias externas. Cubre el flujo de
migracion: partir un export de Google (un .vcf con N contactos, un .ics con
N eventos) en recursos individuales y subirlos uno a uno por HTTP PUT con Basic auth.
Tambien listar y descargar recursos para verificar o hacer backup.
Formaliza el flujo ad-hoc (heredocs) que migro 820 contactos + 98 eventos a Xandikos
(regla function_growth_and_self_docs: una composicion repetida >2 veces se promueve
a funciones/pipelines del registry).
Restriccion de diseno
Solo stdlib (urllib.request, re, hashlib, base64, ssl). Sin requests,
caldav ni vobject. El header Authorization: Basic base64(user:pass) se construye
a mano. verify_tls=True por defecto. Coherente con el grupo osint-passive (sin deps).
Funciones
| ID | Firma corta | Que hace | Purity |
|---|---|---|---|
split_vcards_py_infra |
split_vcards(vcf_text) -> list |
Parte un .vcf en VCARDs individuales |
pure |
split_vevents_to_vcalendars_py_infra |
split_vevents_to_vcalendars(ics_text, prodid?) -> list |
Parte un VCALENDAR con N VEVENT en N VCALENDARs autonomos (replica VTIMEZONE) | pure |
extract_or_make_uid_py_infra |
extract_or_make_uid(text, prefix?) -> str |
Extrae el UID: o sintetiza <prefix><md5[:16]> determinista |
pure |
carddav_put_vcard_py_infra |
carddav_put_vcard(base_url, user, pw, coll, uid, vcard) -> dict |
PUT de un VCARD (.vcf, text/vcard) |
impure |
caldav_put_event_py_infra |
caldav_put_event(base_url, user, pw, coll, uid, vcal) -> dict |
PUT de un VCALENDAR (.ics, text/calendar) |
impure |
dav_list_resources_py_infra |
dav_list_resources(base_url, user, pw, coll) -> dict |
PROPFIND Depth:1 -> lista de {href, etag} |
impure |
dav_get_resource_py_infra |
dav_get_resource(base_url, user, pw, href) -> dict |
GET de un recurso -> texto VCARD/VCALENDAR | impure |
dav_make_calendar_py_infra |
dav_make_calendar(base_url, user, pw, calendar_home, slug, name?, color?, desc?) -> dict |
MKCALENDAR + PROPPATCH: crea una coleccion de calendario (agenda) nueva | impure |
dav_make_addressbook_py_infra |
dav_make_addressbook(base_url, user, pw, contacts_home, slug, name?, desc?) -> dict |
Extended MKCOL: crea una coleccion CardDAV (libreta/agenda de contactos) nueva | impure |
dav_list_addressbooks_py_infra |
dav_list_addressbooks(base_url, user, pw, contacts_home) -> dict |
PROPFIND Depth:1: lista las libretas CardDAV del contacts-home con nombre y descripcion | impure |
build_vcard_py_core |
build_vcard(contact: dict) -> str |
Serializa un contacto a VCARD 3.0 MULTI-VALOR (N TEL/EMAIL/ADR + X-OSINT-*); pura | pure |
expand_rrule_py_infra |
expand_rrule(dtstart_ical, rrule, range_start, range_end, all_day?) -> list |
Expande una RRULE iCalendar a las fechas de cada ocurrencia dentro de un rango | pure |
import_vcf_to_carddav_py_pipelines |
import_vcf_to_carddav(vcf_path, base_url, user, pw, coll) -> dict |
Pipeline: .vcf -> split -> uid -> PUT por tarjeta | impure |
import_ics_to_caldav_py_pipelines |
import_ics_to_caldav(ics_path, base_url, user, pw, coll) -> dict |
Pipeline: .ics -> split -> uid -> PUT por evento | impure |
Sistema real (para los ejemplos)
- Servidor: Xandikos en
https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com, Basic auth, usuarioenmanuel. - Password:
pass dav/xandikos-enmanuel(primera linea). Resolver conpass_get_secret_py_infra, NUNCA hardcodear. - Principal:
/enmanuel/. Colecciones:- CardDAV:
/enmanuel/contacts/addressbook/ - CalDAV:
/enmanuel/calendars/calendar/
- CardDAV:
Ejemplo canonico end-to-end
Importar un .vcf exportado de Google a Xandikos, leyendo la password de pass:
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
BASE = "https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com"
pw = pass_get_secret("dav/xandikos-enmanuel")["value"] # NO logear
summary = import_vcf_to_carddav(
vcf_path="/home/enmanuel/Descargas/contacts.vcf",
base_url=BASE,
username="enmanuel",
password=pw,
collection_path="/enmanuel/contacts/addressbook/",
)
print(summary["ok"], summary["fail"], summary["total"]) # 820 0 820
Verificar el resultado listando la coleccion:
from infra.dav_list_resources import dav_list_resources
res = dav_list_resources(BASE, "enmanuel", pw, "/enmanuel/contacts/addressbook/")
print(res["status"], len(res["resources"])) # ok 820
El calendario es analogo con import_ics_to_caldav + /enmanuel/calendars/calendar/.
Desde la CLI del registry (resuelve la pass como variable, no la pongas en claro):
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/
Fronteras
- No descubre el principal ni las colecciones: hay que conocer los paths
(
/enmanuel/contacts/addressbook/, etc.). No implementacurrent-user-principalniaddressbook-home-setdiscovery. - No hace sync incremental real:
dav_list_resourcesdevuelve etags pero no hay logica de diff/merge. Re-importar es idempotente por UID (sobrescribe), no incremental. - No parsea campos VCARD/VEVENT: trata cada componente como texto opaco. Para transformar contenido (renombrar, deduplicar por nombre) usa otra herramienta.
- Solo VEVENT en calendario: VTODO/VJOURNAL se ignoran al partir el
.ics. - Escrituras irreversibles: los PUT sobrescriben en el servidor. Idempotente
por UID pero no hay confirmacion previa; valida el
.vcf/.icsantes de importar.
Prerequisitos
passconfigurado con la entradadav/xandikos-enmanuel.- Conectividad TLS al endpoint publico (
verify_tls=True). - Python del registry:
python/.venv/bin/python3.