1c4a4b9259
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>
107 lines
6.0 KiB
Markdown
107 lines
6.0 KiB
Markdown
# 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, usuario `enmanuel`.
|
|
- Password: `pass dav/xandikos-enmanuel` (primera linea). Resolver con `pass_get_secret_py_infra`, NUNCA hardcodear.
|
|
- Principal: `/enmanuel/`. Colecciones:
|
|
- CardDAV: `/enmanuel/contacts/addressbook/`
|
|
- CalDAV: `/enmanuel/calendars/calendar/`
|
|
|
|
## Ejemplo canonico end-to-end
|
|
|
|
Importar un `.vcf` exportado de Google a Xandikos, leyendo la password de `pass`:
|
|
|
|
```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
|
|
|
|
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:
|
|
|
|
```python
|
|
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):
|
|
|
|
```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/
|
|
```
|
|
|
|
## Fronteras
|
|
|
|
- **No descubre el principal ni las colecciones**: hay que conocer los paths
|
|
(`/enmanuel/contacts/addressbook/`, etc.). No implementa `current-user-principal`
|
|
ni `addressbook-home-set` discovery.
|
|
- **No hace sync incremental** real: `dav_list_resources` devuelve 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`/`.ics` antes de importar.
|
|
|
|
## Prerequisitos
|
|
|
|
- `pass` configurado con la entrada `dav/xandikos-enmanuel`.
|
|
- Conectividad TLS al endpoint publico (`verify_tls=True`).
|
|
- Python del registry: `python/.venv/bin/python3`.
|