"""Borra (DELETE) un recurso DAV individual via HTTP Basic auth. Funcion impura: hace una peticion HTTP DELETE. Construye el header `Authorization: Basic base64(user:pass)` a mano con stdlib. El resource_path puede ser un href absoluto (como los que devuelve dav_list_resources / dav_get_collection) o una ruta relativa al base_url. Opcionalmente envia el header `If-Match: ` para un borrado condicional (solo borra si el etag coincide, evita pisar una edicion concurrente). Maneja errores sin lanzar. Solo usa stdlib (urllib, base64, ssl). ATENCION: DELETE es DESTRUCTIVO e IRREVERSIBLE en el servidor. Usar con confirmacion explicita del caller (nunca a ciegas en un bucle de sync). Pensado para limpiar recursos de prueba o retirar contactos obsoletos de forma controlada. """ import base64 import ssl import urllib.error import urllib.request def _basic_auth_header(username: str, password: str) -> str: raw = ("%s:%s" % (username, password)).encode("utf-8") return "Basic " + base64.b64encode(raw).decode("ascii") def _resolve_url(base_url: str, resource_path: str) -> str: if resource_path.startswith("http://") or resource_path.startswith("https://"): return resource_path return base_url.rstrip("/") + "/" + resource_path.lstrip("/") def dav_delete_resource( base_url: str, username: str, password: str, resource_path: str, *, etag: str = "", timeout_s: float = 20.0, verify_tls: bool = True, ) -> dict: """Borra un recurso DAV (DELETE). DESTRUCTIVO e IRREVERSIBLE. Args: base_url: URL base del servidor DAV. Se ignora si resource_path ya es una URL absoluta. username: usuario para HTTP Basic auth. password: contrasena para HTTP Basic auth. Resolver desde pass. resource_path: href absoluto (p.ej. '/enmanuel/contacts/addressbook/x.vcf') o URL completa del recurso a borrar. Acepta directamente los hrefs que devuelven dav_list_resources / dav_get_collection. etag: si se da, se envia como header If-Match para un borrado condicional (el servidor solo borra si el etag actual coincide; devuelve 412 Precondition Failed si cambio). Vacio = borrado incondicional. timeout_s: timeout de la peticion en segundos. Default 20.0. verify_tls: si True (default) verifica el certificado TLS. Returns: dict. En exito: {status:'ok', http_status:int, url:str} (DELETE devuelve normalmente 204 No Content o 200). En error (sin lanzar): {status:'error', error:str, http_status:int|None}. Un 404 (ya no existe) se devuelve como error con http_status=404; el caller puede tratarlo como idempotente (ya borrado). """ url = _resolve_url(base_url, resource_path) headers = {"Authorization": _basic_auth_header(username, password)} if etag: headers["If-Match"] = etag req = urllib.request.Request(url, method="DELETE", headers=headers) context = None if verify_tls else ssl._create_unverified_context() try: with urllib.request.urlopen(req, timeout=timeout_s, context=context) as resp: return {"status": "ok", "http_status": resp.status, "url": url} except urllib.error.HTTPError as e: return {"status": "error", "error": "http %s" % e.code, "http_status": e.code} except urllib.error.URLError as e: return {"status": "error", "error": str(e.reason), "http_status": None} except Exception as e: # noqa: BLE001 return {"status": "error", "error": str(e), "http_status": None}