6 Commits

Author SHA1 Message Date
Nathaniel Landau
9131ce128d bump(release): v0.1.0 → v0.1.1 2023-01-23 00:31:51 +00:00
Nathaniel Landau
b7735760e9 test: add tests for Application class
Need more attention here. Very difficult to test the keyboard interaction with questionary. Going to try using pexpect soon to hopefully add better coverage.
2023-01-23 00:31:08 +00:00
Nathaniel Landau
3fd6866760 fix(notes): diff now prints values in the form [value] 2023-01-22 18:23:50 -05:00
Nathaniel Landau
759fc3434f fix(application): exit after committing changes 2023-01-22 18:23:50 -05:00
Nathaniel Landau
c427a987c1 docs(readme): update install instructions 2023-01-22 18:23:50 -05:00
Nathaniel Landau
9123ee149f ci: harden runner configuration 2023-01-22 17:17:09 +00:00
14 changed files with 122 additions and 59 deletions

View File

@@ -20,8 +20,11 @@ jobs:
steps:
- uses: step-security/harden-runner@18bf8ad2ca49c14cbb28b91346d626ccfb00c518 # v2.1.0
with:
egress-policy: audit
egress-policy: block
disable-sudo: true
allowed-endpoints: >
github.com:443
upload.pypi.org:443
- name: Checkout repository
uses: actions/checkout@v3

View File

@@ -1,3 +1,10 @@
## v0.1.1 (2023-01-23)
### Fix
- **notes**: diff now prints values in the form `[value]`
- **application**: exit after committing changes
## v0.1.0 (2023-01-22)
### Feat

View File

