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)
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
---
|
||||
name: build_obsidian_graph
|
||||
kind: function
|
||||
lang: py
|
||||
domain: obsidian
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def build_obsidian_graph(vault_dir: str, include_dangling: bool = True) -> dict"
|
||||
description: "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."
|
||||
tags: [obsidian, graph, vault, nodes, edges, wikilinks, sigma, osint]
|
||||
uses_functions: ["list_obsidian_notes_py_obsidian", "read_obsidian_note_py_obsidian", "slugify_obsidian_name_py_obsidian"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["os", "re"]
|
||||
params:
|
||||
- name: vault_dir
|
||||
desc: "ruta (absoluta o relativa) a la raiz del vault de Obsidian a indexar; se excluyen .obsidian/ y .trash/"
|
||||
- name: include_dangling
|
||||
desc: "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"
|
||||
output: "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)"
|
||||
tested: true
|
||||
tests:
|
||||
- "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"
|
||||
test_file_path: "python/functions/obsidian/build_obsidian_graph_test.py"
|
||||
file_path: "python/functions/obsidian/build_obsidian_graph.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
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):
|
||||
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user