feat(browser): auto-commit con 178 cambios

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-20 18:22:23 +02:00
parent 7d100e7f3e
commit 763e06c127
178 changed files with 19917 additions and 317 deletions
@@ -0,0 +1,179 @@
"""Pipeline: anade UN contacto a la libreta CardDAV de Enmanuel en una llamada.
Compone funciones del registry: genera un UID determinista cuando el caller no
da uno (contact_import_key) para que re-anadir el mismo contacto sobrescriba en
vez de duplicar, serializa el dict de contacto a VCARD 3.0 (build_vcard),
resuelve la contrasena CardDAV desde `pass` (pass_get_secret) y sube el VCARD
via HTTP PUT (carddav_put_vcard). Impuro (red + lectura de `pass`). Solo stdlib.
La contrasena resuelta NUNCA se logea ni se incluye en el dict de retorno.
"""
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from core.build_vcard import build_vcard
from core.contact_import_key import contact_import_key
from infra.carddav_put_vcard import carddav_put_vcard
from infra.pass_get_secret import pass_get_secret
# Config destino embebida (libreta CardDAV de Enmanuel en Xandikos self-hosted).
DEFAULT_BASE_URL = "https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com"
DEFAULT_USERNAME = "enmanuel"
DEFAULT_COLLECTION = "/enmanuel/contacts/addressbook/"
def _as_list(value) -> list:
"""Normaliza None / string suelto / lista a lista de strings.
None -> []; string suelto -> [string]; lista|tupla -> lista. Cualquier otro
valor escalar se envuelve en una lista de un elemento.
"""
if value is None:
return []
if isinstance(value, str):
return [value]
if isinstance(value, (list, tuple)):
return list(value)
return [value]
def add_contact_dav(
name: str,
*,
tels=None,
emails=None,
adrs=None,
org: str = "",
note: str = "",
uid: str = "",
base_url: str = DEFAULT_BASE_URL,
username: str = DEFAULT_USERNAME,
collection_path: str = DEFAULT_COLLECTION,
secret_path: str = "dav/xandikos-enmanuel",
timeout_s: float = 20.0,
verify_tls: bool = True,
) -> dict:
"""Anade un contacto a la libreta CardDAV en una sola llamada (one-shot).
Args:
name: nombre completo del contacto (FN del vCard). Obligatorio.
tels: telefono(s). Acepta lista, string suelto o None.
emails: email(s). Acepta lista, string suelto o None.
adrs: direccion(es). Acepta lista, string suelto o None.
org: organizacion (ORG). Vacio = se omite.
note: nota libre (NOTE). Vacio = se omite.
uid: UID del vCard. Si se deja vacio se calcula con contact_import_key
(telefono > email > nombre), de modo que re-anadir el mismo contacto
sobrescribe el recurso en vez de duplicarlo (idempotencia).
base_url: URL base del servidor DAV. Default = libreta de Enmanuel.
username: usuario HTTP Basic. Default = enmanuel.
collection_path: ruta de la coleccion CardDAV destino.
secret_path: ruta del secreto en `pass` con la contrasena (primera linea).
timeout_s: timeout del PUT en segundos. Default 20.0.
verify_tls: si True (default) verifica el certificado TLS.
Returns:
dict. En exito reusa el dict de carddav_put_vcard mas el uid usado:
{status:'ok', http_status:int, url:str, uid:str}. En error (sin lanzar):
{status:'error', error:str, uid:str, http_status:int|None}. Si la
contrasena no se encuentra en `pass`, devuelve {status:'error',
error:..., uid:...} sin tocar la red.
"""
tels_list = _as_list(tels)
emails_list = _as_list(emails)
adrs_list = _as_list(adrs)
used_uid = uid.strip() if uid else ""
if not used_uid:
used_uid = contact_import_key(name, phones=tels_list, emails=emails_list)
contact = {"uid": used_uid, "fn": name}
if tels_list:
contact["tels"] = tels_list
if emails_list:
contact["emails"] = emails_list
if adrs_list:
contact["adrs"] = adrs_list
if org:
contact["org"] = org
if note:
contact["note"] = note
vcard_text = build_vcard(contact)
secret = pass_get_secret(secret_path)
if secret.get("status") != "ok":
return {
"status": "error",
"error": "pass: %s" % secret.get("error", "secret not found"),
"uid": used_uid,
"http_status": None,
}
password = secret["value"]
result = carddav_put_vcard(
base_url,
username,
password,
collection_path,
used_uid,
vcard_text,
timeout_s=timeout_s,
verify_tls=verify_tls,
)
# Reusar el dict de carddav_put_vcard + asegurar el uid usado.
result["uid"] = used_uid
return result
if __name__ == "__main__":
import argparse
import json
parser = argparse.ArgumentParser(
description="Anade UN contacto a la libreta CardDAV de Enmanuel."
)
parser.add_argument("--name", required=True, help="Nombre completo (FN).")
parser.add_argument(
"--tel", action="append", default=[], help="Telefono (repetible)."
)
parser.add_argument(
"--email", action="append", default=[], help="Email (repetible)."
)
parser.add_argument(
"--adr", action="append", default=[], help="Direccion (repetible)."
)
parser.add_argument("--org", default="", help="Organizacion (ORG).")
parser.add_argument("--note", default="", help="Nota libre (NOTE).")
parser.add_argument("--uid", default="", help="UID explicito (opcional).")
parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
parser.add_argument("--username", default=DEFAULT_USERNAME)
parser.add_argument("--collection-path", default=DEFAULT_COLLECTION)
parser.add_argument("--secret-path", default="dav/xandikos-enmanuel")
parser.add_argument("--timeout-s", type=float, default=20.0)
parser.add_argument(
"--no-verify-tls",
action="store_true",
help="Desactiva la verificacion TLS (solo pruebas).",
)
args = parser.parse_args()
out = add_contact_dav(
args.name,
tels=args.tel,
emails=args.email,
adrs=args.adr,
org=args.org,
note=args.note,
uid=args.uid,
base_url=args.base_url,
username=args.username,
collection_path=args.collection_path,
secret_path=args.secret_path,
timeout_s=args.timeout_s,
verify_tls=not args.no_verify_tls,
)
print(json.dumps(out, ensure_ascii=False))