Files
osint_db/server/registry_bridge.py
T
egutierrez d53d7a9a7e 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>
2026-06-13 12:15:27 +02:00

166 lines
6.4 KiB
Python

"""Puente con el fn_registry: localiza python/functions y expone las funciones.
Registry-first: esta app NO reimplementa el parseo de notas Obsidian, el
protocolo DAV, el acceso a pass ni la ejecución read-only de DuckDB. Importa
las funciones del registry y las reexporta para el resto de módulos del
service. Toda función importada aquí está declarada en uses_functions del
app.md.
La localización de python/functions evita paths hardcodeados de usuario:
prueba las variables de entorno FN_REGISTRY_FUNCTIONS y FN_REGISTRY_ROOT,
después sube por los directorios padre de este archivo hasta encontrar una
raíz que contenga python/functions/obsidian (la app vive en
<root>/projects/osint/apps/osint_db/server/), y por último cae al layout
estándar del PC.
"""
from __future__ import annotations
import os
import sys
def _registry_functions_dir() -> str:
"""Devuelve el directorio python/functions del fn_registry."""
env_functions = os.environ.get("FN_REGISTRY_FUNCTIONS")
if env_functions and os.path.isdir(os.path.join(env_functions, "obsidian")):
return env_functions
candidates: list[str] = []
env_root = os.environ.get("FN_REGISTRY_ROOT")
if env_root:
candidates.append(env_root)
current = os.path.dirname(os.path.abspath(__file__))
while True:
candidates.append(current)
parent = os.path.dirname(current)
if parent == current:
break
current = parent
candidates.append(os.path.expanduser("~/fn_registry"))
for root in candidates:
functions_dir = os.path.join(root, "python", "functions")
if os.path.isdir(os.path.join(functions_dir, "obsidian")):
return functions_dir
raise RuntimeError(
"no se encontró python/functions/obsidian subiendo desde "
f"{os.path.abspath(__file__)}; define FN_REGISTRY_ROOT con la raíz "
"del fn_registry"
)
_FUNCTIONS_DIR = _registry_functions_dir()
if _FUNCTIONS_DIR not in sys.path:
sys.path.insert(0, _FUNCTIONS_DIR)
# Grupo obsidian: CRUD de notas del vault + slugs. El __init__ del paquete
# obsidian es ligero (sin dependencias pesadas), así que se importa directo.
from obsidian import ( # noqa: E402
create_obsidian_note,
list_obsidian_notes,
read_obsidian_note,
slugify_obsidian_name,
update_obsidian_note,
)
def _load_registry_fn(package: str, module_name: str, attr: str):
"""Carga una función del registry por path, sin ejecutar el __init__ del paquete.
Los __init__ de los paquetes infra y core importan TODAS sus funciones, y
algunas arrastran dependencias pesadas (Pillow, etc.) que este service no
necesita. Cargamos el archivo concreto con importlib (mismo patrón que
osint_web). Sigue siendo registry-first: se usa la función del registry sin
reimplementarla, solo se importa de forma quirúrgica.
"""
import importlib.util
file_path = os.path.join(_FUNCTIONS_DIR, package, module_name + ".py")
spec = importlib.util.spec_from_file_location(
f"{package}_{module_name}", file_path
)
if spec is None or spec.loader is None: # pragma: no cover - defensivo
raise ImportError(f"no se pudo cargar {file_path}")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return getattr(module, attr)
# Grupo dav: lectura bulk de colecciones Xandikos (CardDAV/CalDAV).
dav_get_collection = _load_registry_fn("infra", "dav_get_collection", "dav_get_collection")
dav_list_calendars = _load_registry_fn("infra", "dav_list_calendars", "dav_list_calendars")
dav_list_addressbooks = _load_registry_fn(
"infra", "dav_list_addressbooks", "dav_list_addressbooks"
)
dav_collection_ctag = _load_registry_fn("infra", "dav_collection_ctag", "dav_collection_ctag")
# Grupo dav: listado incremental (PROPFIND Depth:1 -> [{href, etag}]) y GET de
# un recurso suelto. Base del sync inverso por etag (/api/sync/dav-pull).
dav_list_resources = _load_registry_fn("infra", "dav_list_resources", "dav_list_resources")
dav_get_resource = _load_registry_fn("infra", "dav_get_resource", "dav_get_resource")
# Grupo dav: escritura (push DB -> Xandikos) y creación de colecciones.
carddav_put_vcard = _load_registry_fn("infra", "carddav_put_vcard", "carddav_put_vcard")
caldav_put_event = _load_registry_fn("infra", "caldav_put_event", "caldav_put_event")
dav_delete_resource = _load_registry_fn(
"infra", "dav_delete_resource", "dav_delete_resource"
)
dav_make_addressbook = _load_registry_fn(
"infra", "dav_make_addressbook", "dav_make_addressbook"
)
dav_make_calendar = _load_registry_fn("infra", "dav_make_calendar", "dav_make_calendar")
# Secretos via pass (credencial Xandikos, nunca hardcodeada).
pass_get_secret = _load_registry_fn("infra", "pass_get_secret", "pass_get_secret")
# Lectura read-only de DuckDB (la conexión de /api/query, separada del writer).
duckdb_query_readonly = _load_registry_fn(
"infra", "duckdb_query_readonly", "duckdb_query_readonly"
)
# Escritura DuckDB del grupo: DDL/DML directo + UPSERT con ownership selectivo.
duckdb_execute = _load_registry_fn("infra", "duckdb_execute", "duckdb_execute")
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"
)
upsert_sentinel_block = _load_registry_fn(
"core", "upsert_sentinel_block", "upsert_sentinel_block"
)
__all__ = [
"create_obsidian_note",
"list_obsidian_notes",
"read_obsidian_note",
"slugify_obsidian_name",
"update_obsidian_note",
"dav_collection_ctag",
"dav_get_collection",
"dav_list_calendars",
"dav_list_addressbooks",
"dav_list_resources",
"dav_get_resource",
"carddav_put_vcard",
"caldav_put_event",
"dav_delete_resource",
"dav_make_addressbook",
"dav_make_calendar",
"pass_get_secret",
"duckdb_query_readonly",
"duckdb_execute",
"duckdb_upsert",
"build_vcard",
"contact_import_key",
"render_markdown_table",
"upsert_sentinel_block",
]