Files
fn_registry/python/functions/obsidian/build_obsidian_graph.md
T
egutierrez a76760edba feat(dav,obsidian): grupo dav completo (CardDAV/CalDAV client + split vcf/ics + import pipelines) + build_obsidian_graph + dav_list_calendars
Funciones reutilizables creadas esta sesion para el sistema self-hosted de contactos/calendario (Xandikos) y la app osint_web:
- grupo dav (infra): split_vcards, split_vevents_to_vcalendars, extract_or_make_uid, carddav_put_vcard, caldav_put_event, dav_list_resources, dav_get_resource, dav_list_calendars
- pipelines: import_vcf_to_carddav, import_ics_to_caldav
- obsidian: build_obsidian_graph (grafo agregado del vault)
2026-06-12 00:43:59 +02:00

5.5 KiB

name, kind, lang, domain, version, purity, signature, description, tags, uses_functions, uses_types, returns, returns_optional, error_type, imports, params, output, tested, tests, test_file_path, file_path
name kind lang domain version purity signature description tags uses_functions uses_types returns returns_optional error_type imports params output tested tests test_file_path file_path
build_obsidian_graph function py obsidian 1.0.0 impure def build_obsidian_graph(vault_dir: str, include_dangling: bool = True) -> dict Construye el grafo agregado (nodos + aristas) de un vault de Obsidian leyendo todas sus notas .md. Cada nota es un nodo tipado (tipo por carpeta o frontmatter, id = slug, label = frontmatter['nombre'] o slug) y cada wikilink ... del cuerpo es una arista dirigida resuelta por slug del ultimo segmento del destino. Los wikilinks rotos se incluyen como nodos fantasma dangling o se descartan segun include_dangling. Compone list_obsidian_notes, read_obsidian_note y slugify_obsidian_name del grupo obsidian. Es la pieza que cierra la frontera 'el grupo obsidian no indexa el grafo agregado'. Base de la vista grafo (sigma.js) de la app osint_web.
obsidian
graph
vault
nodes
edges
wikilinks
sigma
osint
list_obsidian_notes_py_obsidian
read_obsidian_note_py_obsidian
slugify_obsidian_name_py_obsidian
false error_go_core
os
re
name desc
vault_dir ruta (absoluta o relativa) a la raiz del vault de Obsidian a indexar; se excluyen .obsidian/ y .trash/
name desc
include_dangling si True (por defecto) los wikilinks que no resuelven a ninguna nota generan un nodo fantasma con dangling=True y su arista; si False, esos enlaces rotos se descartan
dict con 'nodes' (lista de {id: slug, tipo: str, label: str, frontmatter: dict}; los fantasma anaden dangling=True y frontmatter vacio) y 'edges' (lista de {source: slug, target: slug, kind: str} deduplicada; kind es relacion/lugar/documento segun la seccion ## donde aparece el wikilink, o 'wikilink' por defecto) true
golden grafo del mini-vault con nodos y aristas esperados
resuelve wikilink con acentos maria del mar al slug
el kind de la arista sale de la seccion del cuerpo
dangling marcado con true y excluido con false
el tipo cae a la carpeta si falta en frontmatter
wikilink sintacticamente roto no tumba el grafo
vault inexistente lanza filenotfounderror
python/functions/obsidian/build_obsidian_graph_test.py python/functions/obsidian/build_obsidian_graph.py

Ejemplo

import sys, os
sys.path.insert(0, os.path.join("python", "functions"))
from obsidian.build_obsidian_graph import build_obsidian_graph

graph = build_obsidian_graph("/home/enmanuel/Obsidian/osint")
print(len(graph["nodes"]), "nodos,", len(graph["edges"]), "aristas")
# 1199 nodos (984 reales + 215 fantasma), 618 aristas

# Un nodo tipado listo para sigma.js: color por tipo, label visible.
n = next(x for x in graph["nodes"] if x["tipo"] == "persona")
print(n["id"], n["label"], n["tipo"])   # ana-gomez Ana Gómez persona

# Aristas con su kind (relacion / lugar / documento / wikilink).
for e in graph["edges"][:3]:
    print(e["source"], "->", e["target"], f"({e['kind']})")

Lanzable directo sobre el vault real (imprime conteo de nodos/aristas/dangling):

cd /home/enmanuel/fn_registry
PYTHONPATH=python/functions python/.venv/bin/python3 \
  python/functions/obsidian/build_obsidian_graph.py /home/enmanuel/Obsidian/osint

Cuando usarla

Cuando necesites el grafo entero de un vault de Obsidian de una sola pasada: para pintar una vista de nodos navegable (sigma.js / graphology), para detectar objetivos OSINT aun sin fichar (nodos dangling), o para alimentar el endpoint /api/graph de una app que lee el vault sin BD intermedia. Es el agregado que las funciones atomicas del grupo obsidian (list_obsidian_notes, read_obsidian_note) no daban por si solas.

Gotchas

  • Lee todo el vault de disco (I/O impuro): el grafo refleja el estado de los .md en ese instante; vuelve a llamar para refrescar tras editar notas.
  • El id de un nodo es el slug = nombre de archivo sin .md. Si dos notas en carpetas distintas comparten ese nombre (caso real en el vault osint: dni.md, fotos.md, certificado-digital.md repetidos dentro de cada subcarpeta personas/<slug>/), colapsan al mismo nodo y solo la primera en orden alfabetico sobrevive. Por eso el vault con 1022 .md produce 984 nodos reales (13 grupos de slugs colisionan). Para evitarlo habria que usar el path relativo como id — se dejo el slug por compatibilidad con la resolucion de wikilinks [[slug]].
  • Resolucion de wikilinks por ultimo segmento: [[organizaciones/acme-sl|Acme SL]] resuelve a la nota cuyo slug es acme-sl; el alias (|...) y el ancla (#...) se ignoran. Nombres con acentos/mayusculas ([[María del Mar]]) se slugifican con slugify_obsidian_name antes de buscar, asi que resuelven igual que [[maria-del-mar]].
  • kind por seccion es heuristico: depende del texto del encabezado ## ... mas cercano por encima del wikilink (Relaciones/Relacionado -> relacion, Lugares -> lugar, Documentos -> documento, resto -> wikilink). Un wikilink fuera de cualquier seccion conocida es wikilink.
  • Auto-enlaces ignorados: si una nota se enlaza a si misma, esa arista no se emite.
  • Nodos fantasma (dangling: true) llevan tipo: "desconocido" y frontmatter vacio; no representan un .md en disco. Con include_dangling=False no aparecen ni ellos ni sus aristas.
  • Lanza FileNotFoundError si vault_dir no existe y NotADirectoryError si no es un directorio (heredado de list_obsidian_notes). Una nota individual ilegible se omite sin tumbar el grafo.