@@ -2,34 +2,29 @@
# obsidian-metadata
A script to make batch updates to metadata in an Obsidian vault. Provides the following capabilities:
- in-text tag: delete every occurrence
- in-text tags: Rename tag (`#tag1` -> `#tag2`)
- frontmatter: Delete a key matching a regex pattern and all associated values
- frontmatter: Rename a key
- frontmatter: Delete a value matching a regex pattern from a specified key
- frontmatter: Rename a value from a specified key
- inline metadata: Delete a key matching a regex pattern and all associated values
- inline metadata: Rename a key
- inline metadata: Delete a value matching a regex pattern from a specified key
- inline metadata: Rename a value from a specified key
- vault: Create a backup of the Obsidian vault
- `in-text tag`: delete every occurrence
- `in-text tags`: Rename tag (`#tag1` -> `#tag2`)
- `frontmatter`: Delete a key matching a regex pattern and all associated values
- `frontmatter`: Rename a key
- `frontmatter`: Delete a value matching a regex pattern from a specified key
- `frontmatter`: Rename a value from a specified key
- `inline metadata`: Delete a key matching a regex pattern and all associated values
- `inline metadata`: Rename a key
- `inline metadata`: Delete a value matching a regex pattern from a specified key
- `inline metadata`: Rename a value from a specified key
- `vault`: Create a backup of the Obsidian vault
## Install
`obsidian-metadata` requires Python v3.10 or above.
Use [PIPX](https://pypa.github.io/pipx/) to install this package from Github.
```bash
pipx install git+https://${GITHUB_TOKEN}@github.com/natelandau/obsidian-metadata
pip install obsidian-metadata
```
## Disclaimer
**Important:** It is strongly recommended that you back up your vault prior to committing changes. This script makes changes directly to the markdown files in your vault. Once the changes are committed, there is no ability to recreate the original information unless you have a backup. Follow the instructions in the script to create a backup of your vault if needed.
The author of this script is not responsible for any data loss that may occur. Use at your own risk.
## Important Disclaimer
**It is strongly recommended that you back up your vault prior to committing changes.** This script makes changes directly to the markdown files in your vault. Once the changes are committed, there is no ability to recreate the original information unless you have a backup. Follow the instructions in the script to create a backup of your vault if needed. The author of this script is not responsible for any data loss that may occur. Use at your own risk.
## Usage
The script provides a menu of available actions. Make as many changes as you require and review them as you go. No changes are made to the Vault until they are explicitly committed.

View File

@@ -11,7 +11,7 @@
name = "obsidian-metadata"
readme = "README.md"
repository = "https://github.com/natelandau/obsidian-metadata"
version = "0.1.0"
version = "0.1.1"
[tool.poetry.scripts] # https://python-poetry.org/docs/pyproject/#scripts
obsidian-metadata = "obsidian_metadata.cli:app"
@@ -142,7 +142,7 @@
bump_message = "bump(release): v$current_version → v$new_version"
tag_format = "v$version"
update_changelog_on_bump = true
version = "0.1.0"
version = "0.1.1"
version_files = [
"pyproject.toml:version",
"src/obsidian_metadata/__version__.py:__version__",

View File

@@ -1,2 +1,2 @@
"""obsidian-metadata version."""
__version__ = "0.1.0"
__version__ = "0.1.1"

View File

@@ -132,7 +132,7 @@ def clean_dictionary(dictionary: dict[str, Any]) -> dict[str, Any]:
return new_dict
def clear_screen() -> None:
def clear_screen() -> None: # pragma: no cover
"""Clears the screen."""
# for windows
_ = system("cls") if name == "nt" else system("clear")

View File

@@ -82,7 +82,9 @@ def main(
- [code]vault:[/] Create a backup of the Obsidian vault.
[bold underline]Usage:[/]
Run [tan]obsidian-metadata[/] from the command line. The script will allow you to make batch updates to metadata in an Obsidian vault. Once you have made your changes, review them prior to committing them to the vault.
[tan]Obsidian-metadata[/] allows you to make batch updates to metadata in an Obsidian vault. Once you have made your changes, review them prior to committing them to the vault. The script provides a menu of available actions. Make as many changes as you require and review them as you go. No changes are made to the Vault until they are explicitly committed.
[bold underline]It is strongly recommended that you back up your vault prior to committing changes.[/] This script makes changes directly to the markdown files in your vault. Once the changes are committed, there is no ability to recreate the original information unless you have a backup. Follow the instructions in the script to create a backup of your vault if needed. The author of this script is not responsible for any data loss that may occur. Use at your own risk.
Configuration is specified in a configuration file. On First run, this file will be created at [tan]~/.{0}.env[/]. Any options specified on the command line will override the configuration file.
"""

View File

@@ -4,7 +4,6 @@
from typing import Any
import questionary
import typer
from rich import print
from obsidian_metadata._config import Config
@@ -35,8 +34,6 @@ class Application:
]
)
clear_screen()
def load_vault(self, path_filter: str = None) -> None:
"""Load the vault.
@@ -48,6 +45,7 @@ class Application:
def main_app(self) -> None: # noqa: C901
"""Questions for the main application."""
clear_screen()
self.load_vault()
while True:
@@ -132,8 +130,8 @@ class Application:
if operation == "review_changes":
self.review_changes()
if operation == "commit_changes":
self.commit_changes()
if operation == "commit_changes" and self.commit_changes():
break
if operation == "abort":
break
@@ -346,25 +344,29 @@ class Application:
break
changed_notes[note_to_review].print_diff()
def commit_changes(self) -> None:
"""Write all changes to disk."""
def commit_changes(self) -> bool:
"""Write all changes to disk.
Returns:
True if changes were committed, False otherwise.
"""
changed_notes = self.vault.get_changed_notes()
if len(changed_notes) == 0:
print("\n")
alerts.notice("No changes to commit.\n")
return
return False
backup = questionary.confirm("Create backup before committing changes").ask()
if backup is None:
return
return False
if backup:
self.vault.backup()
if questionary.confirm(f"Commit {len(changed_notes)} changed files to disk?").ask():
self.vault.write()
alerts.success("Changes committed to disk. Exiting.")
typer.Exit()
alerts.success(f"{len(changed_notes)} changes committed to disk. Exiting")
return True
return
return False

View File

@@ -7,7 +7,6 @@ from pathlib import Path
import rich.repr
import typer
from rich import print
from obsidian_metadata._utils import alerts
from obsidian_metadata._utils.alerts import logger as log
@@ -228,9 +227,9 @@ class Note:
result = list(diff.compare(a, b))
for line in result:
if line.startswith("+"):
print(f"[green]{line}[/]")
print(f"\033[92m{line}\033[0m")
elif line.startswith("-"):
print(f"[red]{line}[/]")
print(f"\033[91m{line}\033[0m")
def sub(self, pattern: str, replacement: str, is_regex: bool = False) -> None:
"""Substitutes text within the note.
@@ -348,13 +347,17 @@ class Note:
self.file_content = new_frontmatter + self.file_content
return
self.sub(current_frontmatter, new_frontmatter)
current_frontmatter = re.escape(current_frontmatter)
self.sub(current_frontmatter, new_frontmatter, is_regex=True)
def write(self, path: Path | None = None) -> None:
def write(self, path: Path = None) -> None:
"""Writes the note's content to disk.
Args:
path (Path): Path to write the note to. Defaults to the note's path.
Raises:
typer.Exit: If the note's path is not found.
"""
p = self.note_path if path is None else path

View File

@@ -24,7 +24,6 @@ class Vault:
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.
"""
@@ -32,7 +31,6 @@ class Vault:
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:
@@ -55,12 +53,11 @@ class Vault:
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:
def __rich_repr__(self) -> rich.repr.Result: # pragma: no cover
"""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
@@ -285,18 +282,10 @@ class Vault:
return True
return False
def write(self, new_vault: bool = False) -> None:
def write(self) -> None:
"""Write changes to the vault."""
log.debug("Writing changes to vault...")
if new_vault:
log.debug("Writing changes to backup")
if self.dry_run is False:
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}")
log.trace(f"writing to {_note.note_path}")
_note.write()

18
tests/application_test.py Normal file
View File

@@ -0,0 +1,18 @@
# type: ignore
"""Tests for the application module."""
from obsidian_metadata._config import Config
from obsidian_metadata.models.application import Application
def test_load_vault(test_vault) -> None:
"""Test application."""
vault_path = test_vault
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path)
app = Application(config=config, dry_run=False)
app.load_vault()
assert app.dry_run is False
assert app.config == config
assert app.vault.num_notes() == 2

View File

@@ -4,7 +4,8 @@
from typer.testing import CliRunner
from obsidian_metadata.cli import app
from tests.helpers import Regex
from .helpers import KeyInputs, Regex # noqa: F401
runner = CliRunner()
@@ -14,3 +15,29 @@ def test_version() -> None:
result = runner.invoke(app, ["--version"])
assert result.exit_code == 0
assert result.output == Regex(r"obsidian_metadata: v\d+\.\d+\.\d+$")
def test_application(test_vault, tmp_path) -> None:
"""Test the application."""
vault_path = test_vault
config_path = tmp_path / "config.toml"
result = runner.invoke(
app,
["--vault-path", vault_path, "--config-file", config_path],
# input=KeyInputs.DOWN + KeyInputs.DOWN + KeyInputs.DOWN + KeyInputs.ENTER, # noqa: ERA001
)
banner = r"""
___ _ _ _ _
/ _ \| |__ ___(_) __| (_) __ _ _ __
| | | | '_ \/ __| |/ _` | |/ _` | '_ \
| |_| | |_) \__ \ | (_| | | (_| | | | |
\___/|_.__/|___/_|\__,_|_|\__,_|_| |_|
| \/ | ___| |_ __ _ __| | __ _| |_ __ _
| |\/| |/ _ \ __/ _` |/ _` |/ _` | __/ _` |
| | | | __/ || (_| | (_| | (_| | || (_| |
|_| |_|\___|\__\__,_|\__,_|\__,_|\__\__,_|
"""
assert banner in result.output
assert result.exit_code == 1

View File

@@ -4,6 +4,24 @@
import re
class KeyInputs:
"""Key inputs for testing."""
DOWN = "\x1b[B"
UP = "\x1b[A"
LEFT = "\x1b[D"
RIGHT = "\x1b[C"
ENTER = "\r"
ESCAPE = "\x1b"
CONTROLC = "\x03"
BACK = "\x7f"
SPACE = " "
TAB = "\x09"
ONE = "1"
TWO = "2"
THREE = "3"
class Regex:
"""Assert that a given string meets some expectations.

View File

@@ -16,7 +16,6 @@ def test_vault_creation(test_vault):
assert vault.vault_path == vault_path
assert vault.backup_path == Path(f"{vault_path}.bak")
assert vault.new_vault_path == Path(f"{vault_path}.new")
assert vault.dry_run is False
assert str(vault.exclude_paths[0]) == Regex(r".*\.git")
assert vault.num_notes() == 2