cuando termines y verifica que esté todo subido
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,12 @@ from .search_obsidian_notes import search_obsidian_notes
|
||||
from .list_obsidian_vaults import list_obsidian_vaults
|
||||
from .create_obsidian_vault import create_obsidian_vault
|
||||
|
||||
# CRUD de vaults REGISTRADOS en la app de escritorio Obsidian (obsidian.json)
|
||||
from .register_obsidian_vault import register_obsidian_vault
|
||||
from .list_registered_obsidian_vaults import list_registered_obsidian_vaults
|
||||
from .unregister_obsidian_vault import unregister_obsidian_vault
|
||||
from .open_obsidian_vault import open_obsidian_vault
|
||||
|
||||
# Utilidades de migracion / extraccion de subgrafos (grupo obsidian)
|
||||
from .slugify_obsidian_name import slugify_obsidian_name
|
||||
from .extract_obsidian_embeds import extract_obsidian_embeds
|
||||
@@ -34,6 +40,10 @@ __all__ = [
|
||||
"search_obsidian_notes",
|
||||
"list_obsidian_vaults",
|
||||
"create_obsidian_vault",
|
||||
"register_obsidian_vault",
|
||||
"list_registered_obsidian_vaults",
|
||||
"unregister_obsidian_vault",
|
||||
"open_obsidian_vault",
|
||||
"slugify_obsidian_name",
|
||||
"extract_obsidian_embeds",
|
||||
"resolve_obsidian_embed",
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: list_registered_obsidian_vaults
|
||||
kind: function
|
||||
lang: py
|
||||
domain: obsidian
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def list_registered_obsidian_vaults(config_path: str = '') -> list"
|
||||
description: "Lista los vaults que la app de escritorio Obsidian conoce, leyendo la clave 'vaults' de ~/.config/obsidian/obsidian.json. Opera sobre la config de la app, NO inspecciona el filesystem como list_obsidian_vaults. Devuelve una entrada por vault registrado con id, path, open y ts, ordenada por path. Lista vacia si el archivo no existe."
|
||||
tags: [obsidian, vault, list, config, desktop-app, obsidian-json]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["json", "os"]
|
||||
params:
|
||||
- name: config_path
|
||||
desc: "ruta al obsidian.json de la app; vacio usa ~/.config/obsidian/obsidian.json"
|
||||
output: "lista de dicts {'id','path','open','ts'} una por vault registrado, ordenada por path; lista vacia si el archivo no existe o no tiene vaults"
|
||||
tested: true
|
||||
tests:
|
||||
- "lista vaults registrados ordenados por path"
|
||||
- "config inexistente devuelve lista vacia"
|
||||
test_file_path: "python/functions/obsidian/list_registered_obsidian_vaults_test.py"
|
||||
file_path: "python/functions/obsidian/list_registered_obsidian_vaults.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from obsidian import list_registered_obsidian_vaults
|
||||
|
||||
vaults = list_registered_obsidian_vaults(config_path="") # ~/.config/obsidian/obsidian.json
|
||||
for v in vaults:
|
||||
print(v["id"], v["open"], v["path"])
|
||||
# 3f9a1c0b7e2d4a86 True /home/enmanuel/vaults/osint
|
||||
# a1b2c3d4e5f60718 False /home/enmanuel/vaults/personal
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando necesites saber que vaults tiene dados de alta la app de escritorio Obsidian (no los que existan en disco): para auditar, desregistrar uno concreto (`unregister_obsidian_vault`), o comprobar si un vault ya esta registrado antes de abrirlo. Distinta de `list_obsidian_vaults`, que escanea un directorio del filesystem en busca de carpetas con `.obsidian/`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Lee la config de la app** (I/O impuro): refleja el estado del archivo `~/.config/obsidian/obsidian.json` en ese instante, no el filesystem real. Un vault listado aqui puede ya no existir en disco (entrada huerfana).
|
||||
- **Single-instance**: si Obsidian esta abierto y se han registrado vaults despues de arrancar, la lista en disco puede divergir de la que la app tiene en memoria.
|
||||
- El campo `ts` es epoch en milisegundos (no segundos). Convertir con `datetime.fromtimestamp(ts/1000)` si lo necesitas como fecha.
|
||||
- Devuelve lista vacia (no lanza) cuando el archivo no existe — util para arranques en limpio.
|
||||
@@ -0,0 +1,87 @@
|
||||
"""Lista los vaults registrados en la app de escritorio Obsidian.
|
||||
|
||||
Lee el archivo de configuracion de la app (~/.config/obsidian/obsidian.json) y
|
||||
devuelve las entradas de la clave "vaults". NO inspecciona el sistema de archivos
|
||||
del vault — solo refleja lo que la app Obsidian conoce.
|
||||
|
||||
Funcion impura: lee un archivo del disco (la config de la app).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
def _default_config_path() -> str:
|
||||
"""Ruta por defecto del obsidian.json de la app de escritorio Obsidian."""
|
||||
return os.path.expanduser("~/.config/obsidian/obsidian.json")
|
||||
|
||||
|
||||
def _load_config(config_path: str) -> dict:
|
||||
"""Carga obsidian.json. Si no existe devuelve la estructura vacia base."""
|
||||
if not os.path.exists(config_path):
|
||||
return {"vaults": {}}
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"obsidian config is not a JSON object: {config_path}")
|
||||
if "vaults" not in data or not isinstance(data.get("vaults"), dict):
|
||||
data["vaults"] = {}
|
||||
return data
|
||||
|
||||
|
||||
def list_registered_obsidian_vaults(config_path: str = "") -> list:
|
||||
"""Devuelve los vaults registrados en la app Obsidian, ordenados por path.
|
||||
|
||||
Args:
|
||||
config_path: ruta al obsidian.json de la app. Vacio -> default
|
||||
~/.config/obsidian/obsidian.json.
|
||||
|
||||
Returns:
|
||||
Lista de dicts {"id", "path", "open", "ts"}, una por vault registrado,
|
||||
ordenada por "path". Lista vacia si el archivo no existe o no tiene vaults.
|
||||
|
||||
Raises:
|
||||
ValueError: si el obsidian.json existente no es un objeto JSON valido.
|
||||
OSError: si la lectura del archivo falla por I/O.
|
||||
"""
|
||||
cfg_path = config_path or _default_config_path()
|
||||
if not os.path.exists(cfg_path):
|
||||
return []
|
||||
|
||||
data = _load_config(cfg_path)
|
||||
out = []
|
||||
for vid, entry in data["vaults"].items():
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
out.append(
|
||||
{
|
||||
"id": vid,
|
||||
"path": entry.get("path", ""),
|
||||
"open": entry.get("open", False),
|
||||
"ts": entry.get("ts", 0),
|
||||
}
|
||||
)
|
||||
out.sort(key=lambda e: e["path"])
|
||||
return out
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
tmp = tempfile.mkdtemp()
|
||||
cfg = os.path.join(tmp, "obsidian.json")
|
||||
payload = {
|
||||
"extra": "preservar",
|
||||
"vaults": {
|
||||
"aaaaaaaaaaaaaaaa": {"path": "/z/Zeta", "ts": int(time.time() * 1000), "open": True},
|
||||
"bbbbbbbbbbbbbbbb": {"path": "/a/Alpha", "ts": int(time.time() * 1000), "open": False},
|
||||
},
|
||||
}
|
||||
with open(cfg, "w", encoding="utf-8") as f:
|
||||
json.dump(payload, f)
|
||||
|
||||
rows = list_registered_obsidian_vaults(config_path=cfg)
|
||||
assert [r["path"] for r in rows] == ["/a/Alpha", "/z/Zeta"], rows
|
||||
assert list_registered_obsidian_vaults(config_path=os.path.join(tmp, "nope.json")) == []
|
||||
print("list_registered_obsidian_vaults smoke OK")
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Tests para list_registered_obsidian_vaults."""
|
||||
|
||||
import json
|
||||
|
||||
from list_registered_obsidian_vaults import list_registered_obsidian_vaults
|
||||
|
||||
|
||||
def test_lista_vaults_registrados_ordenados_por_path(tmp_path):
|
||||
# Golden path: dos vaults registrados se devuelven ordenados por path.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
with open(cfg, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{
|
||||
"extra": "preservar",
|
||||
"vaults": {
|
||||
"aaaaaaaaaaaaaaaa": {"path": "/z/Zeta", "ts": 200, "open": True},
|
||||
"bbbbbbbbbbbbbbbb": {"path": "/a/Alpha", "ts": 100, "open": False},
|
||||
},
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
rows = list_registered_obsidian_vaults(config_path=cfg)
|
||||
assert len(rows) == 2
|
||||
assert [r["path"] for r in rows] == ["/a/Alpha", "/z/Zeta"]
|
||||
|
||||
alpha = rows[0]
|
||||
assert alpha["id"] == "bbbbbbbbbbbbbbbb"
|
||||
assert alpha["open"] is False
|
||||
assert alpha["ts"] == 100
|
||||
|
||||
|
||||
def test_config_inexistente_devuelve_lista_vacia(tmp_path):
|
||||
# Edge: archivo de config ausente -> lista vacia, sin excepcion.
|
||||
cfg = str(tmp_path / "no_existe.json")
|
||||
assert list_registered_obsidian_vaults(config_path=cfg) == []
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
name: open_obsidian_vault
|
||||
kind: function
|
||||
lang: py
|
||||
domain: obsidian
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def open_obsidian_vault(vault: str, register_if_missing: bool = True, launch: bool = True, config_path: str = '') -> dict"
|
||||
description: "Abre un vault en la app de escritorio Obsidian construyendo el URI obsidian://open?vault=<name> (name = basename del path o el propio nombre, URL-encodeado) y lanzandolo desacoplado via xdg-open. Si recibe una ruta existente no registrada y register_if_missing=True, la registra antes componiendo register_obsidian_vault (open=True). Con launch=False construye el URI sin lanzar nada (util en tests)."
|
||||
tags: [obsidian, vault, open, launch, uri, desktop-app, obsidian-json]
|
||||
uses_functions: ["register_obsidian_vault_py_obsidian"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["os", "subprocess", "urllib.parse"]
|
||||
params:
|
||||
- name: vault
|
||||
desc: "ruta absoluta a un vault o su nombre (basename); si es ruta existente el URI usa su basename"
|
||||
- name: register_if_missing
|
||||
desc: "si True (default) y vault es una ruta existente no registrada, la registra (open=True) antes de abrir"
|
||||
- name: launch
|
||||
desc: "si True (default) lanza la app via xdg-open desacoplado; False solo construye el URI sin lanzar (tests)"
|
||||
- name: config_path
|
||||
desc: "ruta al obsidian.json de la app pasada a register_obsidian_vault; vacio usa ~/.config/obsidian/obsidian.json"
|
||||
output: "dict con vault (arg original), uri (obsidian://open?vault=<name>), name (usado en el URI), launched (bool) y registered_now (bool, True si se registro en esta llamada)"
|
||||
tested: true
|
||||
tests:
|
||||
- "registra si falta y construye uri sin lanzar gui"
|
||||
- "por nombre construye uri sin registrar"
|
||||
test_file_path: "python/functions/obsidian/open_obsidian_vault_test.py"
|
||||
file_path: "python/functions/obsidian/open_obsidian_vault.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from obsidian import open_obsidian_vault
|
||||
|
||||
# Abrir por ruta: registra si falta y lanza Obsidian
|
||||
res = open_obsidian_vault(
|
||||
vault="/home/enmanuel/vaults/osint",
|
||||
register_if_missing=True,
|
||||
launch=True,
|
||||
config_path="",
|
||||
)
|
||||
print(res["uri"]) # obsidian://open?vault=osint
|
||||
print(res["registered_now"])# True si no estaba registrado
|
||||
|
||||
# Solo construir el URI (sin lanzar GUI, p.ej. para inspeccion o tests)
|
||||
res = open_obsidian_vault("Mi Vault", launch=False)
|
||||
print(res["uri"]) # obsidian://open?vault=Mi%20Vault
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras abrir un vault en la app de escritorio Obsidian desde codigo o un agente: tras crear (`create_obsidian_vault`) y registrar (`register_obsidian_vault`) un vault, esta funcion lo abre en un solo paso (registra si hace falta + lanza el URI). Para construir el URI sin lanzar la GUI, usa `launch=False`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Single-instance (lo importante)**: Obsidian es de instancia unica. Si ya hay una instancia corriendo con una config vieja en memoria, el URI `obsidian://open?vault=<name>` puede responder **"unable to find a vault"** para un vault recien registrado, hasta reiniciar la app. Si registras y abres en caliente, reinicia Obsidian para que recargue `obsidian.json`.
|
||||
- **Lanza un proceso externo** (`xdg-open`, I/O impuro) de forma desacoplada (`start_new_session=True`, stdio a DEVNULL): no bloquea ni captura salida. `launch=False` evita lanzar nada.
|
||||
- El nombre del URI es el **basename** del path (Obsidian resuelve vaults por nombre, no por ruta completa). Dos vaults con el mismo basename en rutas distintas pueden colisionar en el URI; Obsidian abrira el que tenga ese nombre en su config.
|
||||
- Necesita un entorno grafico: hereda `DISPLAY`/`WAYLAND_DISPLAY` del entorno. En una sesion headless `xdg-open` puede fallar silenciosamente (el proceso se lanza pero no abre nada).
|
||||
- Si `register_if_missing=True` y el vault es una ruta existente nueva, **escribe la config de la app** (via `register_obsidian_vault`, con su backup `.bak`).
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Abre un vault en la app de escritorio Obsidian via el esquema obsidian://.
|
||||
|
||||
Construye el URI `obsidian://open?vault=<name>` y lanza la app para abrir ese vault.
|
||||
Opcionalmente lo registra antes en la config de la app si recibe una ruta existente
|
||||
no registrada (compone register_obsidian_vault del grupo obsidian).
|
||||
|
||||
Funcion impura: puede escribir la config de la app (al registrar) y lanza un proceso
|
||||
externo (xdg-open) de forma desacoplada para abrir el URI en Obsidian.
|
||||
"""
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import urllib.parse
|
||||
|
||||
from obsidian import register_obsidian_vault
|
||||
|
||||
|
||||
def open_obsidian_vault(
|
||||
vault: str,
|
||||
register_if_missing: bool = True,
|
||||
launch: bool = True,
|
||||
config_path: str = "",
|
||||
) -> dict:
|
||||
"""Abre un vault en la app Obsidian construyendo y lanzando un URI obsidian://.
|
||||
|
||||
Args:
|
||||
vault: ruta absoluta a un vault o su nombre (basename). Si es una ruta
|
||||
existente, el nombre del URI es su basename; si no parece una ruta
|
||||
existente se trata como nombre tal cual.
|
||||
register_if_missing: si True (default) y vault es una ruta existente no
|
||||
registrada en la app, la registra (open=True) antes de abrir.
|
||||
launch: si True (default) lanza la app via `xdg-open <uri>` desacoplado.
|
||||
Si False (util en tests) NO lanza nada, solo construye el URI.
|
||||
config_path: ruta al obsidian.json de la app. Vacio -> default
|
||||
~/.config/obsidian/obsidian.json. Se pasa a register_obsidian_vault.
|
||||
|
||||
Returns:
|
||||
dict con:
|
||||
- vault: el argumento original recibido.
|
||||
- uri: el URI obsidian://open?vault=<name> construido.
|
||||
- name: nombre del vault usado en el URI (basename o el propio vault).
|
||||
- launched: True si se lanzo xdg-open.
|
||||
- registered_now: True si se registro el vault en esta llamada.
|
||||
|
||||
Raises:
|
||||
OSError: si el lanzamiento del proceso o el registro fallan por I/O.
|
||||
"""
|
||||
registered_now = False
|
||||
is_path = os.path.sep in vault or vault.startswith("~")
|
||||
abs_path = os.path.abspath(os.path.expanduser(vault)) if is_path else ""
|
||||
|
||||
# Si es una ruta existente, opcionalmente registrarla y usar su basename.
|
||||
if abs_path and os.path.isdir(abs_path):
|
||||
name = os.path.basename(abs_path.rstrip(os.path.sep))
|
||||
if register_if_missing:
|
||||
res = register_obsidian_vault(abs_path, open=True, config_path=config_path)
|
||||
registered_now = bool(res.get("registered"))
|
||||
elif is_path:
|
||||
# Parece ruta pero no existe: usar su basename como nombre.
|
||||
name = os.path.basename(abs_path.rstrip(os.path.sep))
|
||||
else:
|
||||
# Es un nombre, no una ruta.
|
||||
name = vault
|
||||
|
||||
uri = "obsidian://open?vault=" + urllib.parse.quote(name)
|
||||
|
||||
launched = False
|
||||
if launch:
|
||||
env = dict(os.environ)
|
||||
subprocess.Popen(
|
||||
["xdg-open", uri],
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
env=env,
|
||||
)
|
||||
launched = True
|
||||
|
||||
return {
|
||||
"vault": vault,
|
||||
"uri": uri,
|
||||
"name": name,
|
||||
"launched": launched,
|
||||
"registered_now": registered_now,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import tempfile
|
||||
|
||||
tmp = tempfile.mkdtemp()
|
||||
cfg = os.path.join(tmp, "obsidian.json")
|
||||
vault = os.path.join(tmp, "Mi Vault")
|
||||
os.makedirs(vault, exist_ok=True)
|
||||
|
||||
r = open_obsidian_vault(vault, launch=False, config_path=cfg)
|
||||
assert r["launched"] is False, r
|
||||
assert r["registered_now"] is True, r
|
||||
assert r["name"] == "Mi Vault", r
|
||||
assert r["uri"] == "obsidian://open?vault=Mi%20Vault", r
|
||||
|
||||
# Por nombre, sin lanzar ni registrar.
|
||||
r2 = open_obsidian_vault("MiVaultPorNombre", launch=False, config_path=cfg)
|
||||
assert r2["uri"] == "obsidian://open?vault=MiVaultPorNombre", r2
|
||||
assert r2["registered_now"] is False, r2
|
||||
print("open_obsidian_vault smoke OK")
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Tests para open_obsidian_vault."""
|
||||
|
||||
import json
|
||||
|
||||
from open_obsidian_vault import open_obsidian_vault
|
||||
|
||||
|
||||
def test_registra_si_falta_y_construye_uri_sin_lanzar_gui(tmp_path):
|
||||
# Golden path: ruta existente no registrada -> se registra y se construye el
|
||||
# URI con el basename URL-encodeado, sin lanzar la app (launch=False).
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
vault = tmp_path / "Mi Vault"
|
||||
vault.mkdir()
|
||||
|
||||
res = open_obsidian_vault(str(vault), launch=False, config_path=cfg)
|
||||
assert res["launched"] is False
|
||||
assert res["registered_now"] is True
|
||||
assert res["name"] == "Mi Vault"
|
||||
assert res["uri"] == "obsidian://open?vault=Mi%20Vault"
|
||||
assert res["vault"] == str(vault)
|
||||
|
||||
# Quedo registrado en la config de la app.
|
||||
with open(cfg, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
paths = [e["path"] for e in data["vaults"].values()]
|
||||
assert str(vault) in paths
|
||||
# Registrado con open=True por open_obsidian_vault.
|
||||
entry = next(e for e in data["vaults"].values() if e["path"] == str(vault))
|
||||
assert entry["open"] is True
|
||||
|
||||
|
||||
def test_por_nombre_construye_uri_sin_registrar(tmp_path):
|
||||
# Edge: vault es un nombre (no ruta) -> URI directo, sin tocar la config.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
|
||||
res = open_obsidian_vault("MiVaultPorNombre", launch=False, config_path=cfg)
|
||||
assert res["uri"] == "obsidian://open?vault=MiVaultPorNombre"
|
||||
assert res["name"] == "MiVaultPorNombre"
|
||||
assert res["registered_now"] is False
|
||||
assert res["launched"] is False
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: register_obsidian_vault
|
||||
kind: function
|
||||
lang: py
|
||||
domain: obsidian
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def register_obsidian_vault(vault_path: str, open: bool = False, config_path: str = '') -> dict"
|
||||
description: "Registra un vault en la app de escritorio Obsidian anadiendo su entrada a la clave 'vaults' de ~/.config/obsidian/obsidian.json. Opera sobre la config de la app, NO sobre el filesystem del vault. Idempotente por path: no duplica entradas, solo actualiza el flag 'open' si difiere. Genera id hex de 16 chars (secrets.token_hex(8)) y ts en epoch ms. Hace backup .bak antes de escribir y preserva las demas claves del JSON y los demas vaults."
|
||||
tags: [obsidian, vault, register, config, desktop-app, obsidian-json]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["json", "os", "secrets", "shutil", "time"]
|
||||
params:
|
||||
- name: vault_path
|
||||
desc: "ruta al directorio del vault; se normaliza a ruta absoluta antes de registrar"
|
||||
- name: open
|
||||
desc: "flag 'open' de la entrada (si Obsidian deberia abrirlo al arrancar); default False"
|
||||
- name: config_path
|
||||
desc: "ruta al obsidian.json de la app; vacio usa ~/.config/obsidian/obsidian.json"
|
||||
output: "dict con id (hex 16), path (abs), registered (bool, True si entrada nueva), already (bool, True si ya existia), open (bool final), config_path y backup_path (ruta del .bak o '' si no habia archivo previo)"
|
||||
tested: true
|
||||
tests:
|
||||
- "registra entrada nueva"
|
||||
- "segundo registro mismo path no duplica y devuelve already"
|
||||
- "actualiza flag open de entrada existente"
|
||||
- "preserva claves extra de nivel superior"
|
||||
- "crea config y directorios si no existe"
|
||||
- "hace backup bak antes de sobreescribir"
|
||||
test_file_path: "python/functions/obsidian/register_obsidian_vault_test.py"
|
||||
file_path: "python/functions/obsidian/register_obsidian_vault.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from obsidian import register_obsidian_vault
|
||||
|
||||
res = register_obsidian_vault(
|
||||
vault_path="/home/enmanuel/vaults/osint",
|
||||
open=True,
|
||||
config_path="", # usa ~/.config/obsidian/obsidian.json
|
||||
)
|
||||
print(res["id"], res["registered"], res["already"])
|
||||
# 3f9a1c0b7e2d4a86 True False (primera vez)
|
||||
# 3f9a1c0b7e2d4a86 False True (segunda vez, ya registrado)
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras que la app de escritorio Obsidian "conozca" un vault y lo muestre en su selector de vaults (la pantalla de bienvenida / `Open another vault`). Usala despues de `create_obsidian_vault` (que solo crea la carpeta en disco) para dar de alta ese vault en la app. Es el paso previo natural a `open_obsidian_vault`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **Escribe la config de la app** (I/O impuro) en `~/.config/obsidian/obsidian.json` y crea un backup `.bak` antes de sobreescribir. Si la entrada ya existe y nada cambia, NO reescribe ni genera backup.
|
||||
- **NO crea la carpeta del vault**: solo registra la entrada. Si la ruta no existe en disco, Obsidian la mostrara pero no podra abrirla. Usa `create_obsidian_vault` para crear la carpeta.
|
||||
- **Single-instance**: si Obsidian ya esta corriendo, tiene la lista de vaults cargada en memoria; el vault recien registrado puede no aparecer hasta reiniciar la app.
|
||||
- Idempotente **por path absoluto**: dos rutas que resuelven al mismo path absoluto se consideran el mismo vault.
|
||||
- Preserva las demas claves de nivel superior del JSON (`appVersionLastUsed`, `updateDisabled`, etc.) y los demas vaults; solo toca la entrada de este vault.
|
||||
@@ -0,0 +1,154 @@
|
||||
"""Registra un vault en la app de escritorio Obsidian.
|
||||
|
||||
Opera sobre el archivo de configuracion de la app (~/.config/obsidian/obsidian.json),
|
||||
NO sobre el sistema de archivos del vault. Anade (o actualiza) la entrada del vault
|
||||
en la clave "vaults" de ese JSON para que Obsidian lo conozca en su lista de vaults.
|
||||
|
||||
Funcion impura: lee y escribe el archivo de configuracion de la app y crea un
|
||||
backup .bak antes de sobreescribir. Idempotente: si ya existe una entrada con ese
|
||||
path no la duplica, solo actualiza su flag "open" si difiere.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import secrets
|
||||
import shutil
|
||||
import time
|
||||
|
||||
|
||||
def _default_config_path() -> str:
|
||||
"""Ruta por defecto del obsidian.json de la app de escritorio Obsidian."""
|
||||
return os.path.expanduser("~/.config/obsidian/obsidian.json")
|
||||
|
||||
|
||||
def _load_config(config_path: str) -> dict:
|
||||
"""Carga obsidian.json. Si no existe devuelve la estructura vacia base."""
|
||||
if not os.path.exists(config_path):
|
||||
return {"vaults": {}}
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"obsidian config is not a JSON object: {config_path}")
|
||||
if "vaults" not in data or not isinstance(data.get("vaults"), dict):
|
||||
data["vaults"] = {}
|
||||
return data
|
||||
|
||||
|
||||
def _save_config(config_path: str, data: dict) -> str:
|
||||
"""Escribe obsidian.json haciendo backup .bak previo si ya existia.
|
||||
|
||||
Crea los directorios padre que falten. Devuelve la ruta del backup creado
|
||||
(cadena vacia si no habia archivo previo que respaldar).
|
||||
"""
|
||||
parent = os.path.dirname(config_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
|
||||
backup_path = ""
|
||||
if os.path.exists(config_path):
|
||||
backup_path = config_path + ".bak"
|
||||
shutil.copy2(config_path, backup_path)
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return backup_path
|
||||
|
||||
|
||||
def register_obsidian_vault(
|
||||
vault_path: str,
|
||||
open: bool = False,
|
||||
config_path: str = "",
|
||||
) -> dict:
|
||||
"""Registra un vault en la lista de vaults de la app Obsidian.
|
||||
|
||||
Args:
|
||||
vault_path: ruta al directorio del vault. Se normaliza a ruta absoluta.
|
||||
open: valor del flag "open" de la entrada del vault (si Obsidian deberia
|
||||
abrirlo al arrancar). Por defecto False.
|
||||
config_path: ruta al obsidian.json de la app. Vacio -> default
|
||||
~/.config/obsidian/obsidian.json.
|
||||
|
||||
Returns:
|
||||
dict con:
|
||||
- id: id hex de 16 chars de la entrada (existente o recien creada).
|
||||
- path: ruta absoluta normalizada del vault.
|
||||
- registered: True si se creo una entrada nueva.
|
||||
- already: True si ya existia una entrada con ese path.
|
||||
- open: flag "open" final de la entrada.
|
||||
- config_path: ruta del obsidian.json usado.
|
||||
- backup_path: ruta del backup .bak escrito (vacio si no habia archivo previo).
|
||||
|
||||
Raises:
|
||||
ValueError: si el obsidian.json existente no es un objeto JSON valido.
|
||||
OSError: si la lectura/escritura del archivo falla por I/O.
|
||||
"""
|
||||
cfg_path = config_path or _default_config_path()
|
||||
abs_path = os.path.abspath(os.path.expanduser(vault_path))
|
||||
|
||||
data = _load_config(cfg_path)
|
||||
vaults = data["vaults"]
|
||||
|
||||
# Idempotencia: buscar entrada existente por path.
|
||||
existing_id = None
|
||||
for vid, entry in vaults.items():
|
||||
if isinstance(entry, dict) and entry.get("path") == abs_path:
|
||||
existing_id = vid
|
||||
break
|
||||
|
||||
if existing_id is not None:
|
||||
entry = vaults[existing_id]
|
||||
changed = entry.get("open") != open
|
||||
if changed:
|
||||
entry["open"] = open
|
||||
backup_path = _save_config(cfg_path, data) if changed else ""
|
||||
return {
|
||||
"id": existing_id,
|
||||
"path": abs_path,
|
||||
"registered": False,
|
||||
"already": True,
|
||||
"open": entry.get("open", open),
|
||||
"config_path": cfg_path,
|
||||
"backup_path": backup_path,
|
||||
}
|
||||
|
||||
# Entrada nueva.
|
||||
new_id = secrets.token_hex(8)
|
||||
while new_id in vaults:
|
||||
new_id = secrets.token_hex(8)
|
||||
vaults[new_id] = {
|
||||
"path": abs_path,
|
||||
"ts": int(time.time() * 1000),
|
||||
"open": open,
|
||||
}
|
||||
backup_path = _save_config(cfg_path, data)
|
||||
return {
|
||||
"id": new_id,
|
||||
"path": abs_path,
|
||||
"registered": True,
|
||||
"already": False,
|
||||
"open": open,
|
||||
"config_path": cfg_path,
|
||||
"backup_path": backup_path,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import tempfile
|
||||
|
||||
tmp = tempfile.mkdtemp()
|
||||
cfg = os.path.join(tmp, "obsidian.json")
|
||||
vault = os.path.join(tmp, "MiVault")
|
||||
os.makedirs(vault, exist_ok=True)
|
||||
|
||||
r1 = register_obsidian_vault(vault, open=True, config_path=cfg)
|
||||
assert r1["registered"] is True and r1["already"] is False, r1
|
||||
assert r1["open"] is True, r1
|
||||
assert len(r1["id"]) == 16, r1
|
||||
|
||||
r2 = register_obsidian_vault(vault, open=True, config_path=cfg)
|
||||
assert r2["already"] is True and r2["registered"] is False, r2
|
||||
assert r2["id"] == r1["id"], (r1, r2)
|
||||
assert os.path.isfile(cfg + ".bak") is False or True # backup solo si cambio
|
||||
|
||||
print("register_obsidian_vault smoke OK")
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Tests para register_obsidian_vault."""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
from register_obsidian_vault import register_obsidian_vault
|
||||
|
||||
|
||||
def _read(cfg):
|
||||
with open(cfg, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def test_registra_entrada_nueva(tmp_path):
|
||||
# Golden path: registra un vault nuevo en una config inexistente.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
vault = tmp_path / "MiVault"
|
||||
vault.mkdir()
|
||||
|
||||
res = register_obsidian_vault(str(vault), open=True, config_path=cfg)
|
||||
assert res["registered"] is True
|
||||
assert res["already"] is False
|
||||
assert res["open"] is True
|
||||
assert len(res["id"]) == 16
|
||||
assert res["path"] == str(vault)
|
||||
|
||||
data = _read(cfg)
|
||||
assert res["id"] in data["vaults"]
|
||||
entry = data["vaults"][res["id"]]
|
||||
assert entry["path"] == str(vault)
|
||||
assert entry["open"] is True
|
||||
assert isinstance(entry["ts"], int) and entry["ts"] > 0
|
||||
|
||||
|
||||
def test_segundo_registro_mismo_path_no_duplica_y_devuelve_already(tmp_path):
|
||||
# Edge: idempotencia por path. La segunda llamada no crea otra entrada.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
vault = tmp_path / "MiVault"
|
||||
vault.mkdir()
|
||||
|
||||
r1 = register_obsidian_vault(str(vault), open=True, config_path=cfg)
|
||||
r2 = register_obsidian_vault(str(vault), open=True, config_path=cfg)
|
||||
|
||||
assert r2["already"] is True
|
||||
assert r2["registered"] is False
|
||||
assert r2["id"] == r1["id"]
|
||||
|
||||
data = _read(cfg)
|
||||
assert len(data["vaults"]) == 1
|
||||
|
||||
|
||||
def test_actualiza_flag_open_de_entrada_existente(tmp_path):
|
||||
# Edge: misma ruta pero flag open distinto -> actualiza y reescribe.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
vault = tmp_path / "MiVault"
|
||||
vault.mkdir()
|
||||
|
||||
register_obsidian_vault(str(vault), open=False, config_path=cfg)
|
||||
r2 = register_obsidian_vault(str(vault), open=True, config_path=cfg)
|
||||
|
||||
assert r2["already"] is True
|
||||
assert r2["open"] is True
|
||||
|
||||
data = _read(cfg)
|
||||
assert data["vaults"][r2["id"]]["open"] is True
|
||||
|
||||
|
||||
def test_preserva_claves_extra_de_nivel_superior(tmp_path):
|
||||
# Edge: claves ajenas a "vaults" deben sobrevivir a la escritura.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
with open(cfg, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{
|
||||
"appVersionLastUsed": "1.5.3",
|
||||
"updateDisabled": True,
|
||||
"vaults": {
|
||||
"0000000000000000": {"path": "/otro/Vault", "ts": 111, "open": False}
|
||||
},
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
vault = tmp_path / "Nuevo"
|
||||
vault.mkdir()
|
||||
register_obsidian_vault(str(vault), open=True, config_path=cfg)
|
||||
|
||||
data = _read(cfg)
|
||||
assert data["appVersionLastUsed"] == "1.5.3"
|
||||
assert data["updateDisabled"] is True
|
||||
# La entrada previa de otro vault se conserva.
|
||||
assert "0000000000000000" in data["vaults"]
|
||||
assert len(data["vaults"]) == 2
|
||||
|
||||
|
||||
def test_crea_config_y_directorios_si_no_existe(tmp_path):
|
||||
# Edge: la config y sus directorios padre no existen y se crean.
|
||||
cfg = str(tmp_path / "sub" / "dir" / "obsidian.json")
|
||||
vault = tmp_path / "MiVault"
|
||||
vault.mkdir()
|
||||
|
||||
res = register_obsidian_vault(str(vault), config_path=cfg)
|
||||
assert res["registered"] is True
|
||||
assert os.path.isfile(cfg)
|
||||
# Sin archivo previo no hay backup.
|
||||
assert res["backup_path"] == ""
|
||||
|
||||
|
||||
def test_hace_backup_bak_antes_de_sobreescribir(tmp_path):
|
||||
# Edge: una segunda escritura sobre config existente genera backup .bak.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
v1 = tmp_path / "V1"
|
||||
v1.mkdir()
|
||||
v2 = tmp_path / "V2"
|
||||
v2.mkdir()
|
||||
|
||||
register_obsidian_vault(str(v1), config_path=cfg) # crea config
|
||||
res = register_obsidian_vault(str(v2), config_path=cfg) # ahora si hay backup
|
||||
|
||||
assert res["backup_path"] == cfg + ".bak"
|
||||
assert os.path.isfile(cfg + ".bak")
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: unregister_obsidian_vault
|
||||
kind: function
|
||||
lang: py
|
||||
domain: obsidian
|
||||
version: "1.0.0"
|
||||
purity: impure
|
||||
signature: "def unregister_obsidian_vault(vault_ref: str, config_path: str = '') -> dict"
|
||||
description: "Desregistra un vault de la app de escritorio Obsidian quitando su entrada de la clave 'vaults' de ~/.config/obsidian/obsidian.json. Opera sobre la config de la app: NO borra la carpeta del vault en disco, solo hace que Obsidian deje de conocerlo. Acepta vault_ref como id exacto (hex 16) o como ruta (se normaliza a absoluta y se compara con path). Hace backup .bak antes de escribir y preserva el resto del JSON."
|
||||
tags: [obsidian, vault, unregister, config, desktop-app, obsidian-json]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: ["json", "os", "shutil"]
|
||||
params:
|
||||
- name: vault_ref
|
||||
desc: "id exacto de la entrada (hex 16 chars) O ruta al vault (se normaliza a absoluta y se compara con 'path'); primero intenta match por id, luego por path"
|
||||
- name: config_path
|
||||
desc: "ruta al obsidian.json de la app; vacio usa ~/.config/obsidian/obsidian.json"
|
||||
output: "dict con removed (bool), id (de la entrada quitada o ''), path (de la entrada quitada o ''), config_path y backup_path (ruta del .bak o '')"
|
||||
tested: true
|
||||
tests:
|
||||
- "desregistra por path"
|
||||
- "desregistra por id"
|
||||
- "preserva resto del json al quitar entrada"
|
||||
- "ref inexistente devuelve removed false"
|
||||
test_file_path: "python/functions/obsidian/unregister_obsidian_vault_test.py"
|
||||
file_path: "python/functions/obsidian/unregister_obsidian_vault.py"
|
||||
---
|
||||
|
||||
## Ejemplo
|
||||
|
||||
```python
|
||||
import sys, os
|
||||
sys.path.insert(0, os.path.join("python", "functions"))
|
||||
from obsidian import unregister_obsidian_vault
|
||||
|
||||
# Por ruta (se normaliza a absoluta)
|
||||
res = unregister_obsidian_vault("/home/enmanuel/vaults/viejo", config_path="")
|
||||
print(res["removed"], res["id"]) # True 3f9a1c0b7e2d4a86
|
||||
|
||||
# O por id exacto
|
||||
unregister_obsidian_vault("a1b2c3d4e5f60718", config_path="")
|
||||
```
|
||||
|
||||
## Cuando usarla
|
||||
|
||||
Cuando quieras que la app de escritorio Obsidian deje de mostrar un vault en su selector sin tocar los archivos del vault en disco: limpiar entradas obsoletas, quitar un vault movido a otra ruta, o sanear `obsidian.json`. Para listar las entradas y obtener sus ids usa `list_registered_obsidian_vaults`.
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **NO borra la carpeta del vault**: solo elimina la entrada de la config de la app. Los archivos `.md` y el `.obsidian/` del vault siguen en disco.
|
||||
- **Escribe la config de la app** (I/O impuro) y crea backup `.bak` antes de sobreescribir, pero **solo si encontro la entrada** (`removed=True`). Si no la encuentra, no escribe ni hace backup.
|
||||
- Resolucion de `vault_ref`: primero intenta match exacto por id; si no, normaliza el ref a ruta absoluta y compara con `path`. Un id que coincide por azar con un path nunca pasara: el match por id va primero.
|
||||
- **Single-instance**: si Obsidian esta corriendo, sigue teniendo el vault en memoria hasta reiniciar; el desregistro solo afecta al archivo de config.
|
||||
- Preserva las demas claves de nivel superior del JSON y los demas vaults.
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Desregistra un vault de la app de escritorio Obsidian.
|
||||
|
||||
Quita la entrada del vault de la clave "vaults" del archivo de configuracion de la
|
||||
app (~/.config/obsidian/obsidian.json). NO borra la carpeta del vault en disco: solo
|
||||
hace que la app Obsidian deje de conocerlo. Preserva el resto del JSON intacto.
|
||||
|
||||
Funcion impura: lee y escribe la config de la app y hace backup .bak antes de
|
||||
sobreescribir.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
|
||||
|
||||
def _default_config_path() -> str:
|
||||
"""Ruta por defecto del obsidian.json de la app de escritorio Obsidian."""
|
||||
return os.path.expanduser("~/.config/obsidian/obsidian.json")
|
||||
|
||||
|
||||
def _load_config(config_path: str) -> dict:
|
||||
"""Carga obsidian.json. Si no existe devuelve la estructura vacia base."""
|
||||
if not os.path.exists(config_path):
|
||||
return {"vaults": {}}
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
if not isinstance(data, dict):
|
||||
raise ValueError(f"obsidian config is not a JSON object: {config_path}")
|
||||
if "vaults" not in data or not isinstance(data.get("vaults"), dict):
|
||||
data["vaults"] = {}
|
||||
return data
|
||||
|
||||
|
||||
def _save_config(config_path: str, data: dict) -> str:
|
||||
"""Escribe obsidian.json haciendo backup .bak previo si ya existia.
|
||||
|
||||
Devuelve la ruta del backup creado (cadena vacia si no habia archivo previo).
|
||||
"""
|
||||
parent = os.path.dirname(config_path)
|
||||
if parent:
|
||||
os.makedirs(parent, exist_ok=True)
|
||||
|
||||
backup_path = ""
|
||||
if os.path.exists(config_path):
|
||||
backup_path = config_path + ".bak"
|
||||
shutil.copy2(config_path, backup_path)
|
||||
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
return backup_path
|
||||
|
||||
|
||||
def unregister_obsidian_vault(vault_ref: str, config_path: str = "") -> dict:
|
||||
"""Quita un vault de la lista de vaults conocidos por la app Obsidian.
|
||||
|
||||
Args:
|
||||
vault_ref: id exacto de la entrada (hex de 16 chars) O una ruta al vault.
|
||||
Si parece una ruta se normaliza a absoluta y se compara con "path".
|
||||
Primero se intenta match por id; si no hay, por path.
|
||||
config_path: ruta al obsidian.json de la app. Vacio -> default
|
||||
~/.config/obsidian/obsidian.json.
|
||||
|
||||
Returns:
|
||||
dict con:
|
||||
- removed: True si se quito una entrada, False si no se encontro.
|
||||
- id: id de la entrada quitada ("" si no se encontro).
|
||||
- path: path de la entrada quitada ("" si no se encontro).
|
||||
- config_path: ruta del obsidian.json usado.
|
||||
- backup_path: ruta del backup .bak escrito ("" si no se escribio nada).
|
||||
|
||||
Raises:
|
||||
ValueError: si el obsidian.json existente no es un objeto JSON valido.
|
||||
OSError: si la lectura/escritura del archivo falla por I/O.
|
||||
"""
|
||||
cfg_path = config_path or _default_config_path()
|
||||
not_found = {
|
||||
"removed": False,
|
||||
"id": "",
|
||||
"path": "",
|
||||
"config_path": cfg_path,
|
||||
"backup_path": "",
|
||||
}
|
||||
|
||||
if not os.path.exists(cfg_path):
|
||||
return not_found
|
||||
|
||||
data = _load_config(cfg_path)
|
||||
vaults = data["vaults"]
|
||||
|
||||
target_id = None
|
||||
|
||||
# 1) Match directo por id.
|
||||
if vault_ref in vaults:
|
||||
target_id = vault_ref
|
||||
else:
|
||||
# 2) Match por path normalizado.
|
||||
abs_ref = os.path.abspath(os.path.expanduser(vault_ref))
|
||||
for vid, entry in vaults.items():
|
||||
if isinstance(entry, dict) and entry.get("path") == abs_ref:
|
||||
target_id = vid
|
||||
break
|
||||
|
||||
if target_id is None:
|
||||
return not_found
|
||||
|
||||
removed_path = ""
|
||||
entry = vaults.get(target_id)
|
||||
if isinstance(entry, dict):
|
||||
removed_path = entry.get("path", "")
|
||||
del vaults[target_id]
|
||||
|
||||
backup_path = _save_config(cfg_path, data)
|
||||
return {
|
||||
"removed": True,
|
||||
"id": target_id,
|
||||
"path": removed_path,
|
||||
"config_path": cfg_path,
|
||||
"backup_path": backup_path,
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
tmp = tempfile.mkdtemp()
|
||||
cfg = os.path.join(tmp, "obsidian.json")
|
||||
payload = {
|
||||
"extra": "preservar",
|
||||
"vaults": {
|
||||
"aaaaaaaaaaaaaaaa": {"path": "/a/Alpha", "ts": int(time.time() * 1000), "open": True},
|
||||
"bbbbbbbbbbbbbbbb": {"path": "/b/Beta", "ts": int(time.time() * 1000), "open": False},
|
||||
},
|
||||
}
|
||||
with open(cfg, "w", encoding="utf-8") as f:
|
||||
json.dump(payload, f)
|
||||
|
||||
r = unregister_obsidian_vault("/a/Alpha", config_path=cfg)
|
||||
assert r["removed"] is True and r["id"] == "aaaaaaaaaaaaaaaa", r
|
||||
|
||||
r2 = unregister_obsidian_vault("bbbbbbbbbbbbbbbb", config_path=cfg)
|
||||
assert r2["removed"] is True and r2["path"] == "/b/Beta", r2
|
||||
|
||||
with open(cfg, "r", encoding="utf-8") as f:
|
||||
final = json.load(f)
|
||||
assert final["extra"] == "preservar", final
|
||||
assert final["vaults"] == {}, final
|
||||
|
||||
assert unregister_obsidian_vault("nope", config_path=cfg)["removed"] is False
|
||||
print("unregister_obsidian_vault smoke OK")
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Tests para unregister_obsidian_vault."""
|
||||
|
||||
import json
|
||||
|
||||
from unregister_obsidian_vault import unregister_obsidian_vault
|
||||
|
||||
|
||||
def _write_cfg(cfg):
|
||||
with open(cfg, "w", encoding="utf-8") as f:
|
||||
json.dump(
|
||||
{
|
||||
"extra": "preservar",
|
||||
"appVersionLastUsed": "1.5.3",
|
||||
"vaults": {
|
||||
"aaaaaaaaaaaaaaaa": {"path": "/a/Alpha", "ts": 100, "open": True},
|
||||
"bbbbbbbbbbbbbbbb": {"path": "/b/Beta", "ts": 200, "open": False},
|
||||
},
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
|
||||
def _read(cfg):
|
||||
with open(cfg, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def test_desregistra_por_path(tmp_path):
|
||||
# Golden path: quita la entrada cuya 'path' coincide con la ruta dada.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
_write_cfg(cfg)
|
||||
|
||||
res = unregister_obsidian_vault("/a/Alpha", config_path=cfg)
|
||||
assert res["removed"] is True
|
||||
assert res["id"] == "aaaaaaaaaaaaaaaa"
|
||||
assert res["path"] == "/a/Alpha"
|
||||
assert res["backup_path"] == cfg + ".bak"
|
||||
|
||||
data = _read(cfg)
|
||||
assert "aaaaaaaaaaaaaaaa" not in data["vaults"]
|
||||
assert "bbbbbbbbbbbbbbbb" in data["vaults"]
|
||||
|
||||
|
||||
def test_desregistra_por_id(tmp_path):
|
||||
# Edge: quita la entrada por su id hex exacto.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
_write_cfg(cfg)
|
||||
|
||||
res = unregister_obsidian_vault("bbbbbbbbbbbbbbbb", config_path=cfg)
|
||||
assert res["removed"] is True
|
||||
assert res["id"] == "bbbbbbbbbbbbbbbb"
|
||||
assert res["path"] == "/b/Beta"
|
||||
|
||||
data = _read(cfg)
|
||||
assert "bbbbbbbbbbbbbbbb" not in data["vaults"]
|
||||
|
||||
|
||||
def test_preserva_resto_del_json_al_quitar_entrada(tmp_path):
|
||||
# Edge: las demas claves y vaults sobreviven al desregistro.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
_write_cfg(cfg)
|
||||
|
||||
unregister_obsidian_vault("/a/Alpha", config_path=cfg)
|
||||
|
||||
data = _read(cfg)
|
||||
assert data["extra"] == "preservar"
|
||||
assert data["appVersionLastUsed"] == "1.5.3"
|
||||
assert "bbbbbbbbbbbbbbbb" in data["vaults"]
|
||||
assert len(data["vaults"]) == 1
|
||||
|
||||
|
||||
def test_ref_inexistente_devuelve_removed_false(tmp_path):
|
||||
# Error path: ni id ni path coinciden -> removed False, sin reescritura.
|
||||
cfg = str(tmp_path / "obsidian.json")
|
||||
_write_cfg(cfg)
|
||||
|
||||
res = unregister_obsidian_vault("/no/existe", config_path=cfg)
|
||||
assert res["removed"] is False
|
||||
assert res["id"] == ""
|
||||
assert res["backup_path"] == ""
|
||||
|
||||
data = _read(cfg)
|
||||
assert len(data["vaults"]) == 2
|
||||
Reference in New Issue
Block a user