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:
2026-06-13 00:33:12 +02:00
parent 1c8a86594f
commit 1c4a4b9259
17 changed files with 1773 additions and 0 deletions
@@ -0,0 +1,85 @@
"""Tests para duckdb_execute."""
import duckdb
import pytest
from .duckdb_execute import duckdb_execute
@pytest.fixture
def db(tmp_path):
"""Crea una base DuckDB temporal con una tabla vacia y devuelve su path."""
path = str(tmp_path / "test.duckdb")
con = duckdb.connect(path)
con.execute("CREATE TABLE t (id INTEGER, name VARCHAR)")
con.close()
return path
def _read_rows(path: str) -> list:
"""Relee la tabla t en una conexion read_only y devuelve las filas."""
con = duckdb.connect(path, read_only=True)
try:
return con.execute("SELECT id, name FROM t ORDER BY id").fetchall()
finally:
con.close()
def test_insert_devuelve_status_ok_y_persiste(db):
res = duckdb_execute(
db,
"INSERT INTO t VALUES (?, ?), (?, ?), (?, ?)",
params=[1, "a", 2, "b", 3, "c"],
)
assert res["status"] == "ok"
assert res["rowcount"] == 3
# Releemos para confirmar el efecto en disco.
assert _read_rows(db) == [(1, "a"), (2, "b"), (3, "c")]
def test_update_afecta_filas_y_persiste(db):
duckdb_execute(db, "INSERT INTO t VALUES (1,'a'),(2,'b'),(3,'c')")
res = duckdb_execute(db, "UPDATE t SET name = ? WHERE id <= ?", params=["x", 2])
assert res["status"] == "ok"
assert res["rowcount"] == 2
assert _read_rows(db) == [(1, "x"), (2, "x"), (3, "c")]
def test_delete_afecta_filas_y_persiste(db):
duckdb_execute(db, "INSERT INTO t VALUES (1,'a'),(2,'b'),(3,'c')")
res = duckdb_execute(db, "DELETE FROM t WHERE id = ?", params=[3])
assert res["status"] == "ok"
assert res["rowcount"] == 1
assert _read_rows(db) == [(1, "a"), (2, "b")]
def test_ddl_create_table_status_ok(db):
res = duckdb_execute(db, "CREATE TABLE u (x INTEGER)")
assert res["status"] == "ok"
# DDL no reporta filas: rowcount queda en -1, no falla.
assert res["rowcount"] == -1
# Confirmamos que la tabla existe insertando en ella.
res2 = duckdb_execute(db, "INSERT INTO u VALUES (42)")
assert res2["status"] == "ok"
con = duckdb.connect(db, read_only=True)
try:
assert con.execute("SELECT x FROM u").fetchall() == [(42,)]
finally:
con.close()
def test_crea_la_base_si_no_existe(tmp_path):
path = str(tmp_path / "nueva.duckdb")
res = duckdb_execute(path, "CREATE TABLE nueva (a INTEGER)")
assert res["status"] == "ok"
res2 = duckdb_execute(path, "INSERT INTO nueva VALUES (7)")
assert res2["status"] == "ok"
assert res2["rowcount"] == 1
def test_sql_invalido_devuelve_status_error(db):
res = duckdb_execute(db, "INSERT INTO tabla_que_no_existe VALUES (1)")
assert res["status"] == "error"
assert "error" in res
assert isinstance(res["error"], str) and res["error"]
# La funcion no lanza: el flujo del test llega hasta aqui sin excepcion.