feat: initial scaffold of osint_db (DuckDB source-of-truth service)
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""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_collection_ctag = _load_registry_fn("infra", "dav_collection_ctag", "dav_collection_ctag")
|
||||
|
||||
# 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"
|
||||
)
|
||||
|
||||
# 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",
|
||||
"pass_get_secret",
|
||||
"duckdb_query_readonly",
|
||||
"render_markdown_table",
|
||||
"upsert_sentinel_block",
|
||||
]
|
||||
Reference in New Issue
Block a user