feat: initial application release

This commit is contained in:
Nathaniel Landau
2022-12-23 04:10:08 +00:00
parent 35717e0760
commit b7bcf74926
78 changed files with 15508 additions and 0 deletions

View File

@@ -0,0 +1,302 @@
"""Obsidian vault representation."""
import re
import shutil
from pathlib import Path
import rich.repr
from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.prompt import Confirm
from rich.table import Table
from obsidian_metadata._config import Config
from obsidian_metadata._utils import alerts
from obsidian_metadata._utils.alerts import logger as log
from obsidian_metadata.models import Note, VaultMetadata
@rich.repr.auto
class Vault:
"""Representation of the Obsidian vault.
Attributes:
vault (Path): Path to the vault.
dry_run (bool): Whether to perform a dry run.
backup_path (Path): Path to the backup of the vault.
new_vault (Path): Path to a new vault.
notes (list[Note]): List of all notes in the vault.
"""
def __init__(self, config: Config, dry_run: bool = False, path_filter: str = None):
self.vault_path: Path = config.vault_path
self.dry_run: bool = dry_run
self.backup_path: Path = self.vault_path.parent / f"{self.vault_path.name}.bak"
self.new_vault_path: Path = self.vault_path.parent / f"{self.vault_path.name}.new"
self.exclude_paths: list[Path] = []
self.metadata = VaultMetadata()
for p in config.exclude_paths:
self.exclude_paths.append(Path(self.vault_path / p))
self.path_filter = path_filter
self.note_paths = self._find_markdown_notes(path_filter)
with Progress(
SpinnerColumn(),
TextColumn("[progress.description]{task.description}"),
transient=True,
) as progress:
progress.add_task(description="Processing notes...", total=None)
self.notes: list[Note] = [
Note(note_path=p, dry_run=self.dry_run) for p in self.note_paths
]
for _note in self.notes:
self.metadata.add_metadata(_note.frontmatter.dict)
self.metadata.add_metadata(_note.inline_metadata.dict)
self.metadata.add_metadata({_note.inline_tags.metadata_key: _note.inline_tags.list})
def __rich_repr__(self) -> rich.repr.Result:
"""Define rich representation of Vault."""
yield "vault_path", self.vault_path
yield "dry_run", self.dry_run
yield "backup_path", self.backup_path
yield "new_vault", self.new_vault_path
yield "num_notes", self.num_notes()
yield "exclude_paths", self.exclude_paths
def _find_markdown_notes(self, path_filter: str = None) -> list[Path]:
"""Build list of all markdown files in the vault.
Args:
path_filter (str, optional): Regex to filter notes by path.
Returns:
list[Path]: List of paths to all matching files in the vault.
"""
notes_list = [
p.resolve()
for p in self.vault_path.glob("**/*")
if p.suffix in [".md", ".MD", ".markdown", ".MARKDOWN"]
and not any(item in p.parents for item in self.exclude_paths)
]
if path_filter is not None:
notes_list = [
p for p in notes_list if re.search(path_filter, str(p.relative_to(self.vault_path)))
]
return notes_list
def backup(self) -> None:
"""Backup the vault."""
log.debug("Backing up vault")
if self.dry_run:
alerts.dryrun(f"Backup up vault to: {self.backup_path}")
return
try:
shutil.copytree(self.vault_path, self.backup_path)
except FileExistsError: # pragma: no cover
log.debug("Backup already exists")
if not Confirm.ask("Vault backup already exists. Overwrite?"):
alerts.info("Exiting backup not overwritten.")
return
log.debug("Overwriting backup")
shutil.rmtree(self.backup_path)
shutil.copytree(self.vault_path, self.backup_path)
alerts.success(f"Vault backed up to: {self.backup_path}")
def contains_inline_tag(self, tag: str, is_regex: bool = False) -> bool:
"""Check if vault contains the given inline tag.
Args:
tag (str): Tag to check for.
is_regex (bool, optional): Whether to use regex to match tag.
Returns:
bool: True if tag is found in vault.
"""
return any(_note.contains_inline_tag(tag) for _note in self.notes)
def contains_metadata(self, key: str, value: str = None, is_regex: bool = False) -> bool:
"""Check if vault contains the given metadata.
Args:
key (str): Key to check for. If value is None, will check vault for key.
value (str, optional): Value to check for.
is_regex (bool, optional): Whether to use regex to match key/value.
Returns:
bool: True if tag is found in vault.
"""
if value is None:
return self.metadata.contains(key, is_regex=is_regex)
return self.metadata.contains(key, value, is_regex=is_regex)
def delete_backup(self) -> None:
"""Delete the vault backup."""
log.debug("Deleting vault backup")
if self.backup_path.exists() and self.dry_run is False:
shutil.rmtree(self.backup_path)
alerts.success("Backup deleted")
elif self.backup_path.exists() and self.dry_run is True:
alerts.dryrun("Delete backup")
else:
alerts.info("No backup found")
def delete_inline_tag(self, tag: str) -> bool:
"""Delete an inline tag in the vault.
Args:
tag (str): Tag to delete.
Returns:
bool: True if tag was deleted.
"""
changes = False
for _note in self.notes:
if _note.delete_inline_tag(tag):
changes = True
if changes:
self.metadata.delete(self.notes[0].inline_tags.metadata_key, tag)
return True
return False
def delete_metadata(self, key: str, value: str = None) -> int:
"""Delete metadata in the vault.
Args:
key (str): Key to delete. Regex is supported
value (str, optional): Value to delete. Regex is supported
Returns:
int: Number of notes that had metadata deleted.
"""
num_changed = 0
for _note in self.notes:
if _note.delete_metadata(key, value):
num_changed += 1
if num_changed > 0:
self.metadata.delete(key, value)
return num_changed
return num_changed
def get_changed_notes(self) -> list[Note]:
"""Returns a list of notes that have changes.
Returns:
list[Note]: List of notes that have changes.
"""
changed_notes = []
for _note in self.notes:
if _note.has_changes():
changed_notes.append(_note)
changed_notes = sorted(changed_notes, key=lambda x: x.note_path)
return changed_notes
def info(self) -> None:
"""Print information about the vault."""
log.debug("Printing vault info")
table = Table(title="Vault Info", show_header=False)
table.add_row("Vault", str(self.vault_path))
table.add_row("Notes being edited", str(self.num_notes()))
table.add_row("Notes excluded from editing", str(self.num_excluded_notes()))
if self.backup_path.exists():
table.add_row("Backup path", str(self.backup_path))
else:
table.add_row("Backup", "None")
table.add_row("Active path filter", str(self.path_filter))
table.add_row("Notes with updates", str(len(self.get_changed_notes())))
Console().print(table)
def list_editable_notes(self) -> None:
"""Print a list of notes within the scope that are being edited."""
for _note in self.notes:
print(_note.note_path.relative_to(self.vault_path))
def num_excluded_notes(self) -> int:
"""Count number of excluded notes."""
excluded_notes = [
p.resolve()
for p in self.vault_path.glob("**/*")
if p.suffix in [".md", ".MD", ".markdown", ".MARKDOWN"] and p not in self.note_paths
]
return len(excluded_notes)
def num_notes(self) -> int:
"""Number of notes in the vault.
Returns:
int: Number of notes in the vault.
"""
return len(self.notes)
def rename_metadata(self, key: str, value_1: str, value_2: str = None) -> bool:
"""Renames a key or key-value pair in the note's metadata.
If no value is provided, will rename an entire key.
Args:
key (str): Key to rename.
value_1 (str): Value to rename or new name of key if no value_2 is provided.
value_2 (str, optional): New value.
Returns:
bool: True if metadata was renamed.
"""
changes = False
for _note in self.notes:
if _note.rename_metadata(key, value_1, value_2):
changes = True
if changes:
self.metadata.rename(key, value_1, value_2)
return True
return False
def rename_inline_tag(self, old_tag: str, new_tag: str) -> bool:
"""Rename an inline tag in the vault.
Args:
old_tag (str): Old tag name.
new_tag (str): New tag name.
Returns:
bool: True if tag was renamed.
"""
changes = False
for _note in self.notes:
if _note.rename_inline_tag(old_tag, new_tag):
changes = True
if changes:
self.metadata.rename(self.notes[0].inline_tags.metadata_key, old_tag, new_tag)
return True
return False
def write(self, new_vault: bool = False) -> None:
"""Write changes to the vault."""
log.debug("Writing changes to vault...")
if new_vault:
log.debug("Writing changes to backup")
for _note in self.notes:
_new_note_path: Path = Path(
self.new_vault_path / Path(_note.note_path).relative_to(self.vault_path)
)
log.debug(f"writing to {_new_note_path}")
_note.write(path=_new_note_path)
else:
for _note in self.notes:
log.debug(f"writing to {_note.note_path}")
_note.write()