"""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 /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") # 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", "render_markdown_table", "upsert_sentinel_block", ]