--- 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 /. 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 /. 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.