fix(events): envolver VEVENT en VCALENDAR al push (Xandikos 500) + INSERT explicito en contacts (columna import_key)
El raw de un evento guardaba solo BEGIN:VEVENT...END:VEVENT; subirlo a CalDAV genera un .ics invalido que rompe Xandikos (assert isinstance(cal, Calendar) -> 500 en todo el calendario). _ensure_vcalendar lo envuelve en el push. Ademas, la columna import_key (migracion 004) rompia los INSERT posicionales de contacts: ahora son explicitos por columna y el ingest puebla import_key con la funcion del registry. Tests actualizados (4 derivadas, INSERT explicito). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+7
-1
@@ -30,6 +30,7 @@ from .config import Config
|
||||
from .db import write_conn
|
||||
from .derived import rebuild_derived
|
||||
from .registry_bridge import (
|
||||
contact_import_key,
|
||||
dav_get_collection,
|
||||
dav_list_addressbooks,
|
||||
dav_list_calendars,
|
||||
@@ -450,6 +451,9 @@ def ingest_dav(cfg: Config) -> dict:
|
||||
res.get("data", ""),
|
||||
None, # note_path se rellena en el enlace posterior
|
||||
now,
|
||||
# Clave de importación determinística: nace con el contacto
|
||||
# para que los re-imports lo localicen sin match frágil.
|
||||
contact_import_key(parsed["fn"] or "", parsed["tels"], parsed["emails"]),
|
||||
]
|
||||
)
|
||||
|
||||
@@ -503,7 +507,9 @@ def ingest_dav(cfg: Config) -> dict:
|
||||
conn.execute("DELETE FROM contacts")
|
||||
if contact_rows:
|
||||
conn.executemany(
|
||||
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO contacts (uid, collection, etag, fn, tels, "
|
||||
"emails, raw, note_path, updated_at, import_key) "
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
contact_rows,
|
||||
)
|
||||
conn.execute("DELETE FROM events")
|
||||
|
||||
@@ -123,6 +123,12 @@ duckdb_upsert = _load_registry_fn("infra", "duckdb_upsert", "duckdb_upsert")
|
||||
# Composición del vCard multi-valor (DB -> Xandikos), puro.
|
||||
build_vcard = _load_registry_fn("core", "build_vcard", "build_vcard")
|
||||
|
||||
# Clave de importación determinística (tel > email > nombre) para imports
|
||||
# idempotentes de contactos. Pura.
|
||||
contact_import_key = _load_registry_fn(
|
||||
"core", "contact_import_key", "contact_import_key"
|
||||
)
|
||||
|
||||
# Render de tablas Markdown + bloques sentinel idempotentes para las notas.
|
||||
render_markdown_table = _load_registry_fn(
|
||||
"core", "render_markdown_table", "render_markdown_table"
|
||||
@@ -153,6 +159,7 @@ __all__ = [
|
||||
"duckdb_execute",
|
||||
"duckdb_upsert",
|
||||
"build_vcard",
|
||||
"contact_import_key",
|
||||
"render_markdown_table",
|
||||
"upsert_sentinel_block",
|
||||
]
|
||||
|
||||
+26
-1
@@ -848,7 +848,7 @@ def push_all_dav(cfg: Config) -> dict:
|
||||
uid = row["uid"]
|
||||
calendar = row.get("calendar") or "default"
|
||||
collection = cfg.dav_calendar_home.rstrip("/") + "/" + calendar + "/"
|
||||
raw = row.get("raw") or _build_vcalendar(uid, {})
|
||||
raw = _ensure_vcalendar(row.get("raw")) or _build_vcalendar(uid, {})
|
||||
push = caldav_put_event(
|
||||
cfg.dav_base, cfg.dav_user, pwd, collection, uid, raw
|
||||
)
|
||||
@@ -866,6 +866,31 @@ def push_all_dav(cfg: Config) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _ensure_vcalendar(raw) -> str:
|
||||
"""Garantiza que un recurso de evento tenga el envoltorio VCALENDAR.
|
||||
|
||||
El ``raw`` de un evento a veces guarda SOLO el bloque ``BEGIN:VEVENT ...
|
||||
END:VEVENT`` (así lo extrae el parser del ingest DAV). Subir eso a CalDAV
|
||||
produce un recurso ``.ics`` inválido: Xandikos falla al pedir la propiedad
|
||||
``schedule-tag`` (``assert isinstance(cal, Calendar)``) y devuelve 500 para
|
||||
todo el calendario. Esta función envuelve el VEVENT en un VCALENDAR mínimo
|
||||
cuando falta, normalizando a CRLF; si el raw ya es un VCALENDAR lo deja igual.
|
||||
Devuelve cadena vacía si no hay contenido (el llamador cae a _build_vcalendar).
|
||||
"""
|
||||
text = (raw or "").strip()
|
||||
if not text:
|
||||
return ""
|
||||
if "BEGIN:VCALENDAR" in text.upper():
|
||||
return raw if raw.endswith("\r\n") else raw + "\r\n"
|
||||
text = text.replace("\r\n", "\n").replace("\r", "\n")
|
||||
body = "\r\n".join(text.split("\n"))
|
||||
return (
|
||||
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//osint_db//events//EN\r\n"
|
||||
+ body
|
||||
+ "\r\nEND:VCALENDAR\r\n"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# push masivo POR DISCO (vía rápida: 1 rsync + 1 commit + 1 PROPFIND)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -140,6 +140,7 @@ def test_ingest_vault_cuenta_entidades(client):
|
||||
assert sorted(r["derived_rebuilt"]) == [
|
||||
"derived.contact_link_quality",
|
||||
"derived.event_monthly",
|
||||
"derived.org_contacts",
|
||||
"derived.person_stats",
|
||||
]
|
||||
|
||||
@@ -237,7 +238,7 @@ def test_derivadas_sin_note_path(client):
|
||||
).json()
|
||||
assert r["status"] == "ok"
|
||||
assert r["rows"] == []
|
||||
# Y las tres derivadas existen de verdad.
|
||||
# Y las derivadas existen de verdad.
|
||||
t = client.post(
|
||||
"/api/query",
|
||||
json={
|
||||
@@ -250,6 +251,7 @@ def test_derivadas_sin_note_path(client):
|
||||
assert [row["table_name"] for row in t["rows"]] == [
|
||||
"contact_link_quality",
|
||||
"event_monthly",
|
||||
"org_contacts",
|
||||
"person_stats",
|
||||
]
|
||||
|
||||
@@ -260,7 +262,7 @@ def test_link_contacts_por_telefono(client, cfg):
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
with write_conn(cfg.db_path) as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO contacts (uid, collection, etag, fn, tels, emails, raw, note_path, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
"uid-movil-1",
|
||||
"/enmanuel/contacts/addressbook/",
|
||||
@@ -274,7 +276,7 @@ def test_link_contacts_por_telefono(client, cfg):
|
||||
],
|
||||
)
|
||||
conn.execute(
|
||||
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO contacts (uid, collection, etag, fn, tels, emails, raw, note_path, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
"uid-movil-2",
|
||||
"/enmanuel/contacts/addressbook/",
|
||||
@@ -576,7 +578,7 @@ def test_compose_agenda_vcard_sin_osint_con_direcciones(client, cfg, monkeypatch
|
||||
# El contacto se enlaza a la ficha por teléfono al re-ingestar el vault.
|
||||
with write_conn(cfg.db_path) as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO contacts (uid, collection, etag, fn, tels, emails, raw, note_path, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
"uid-marca",
|
||||
"/enmanuel/contacts/addressbook/",
|
||||
@@ -651,7 +653,7 @@ def test_pull_dav_incremental_por_etag(client, cfg, monkeypatch):
|
||||
("c-gone", '"e-gone"', "Se Borra"),
|
||||
]:
|
||||
conn.execute(
|
||||
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO contacts (uid, collection, etag, fn, tels, emails, raw, note_path, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[uid, coll, etag, fn, "[]", "[]", "BEGIN:VCARD...", None, now],
|
||||
)
|
||||
|
||||
@@ -731,7 +733,7 @@ def test_write_agenda_vcards_to_dir_nombres_y_sin_osint(client, cfg, tmp_path):
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
with write_conn(cfg.db_path) as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO contacts (uid, collection, etag, fn, tels, emails, raw, note_path, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[
|
||||
"uid con espacios/raro", # fuerza saneo del nombre del recurso
|
||||
"/enmanuel/contacts/addressbook/",
|
||||
@@ -790,7 +792,7 @@ def test_push_all_dav_bulk_flujo_mockeado(client, cfg, monkeypatch):
|
||||
with write_conn(cfg.db_path) as conn:
|
||||
for uid, fn in [("c-a", "Contacto A"), ("c-b", "Contacto B")]:
|
||||
conn.execute(
|
||||
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
"INSERT INTO contacts (uid, collection, etag, fn, tels, emails, raw, note_path, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
[uid, coll, None, fn, "[]", "[]", "BEGIN:VCARD...", None, now],
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user