feat(configuration): support multiple vaults in the configuration file (#6)

When multiple vaults are added to the configuration file, the script will prompt 
you to select one at runtime
This commit is contained in:
Nathaniel Landau
2023-01-24 10:32:56 -05:00
committed by GitHub
parent 5abab2ad20
commit 1e4fbcb4e2
23 changed files with 350 additions and 171 deletions

View File

@@ -1,5 +1,5 @@
"""Config module for obsidian frontmatter."""
from obsidian_metadata._config.config import Config
from obsidian_metadata._config.config import Config, VaultConfig
__all__ = ["Config"]
__all__ = ["Config", "VaultConfig"]

View File

@@ -1,61 +1,52 @@
"""Instantiate the configuration object."""
import re
import shutil
from pathlib import Path
from textwrap import dedent
from typing import Any
import questionary
import rich.repr
import tomlkit
import typer
from obsidian_metadata._utils import alerts, vault_validation
from obsidian_metadata._utils import Questions, alerts
from obsidian_metadata._utils.alerts import logger as log
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib # type: ignore [no-redef]
DEFAULT_CONFIG_FILE: Path = Path(__file__).parent / "default.toml"
@rich.repr.auto
class Config:
"""Configuration class."""
"""Representation of a configuration file."""
def __init__(self, config_path: Path = None, vault_path: Path = None) -> None:
self.config_path: Path = self._validate_config_path(Path(config_path))
self.config: dict[str, Any] = self._load_config()
self.config_content: str = self.config_path.read_text()
self.vault_path: Path = self._validate_vault_path(vault_path)
if vault_path is None:
self.config_path: Path = self._validate_config_path(Path(config_path))
self.config: dict[str, Any] = self._load_config()
if self.config == {}:
log.error(f"Configuration file is empty: '{self.config_path}'")
raise typer.Exit(code=1)
else:
self.config_path = None
self.config = {
"command_line_vault": {"path": vault_path, "exclude_paths": [".git", ".obsidian"]}
}
try:
self.exclude_paths: list[Any] = self.config["exclude_paths"]
except KeyError:
self.exclude_paths = []
try:
self.metadata_location: str = self.config["metadata"]["metadata_location"]
except KeyError:
self.metadata_location = "frontmatter"
try:
self.tags_location: str = self.config["metadata"]["tags_location"]
except KeyError:
self.tags_location = "top"
self.vaults: list[VaultConfig] = [
VaultConfig(vault_name=key, vault_config=self.config[key]) for key in self.config
]
except TypeError as e:
log.error(f"Configuration file is invalid: '{self.config_path}'")
raise typer.Exit(code=1) from e
log.debug(f"Loaded configuration from '{self.config_path}'")
log.trace(self.config)
def __rich_repr__(self) -> rich.repr.Result: # pragma: no cover
"""Define rich representation of Vault."""
"""Define rich representation of the Config object."""
yield "config_path", self.config_path
yield "config_content",
yield "vault_path", self.vault_path
yield "metadata_location", self.metadata_location
yield "tags_location", self.tags_location
yield "exclude_paths", self.exclude_paths
yield "vaults", self.vaults
def _validate_config_path(self, config_path: Path | None) -> Path:
"""Load the configuration path."""
@@ -63,7 +54,7 @@ class Config:
config_path = Path(Path.home() / f".{__package__.split('.')[0]}.toml")
if not config_path.exists():
shutil.copy(DEFAULT_CONFIG_FILE, config_path)
self._write_default_config(config_path)
alerts.info(f"Created default configuration file at '{config_path}'")
return config_path.expanduser().resolve()
@@ -71,46 +62,67 @@ class Config:
def _load_config(self) -> dict[str, Any]:
"""Load the configuration file."""
try:
with self.config_path.open("rb") as f:
return tomllib.load(f)
except tomllib.TOMLDecodeError as e:
with open(self.config_path, encoding="utf-8") as fp:
return tomlkit.load(fp)
except tomlkit.exceptions.TOMLKitError as e:
alerts.error(f"Could not parse '{self.config_path}'")
raise typer.Exit(code=1) from e
def _write_default_config(self, path_to_config: Path) -> None:
"""Write the default configuration file when no config file is found."""
vault_path = Questions.ask_for_vault_path()
config_text = f"""\
# Add another vault by replicating this section and changing the name
["Vault 1"] # Name of the vault.
# Path to your obsidian vault
path = "{vault_path}"
# Folders within the vault to ignore when indexing metadata
exclude_paths = [".git", ".obsidian"]"""
path_to_config.write_text(dedent(config_text))
@rich.repr.auto
class VaultConfig:
"""Representation of a vault configuration."""
def __init__(self, vault_name: str, vault_config: dict) -> None:
"""Initialize the vault configuration."""
self.name: str = vault_name
self.config: dict = vault_config
try:
self.path = self._validate_vault_path(self.config["path"])
Path(self.config["path"]).expanduser().resolve()
except KeyError:
self.path = None
try:
self.exclude_paths = self.config["exclude_paths"]
except KeyError:
self.exclude_paths = []
def __rich_repr__(self) -> rich.repr.Result: # pragma: no cover
"""Define rich representation of a vault config."""
yield "name", self.name
yield "config", self.config
yield "path", self.path
yield "exclude_paths", self.exclude_paths
def _validate_vault_path(self, vault_path: Path | None) -> Path:
"""Validate the vault path."""
if vault_path is None:
try:
vault_path = Path(self.config["vault"]).expanduser().resolve()
except KeyError:
vault_path = Path("/I/Do/Not/Exist")
vault_path = Path(vault_path).expanduser().resolve()
if not vault_path.exists(): # pragma: no cover
if not vault_path.exists():
alerts.error(f"Vault path not found: '{vault_path}'")
raise typer.Exit(code=1)
vault_path = questionary.path(
"Enter a path to Obsidian vault:",
only_directories=True,
validate=vault_validation,
).ask()
if vault_path is None:
raise typer.Exit(code=1)
if not vault_path.is_dir():
alerts.error(f"Vault path is not a directory: '{vault_path}'")
raise typer.Exit(code=1)
vault_path = Path(vault_path).expanduser().resolve()
self.write_config_value("vault", str(vault_path))
return vault_path
def write_config_value(self, key: str, value: str | int) -> None:
"""Write a new value to the configuration file.
Args:
key (str): The key to write.
value (str|int): The value to write.
"""
self.config_content = re.sub(
rf"( *{key} = ['\"])[^'\"]*(['\"].*)", rf"\1{value}\2", self.config_content
)
alerts.notice(f"Writing new configuration for '{key}' to '{self.config_path}'")
self.config_path.write_text(self.config_content)

View File

@@ -1,5 +0,0 @@
# Path to your obsidian vault
vault = "/path/to/vault"
# Folders within the vault to ignore when indexing metadata
exclude_paths = [".git", ".obsidian"]