feat(duckdb,dav): primitivas de escritura DuckDB + libretas CardDAV + vCard multi-valor
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>
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
---
|
||||
name: dav_make_addressbook
|
||||
kind: function
|
||||
lang: py
|
||||
domain: infra
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def dav_make_addressbook(base_url: str, username: str, password: str, contacts_home: str, slug: str, display_name: str = \"\", description: str = \"\", *, timeout_s: float = 20.0, verify_tls: bool = True) -> dict"
|
||||
description: "Crea una nueva coleccion de contactos CardDAV (una libreta/agenda de contactos nueva) bajo el contacts-home de un principal via MKCOL extendido (RFC 5689), declarando el resourcetype como addressbook y fijando el displayname y la descripcion (addressbook-description) en el propio cuerpo XML. La coleccion se crea en <contacts_home><slug>/. El slug se sanea a [a-z0-9_-] (minusculas, espacios->guion); si queda vacio devuelve error de validacion. Idempotente: 201 Created es exito; 405/301 (ya existe) devuelve {status:'ok', existed:True}. Escapa display_name/description para XML. Construye Authorization: Basic base64(user:pass) a mano. Maneja errores sin lanzar (salvo validacion de args). Solo stdlib (urllib, base64, re, ssl, xml.sax.saxutils). Probado contra Xandikos. Analoga de dav_make_calendar para CardDAV."
|
||||
tags: [dav, carddav, addressbook, contacts, mkcol, create, collection, http, infra]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: [base64, re, ssl, urllib.error, urllib.request, xml.sax.saxutils]
|
||||
params:
|
||||
- name: base_url
|
||||
desc: "URL base del servidor DAV sin barra final (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: contacts_home
|
||||
desc: "ruta del contacts-home del principal con barra final (p.ej. '/enmanuel/contacts/'). La nueva coleccion cuelga de el."
|
||||
- name: slug
|
||||
desc: "segmento de path de la coleccion en la URL (p.ej. 'trabajo'); se sanea a [a-z0-9_-]. La coleccion se crea en <contacts_home><slug>/. Si queda vacio tras sanear, devuelve error de validacion."
|
||||
- name: display_name
|
||||
desc: "nombre visible de la coleccion (DAV:displayname). Si vacio, usa el slug saneado."
|
||||
- name: description
|
||||
desc: "descripcion de la coleccion (addressbook-description de CardDAV). Opcional; '' lo omite."
|
||||
- name: timeout_s
|
||||
desc: "timeout de cada peticion HTTP en segundos. Default 20.0."
|
||||
- name: verify_tls
|
||||
desc: "si True (default) verifica el certificado TLS. No desactivar salvo entorno de prueba."
|
||||
output: "dict. En exito: {status:'ok', http_status:int, href:str} y, si la coleccion ya existia, ademas existed:True. En error (sin lanzar): {status:'error', http_status:int|None, href:str, error:str}. href es la ruta de la coleccion (contacts_home + slug saneado + '/')."
|
||||
tested: true
|
||||
tests:
|
||||
- "test_sanitize_slug_minusculas"
|
||||
- "test_sanitize_slug_espacios_a_guion"
|
||||
- "test_sanitize_slug_elimina_caracteres_raros"
|
||||
- "test_sanitize_slug_colapsa_guiones_y_recorta"
|
||||
- "test_sanitize_slug_vacio"
|
||||
- "test_join_url_compone_la_coleccion"
|
||||
- "test_mkcol_xml_es_mkcol_extendido"
|
||||
- "test_mkcol_xml_declara_resourcetype_addressbook"
|
||||
- "test_mkcol_xml_incluye_displayname"
|
||||
- "test_mkcol_xml_escapa_displayname"
|
||||
- "test_mkcol_xml_incluye_y_escapa_descripcion"
|
||||
- "test_mkcol_xml_omite_descripcion_vacia"
|
||||
test_file_path: "python/functions/infra/dav_make_addressbook_test.py"
|
||||
file_path: "python/functions/infra/dav_make_addressbook.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys
|
||||
sys.path.insert(0, "python/functions")
|
||||
from infra.pass_get_secret import pass_get_secret
|
||||
from infra.dav_make_addressbook import dav_make_addressbook
|
||||
|
||||
pw = pass_get_secret("dav/xandikos-enmanuel")["value"] # NO logear
|
||||
|
||||
res = dav_make_addressbook(
|
||||
base_url="https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com",
|
||||
username="enmanuel",
|
||||
password=pw,
|
||||
contacts_home="/enmanuel/contacts/",
|
||||
slug="trabajo",
|
||||
display_name="Trabajo",
|
||||
)
|
||||
print(res)
|
||||
# {'status': 'ok', 'http_status': 201, 'href': '/enmanuel/contacts/trabajo/'}
|
||||
# Volver a llamar con el mismo slug:
|
||||
# {'status': 'ok', 'http_status': 405, 'href': '/enmanuel/contacts/trabajo/', 'existed': True}
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando el usuario quiere una libreta/agenda de contactos nueva ademas de la
|
||||
principal: una coleccion CardDAV separada ("Trabajo", "Personal", "Familia") con
|
||||
su propio nombre visible, bajo el contacts-home del principal. Es la analoga de
|
||||
`dav_make_calendar` para CardDAV. El `href` devuelto es la ruta de la coleccion
|
||||
que luego usas para escribir vCards (PUT de cada contacto) o para listarla en el
|
||||
selector de libretas.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- Impura: requiere red + Basic auth contra el servidor DAV. El password viene de
|
||||
`pass`, no se logea ni se hardcodea.
|
||||
- Idempotente: si la coleccion ya existe en ese path el servidor responde 405
|
||||
(Method Not Allowed) o 301; ambos se traducen a `{status:'ok', existed:True}`
|
||||
en vez de error, asi que es seguro reintentar.
|
||||
- A diferencia de los calendarios (que tienen el metodo HTTP dedicado
|
||||
MKCALENDAR), CardDAV NO define un "MKADDRESSBOOK". La creacion se hace con
|
||||
**MKCOL extendido (RFC 5689)**: metodo HTTP `MKCOL` con un cuerpo XML que
|
||||
declara el `resourcetype` como `D:collection` + `C:addressbook`. Probado contra
|
||||
Xandikos, que lo soporta.
|
||||
- Fallback para servidores sin MKCOL extendido: algunos servidores CardDAV viejos
|
||||
no aceptan cuerpo en MKCOL y devuelven 415/400. En ese caso el patron es
|
||||
`MKCOL` simple (sin cuerpo) para crear la coleccion + un `PROPPATCH` posterior
|
||||
que fije el `resourcetype` addressbook, el `displayname` y la
|
||||
`addressbook-description`. Esta funcion implementa solo el camino extendido (un
|
||||
request); si te topas con un servidor que no lo soporta, anade el fallback
|
||||
MKCOL+PROPPATCH antes de promoverlo.
|
||||
- El `slug` se sanea a `[a-z0-9_-]` (minusculas, espacios->guion, resto fuera).
|
||||
Un slug que queda vacio tras sanear (p.ej. solo simbolos) devuelve error de
|
||||
validacion sin tocar la red. El `display_name` y la `description` se escapan
|
||||
para XML, pero el `slug` que va en la URL ya esta restringido al charset seguro.
|
||||
Reference in New Issue
Block a user