mirror of
https://github.com/natelandau/obsidian-metadata.git
synced 2025-11-13 07:23:47 -05:00
feat(app): limit scope of notes with one or more filters (#13)
* style: rename `VaultMetadata.add_metadata` to `VaultMetadata.index_metadata` * refactor(vault): refactor filtering notes * fix(application): improve usage display * fix(application): improve colors of questions * feat(application): limit the scope of notes to be processed with one or more filters * build(deps): update identify
This commit is contained in:
16
README.md
16
README.md
@@ -22,36 +22,46 @@ Run `obsidian-metadata` from the command line to invoke the script. Add `--help
|
||||
Obsidian-metadata provides a menu of sub-commands.
|
||||
|
||||
**Vault Actions**
|
||||
Create or delete a backup of your vault.
|
||||
- Backup: Create a backup of the vault.
|
||||
- Delete Backup: Delete a backup of the vault.
|
||||
|
||||
**Inspect Metadata**
|
||||
Inspect the metadata in your vault.
|
||||
- View all metadata in the vault
|
||||
|
||||
**Filter Notes in Scope**:
|
||||
Limit the scope of notes to be processed with a regex.
|
||||
- Apply regex: Set a regex to limit scope
|
||||
- List notes in scope: List notes that will be processed.
|
||||
Limit the scope of notes to be processed with one or more filters.
|
||||
- Path filter (regex): Limit scope based on the path or filename
|
||||
- Metadata Filter: Limit scope based on a key or key/value pair
|
||||
- Tag Filter: Limit scope based on an in-text tag
|
||||
- List and Clear Filters List all current filters and clear one or all
|
||||
- List notes in scope: List notes that will be processed.
|
||||
|
||||
**Add Metadata**
|
||||
Add new metadata to your vault.
|
||||
- Add metadata to the frontmatter
|
||||
- Add to inline metadata (Not yet implemented)
|
||||
- Add to inline tag (Not yet implemented)
|
||||
|
||||
**Rename Metadata**
|
||||
Rename either a key and all associated values, a specific value within a key. or an in-text tag.
|
||||
- Rename a key
|
||||
- Rename a value
|
||||
- rename an inline tag
|
||||
|
||||
**Delete Metadata**
|
||||
Delete either a key and all associated values, or a specific value.
|
||||
- Delete a key and associated values
|
||||
- Delete a value from a key
|
||||
- Delete an inline tag
|
||||
|
||||
**Review Changes**
|
||||
Prior to committing changes, review all changes that will be made.
|
||||
- View a diff of the changes that will be made
|
||||
|
||||
**Commit Changes**
|
||||
Write the changes to disk. This step is not undoable.
|
||||
- Commit changes to the vault
|
||||
|
||||
### Configuration
|
||||
|
||||
12
poetry.lock
generated
12
poetry.lock
generated
@@ -188,7 +188,7 @@ pyflakes = ">=3.0.0,<3.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.5.16"
|
||||
version = "2.5.17"
|
||||
description = "File identification library for Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -657,7 +657,7 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "67.0.0"
|
||||
version = "67.1.0"
|
||||
description = "Easily download, build, install, upgrade, and uninstall Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -952,8 +952,8 @@ flake8 = [
|
||||
{file = "flake8-6.0.0.tar.gz", hash = "sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"},
|
||||
]
|
||||
identify = [
|
||||
{file = "identify-2.5.16-py2.py3-none-any.whl", hash = "sha256:832832a58ecc1b8f33d5e8cb4f7d3db2f5c7fbe922dfee5f958b48fed691501a"},
|
||||
{file = "identify-2.5.16.tar.gz", hash = "sha256:c47acedfe6495b1c603ed7e93469b26e839cab38db4155113f36f718f8b3dc47"},
|
||||
{file = "identify-2.5.17-py2.py3-none-any.whl", hash = "sha256:7d526dd1283555aafcc91539acc061d8f6f59adb0a7bba462735b0a318bff7ed"},
|
||||
{file = "identify-2.5.17.tar.gz", hash = "sha256:93cc61a861052de9d4c541a7acb7e3dcc9c11b398a2144f6e52ae5285f5f4f06"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
@@ -1267,8 +1267,8 @@ ruff = [
|
||||
{file = "ruff-0.0.217.tar.gz", hash = "sha256:39b2b1de9330fcf60643bdd6c4c660b457390c686b4ba7101bea019a01446494"},
|
||||
]
|
||||
setuptools = [
|
||||
{file = "setuptools-67.0.0-py3-none-any.whl", hash = "sha256:9d790961ba6219e9ff7d9557622d2fe136816a264dd01d5997cfc057d804853d"},
|
||||
{file = "setuptools-67.0.0.tar.gz", hash = "sha256:883131c5b6efa70b9101c7ef30b2b7b780a4283d5fc1616383cdf22c83cbefe6"},
|
||||
{file = "setuptools-67.1.0-py3-none-any.whl", hash = "sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378"},
|
||||
{file = "setuptools-67.1.0.tar.gz", hash = "sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300"},
|
||||
]
|
||||
shellingham = [
|
||||
{file = "shellingham-1.5.0.post1-py2.py3-none-any.whl", hash = "sha256:368bf8c00754fd4f55afb7bbb86e272df77e4dc76ac29dbcbb81a59e9fc15744"},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"""Logging and alerts."""
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from textwrap import wrap
|
||||
|
||||
import rich.repr
|
||||
import typer
|
||||
@@ -62,6 +63,29 @@ def info(msg: str) -> None:
|
||||
print(f"INFO | {msg}")
|
||||
|
||||
|
||||
def usage(msg: str, width: int = 80) -> None:
|
||||
"""Print a usage message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
width (optional): Width of the message
|
||||
"""
|
||||
for _n, line in enumerate(wrap(msg, width=width)):
|
||||
if _n == 0:
|
||||
print(f"[dim]USAGE | {line}")
|
||||
else:
|
||||
print(f"[dim] | {line}")
|
||||
|
||||
|
||||
def debug(msg: str) -> None:
|
||||
"""Print a debug message without using logging.
|
||||
|
||||
Args:
|
||||
msg: Message to print
|
||||
"""
|
||||
print(f"[blue]DEBUG | {msg}[/blue]")
|
||||
|
||||
|
||||
def dim(msg: str) -> None:
|
||||
"""Print a message in dimmed color.
|
||||
|
||||
|
||||
@@ -82,36 +82,46 @@ def main(
|
||||
[tan]Obsidian-metadata[/] provides a menu of sub-commands.
|
||||
|
||||
[bold underline]Vault Actions[/]
|
||||
Create or delete a backup of your vault.
|
||||
• Backup: Create a backup of the vault.
|
||||
• Delete Backup: Delete a backup of the vault.
|
||||
|
||||
[bold underline]Inspect Metadata[/]
|
||||
Inspect the metadata in your vault.
|
||||
• View all metadata in the vault
|
||||
|
||||
[bold underline]Filter Notes in Scope[/]
|
||||
Limit the scope of notes to be processed with a regex.
|
||||
• Apply regex: Set a regex to limit scope
|
||||
• List notes in scope: List notes that will be processed.
|
||||
Limit the scope of notes to be processed with one or more filters.
|
||||
• Path filter (regex): Limit scope based on the path or filename
|
||||
• Metadata Filter: Limit scope based on a key or key/value pair
|
||||
• Tag Filter: Limit scope based on an in-text tag
|
||||
• List and Clear Filters: List all current filters and clear one or all
|
||||
• List notes in scope: List notes that will be processed.
|
||||
|
||||
[bold underline]Add Metadata[/]
|
||||
Add new metadata to your vault.
|
||||
• Add metadata to the frontmatter
|
||||
• [dim]Add to inline metadata (Not yet implemented)[/]
|
||||
• [dim]Add to inline tag (Not yet implemented)[/]
|
||||
|
||||
[bold underline]Rename Metadata[/]
|
||||
Rename either a key and all associated values, a specific value within a key. or an in-text tag.
|
||||
• Rename a key
|
||||
• Rename a value
|
||||
• rename an inline tag
|
||||
|
||||
[bold underline]Delete Metadata[/]
|
||||
Delete either a key and all associated values, or a specific value.
|
||||
• Delete a key and associated values
|
||||
• Delete a value from a key
|
||||
• Delete an inline tag
|
||||
|
||||
[bold underline]Review Changes[/]
|
||||
Prior to committing changes, review all changes that will be made.
|
||||
• View a diff of the changes that will be made
|
||||
|
||||
[bold underline]Commit Changes[/]
|
||||
Write the changes to disk. This step is not undoable.
|
||||
• Commit changes to the vault
|
||||
|
||||
"""
|
||||
|
||||
@@ -8,7 +8,7 @@ from obsidian_metadata.models.metadata import (
|
||||
VaultMetadata,
|
||||
)
|
||||
from obsidian_metadata.models.notes import Note
|
||||
from obsidian_metadata.models.vault import Vault
|
||||
from obsidian_metadata.models.vault import Vault, VaultFilter
|
||||
|
||||
from obsidian_metadata.models.application import Application # isort: skip
|
||||
|
||||
@@ -23,4 +23,5 @@ __all__ = [
|
||||
"Patterns",
|
||||
"Vault",
|
||||
"VaultMetadata",
|
||||
"VaultFilter",
|
||||
]
|
||||
|
||||
@@ -5,10 +5,12 @@ from typing import Any
|
||||
|
||||
import questionary
|
||||
from rich import print
|
||||
from textwrap import dedent
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.table import Table
|
||||
from obsidian_metadata._config import VaultConfig
|
||||
from obsidian_metadata._utils.alerts import logger as log
|
||||
from obsidian_metadata.models import Patterns, Vault
|
||||
from obsidian_metadata.models import Patterns, Vault, VaultFilter
|
||||
from obsidian_metadata._utils import alerts
|
||||
from obsidian_metadata.models.questions import Questions
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
@@ -28,10 +30,11 @@ class Application:
|
||||
self.config = config
|
||||
self.dry_run = dry_run
|
||||
self.questions = Questions()
|
||||
self.filters: list[VaultFilter] = []
|
||||
|
||||
def application_main(self) -> None:
|
||||
"""Questions for the main application."""
|
||||
self.load_vault()
|
||||
self._load_vault()
|
||||
|
||||
while True:
|
||||
self.vault.info()
|
||||
@@ -65,12 +68,9 @@ class Application:
|
||||
|
||||
def application_add_metadata(self) -> None:
|
||||
"""Add metadata."""
|
||||
help_text = """
|
||||
USAGE | Add Metadata
|
||||
[dim]Add new metadata to your vault. Currently only supports
|
||||
adding to the frontmatter of a note.[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
alerts.usage(
|
||||
"Add new metadata to your vault. Currently only supports adding to the frontmatter of a note."
|
||||
)
|
||||
|
||||
area = self.questions.ask_area()
|
||||
match area:
|
||||
@@ -103,41 +103,109 @@ class Application:
|
||||
|
||||
def application_filter(self) -> None:
|
||||
"""Filter notes."""
|
||||
help_text = """
|
||||
USAGE | Filter Notes
|
||||
[dim]Enter a regex to filter notes by path. This allows you to
|
||||
specify a subset of notes to update. Leave empty to include
|
||||
all markdown files.[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
alerts.usage("Limit the scope of notes to be processed with one or more filters.")
|
||||
|
||||
choices = [
|
||||
{"name": "Apply regex filter", "value": "apply_filter"},
|
||||
{"name": "Apply new regex path filter", "value": "apply_path_filter"},
|
||||
{"name": "Apply new metadata filter", "value": "apply_metadata_filter"},
|
||||
{"name": "Apply new in-text tag filter", "value": "apply_tag_filter"},
|
||||
{"name": "List and clear filters", "value": "list_filters"},
|
||||
{"name": "List notes in scope", "value": "list_notes"},
|
||||
questionary.Separator(),
|
||||
{"name": "Back", "value": "back"},
|
||||
]
|
||||
while True:
|
||||
match self.questions.ask_selection(choices=choices, question="Select an action"):
|
||||
case "apply_filter":
|
||||
case "apply_path_filter":
|
||||
|
||||
path_filter = self.questions.ask_filter_path()
|
||||
if path_filter is None:
|
||||
path = self.questions.ask_filter_path()
|
||||
if path is None or path == "":
|
||||
return
|
||||
|
||||
if path_filter == "":
|
||||
path_filter = None
|
||||
self.filters.append(VaultFilter(path_filter=path))
|
||||
self._load_vault()
|
||||
|
||||
self.load_vault(path_filter=path_filter)
|
||||
case "apply_metadata_filter":
|
||||
key = self.questions.ask_existing_key()
|
||||
if key is None:
|
||||
return
|
||||
|
||||
total_notes = self.vault.num_notes() + self.vault.num_excluded_notes()
|
||||
|
||||
if path_filter is None:
|
||||
alerts.success(f"Loaded all {total_notes} total notes")
|
||||
questions2 = Questions(vault=self.vault, key=key)
|
||||
value = questions2.ask_existing_value(
|
||||
question="Enter the value for the metadata filter",
|
||||
)
|
||||
if value is None:
|
||||
return
|
||||
if value == "":
|
||||
self.filters.append(VaultFilter(key_filter=key))
|
||||
else:
|
||||
alerts.success(
|
||||
f"Loaded {self.vault.num_notes()} notes from {total_notes} total notes"
|
||||
)
|
||||
self.filters.append(VaultFilter(key_filter=key, value_filter=value))
|
||||
self._load_vault()
|
||||
|
||||
case "apply_tag_filter":
|
||||
tag = self.questions.ask_existing_inline_tag()
|
||||
if tag is None or tag == "":
|
||||
return
|
||||
|
||||
self.filters.append(VaultFilter(tag_filter=tag))
|
||||
self._load_vault()
|
||||
|
||||
case "list_filters":
|
||||
if len(self.filters) == 0:
|
||||
alerts.notice("No filters have been applied")
|
||||
return
|
||||
|
||||
print("")
|
||||
table = Table(
|
||||
"Opt",
|
||||
"Filter",
|
||||
"Type",
|
||||
title="Current Filters",
|
||||
show_header=False,
|
||||
box=box.HORIZONTALS,
|
||||
)
|
||||
for _n, filter in enumerate(self.filters, start=1):
|
||||
if filter.path_filter is not None:
|
||||
table.add_row(
|
||||
str(_n),
|
||||
f"Path regex: [tan bold]{filter.path_filter}",
|
||||
end_section=bool(_n == len(self.filters)),
|
||||
)
|
||||
elif filter.tag_filter is not None:
|
||||
table.add_row(
|
||||
str(_n),
|
||||
f"Tag filter: [tan bold]{filter.tag_filter}",
|
||||
end_section=bool(_n == len(self.filters)),
|
||||
)
|
||||
elif filter.key_filter is not None and filter.value_filter is None:
|
||||
table.add_row(
|
||||
str(_n),
|
||||
f"Key filter: [tan bold]{filter.key_filter}",
|
||||
end_section=bool(_n == len(self.filters)),
|
||||
)
|
||||
elif filter.key_filter is not None and filter.value_filter is not None:
|
||||
table.add_row(
|
||||
str(_n),
|
||||
f"Key/Value : [tan bold]{filter.key_filter}={filter.value_filter}",
|
||||
end_section=bool(_n == len(self.filters)),
|
||||
)
|
||||
table.add_row(f"{len(self.filters) + 1}", "Clear All")
|
||||
table.add_row(f"{len(self.filters) + 2}", "Return to Main Menu")
|
||||
Console().print(table)
|
||||
|
||||
num = self.questions.ask_number(
|
||||
question="Enter the number of the filter to clear"
|
||||
)
|
||||
if num is None:
|
||||
return
|
||||
if int(num) <= len(self.filters):
|
||||
self.filters.pop(int(num) - 1)
|
||||
self._load_vault()
|
||||
return
|
||||
if int(num) == len(self.filters) + 1:
|
||||
self.filters = []
|
||||
self._load_vault()
|
||||
return
|
||||
|
||||
case "list_notes":
|
||||
self.vault.list_editable_notes()
|
||||
@@ -147,12 +215,9 @@ class Application:
|
||||
|
||||
def application_inspect_metadata(self) -> None:
|
||||
"""View metadata."""
|
||||
help_text = """
|
||||
USAGE | View Metadata
|
||||
[dim]Inspect the metadata in your vault. Note, uncommitted
|
||||
changes will be reflected in these reports[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
alerts.usage(
|
||||
"Inspect the metadata in your vault. Note, uncommitted changes will be reflected in these reports"
|
||||
)
|
||||
|
||||
choices = [
|
||||
{"name": "View all metadata", "value": "all_metadata"},
|
||||
@@ -168,11 +233,7 @@ class Application:
|
||||
|
||||
def application_vault(self) -> None:
|
||||
"""Vault actions."""
|
||||
help_text = """
|
||||
USAGE | Vault Actions
|
||||
[dim]Create or delete a backup of your vault.[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
alerts.usage("Create or delete a backup of your vault.")
|
||||
|
||||
choices = [
|
||||
{"name": "Backup vault", "value": "backup_vault"},
|
||||
@@ -191,12 +252,7 @@ class Application:
|
||||
return
|
||||
|
||||
def application_delete_metadata(self) -> None:
|
||||
help_text = """
|
||||
USAGE | Delete Metadata
|
||||
[dim]Delete either a key and all associated values,
|
||||
or a specific value.[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
alerts.usage("Delete either a key and all associated values, or a specific value.")
|
||||
|
||||
choices = [
|
||||
{"name": "Delete key", "value": "delete_key"},
|
||||
@@ -219,11 +275,7 @@ class Application:
|
||||
|
||||
def application_rename_metadata(self) -> None:
|
||||
"""Rename metadata."""
|
||||
help_text = """
|
||||
USAGE | Rename Metadata
|
||||
[dim]Select the type of metadata to rename.[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
alerts.usage("Select the type of metadata to rename.")
|
||||
|
||||
choices = [
|
||||
{"name": "Rename key", "value": "rename_key"},
|
||||
@@ -324,14 +376,17 @@ class Application:
|
||||
|
||||
return
|
||||
|
||||
def load_vault(self, path_filter: str = None) -> None:
|
||||
"""Load the vault.
|
||||
def _load_vault(self) -> None:
|
||||
"""Load the vault."""
|
||||
|
||||
Args:
|
||||
path_filter (str, optional): Regex to filter notes by path.
|
||||
"""
|
||||
self.vault: Vault = Vault(config=self.config, dry_run=self.dry_run, path_filter=path_filter)
|
||||
log.info(f"Indexed {self.vault.num_notes()} notes from {self.vault.vault_path}")
|
||||
if len(self.filters) == 0:
|
||||
self.vault: Vault = Vault(config=self.config, dry_run=self.dry_run)
|
||||
else:
|
||||
self.vault = Vault(config=self.config, dry_run=self.dry_run, filters=self.filters)
|
||||
|
||||
alerts.success(
|
||||
f"Loaded {len(self.vault.notes_in_scope)} notes from {len(self.vault.all_notes)} total notes"
|
||||
)
|
||||
self.questions = Questions(vault=self.vault)
|
||||
|
||||
def rename_key(self) -> None:
|
||||
|
||||
@@ -31,8 +31,8 @@ class VaultMetadata:
|
||||
"""Representation of all metadata."""
|
||||
return str(self.dict)
|
||||
|
||||
def add_metadata(self, metadata: dict[str, list[str]]) -> None:
|
||||
"""Add metadata to the vault. Takes a dictionary as input and merges it with the existing metadata. Does not overwrite existing keys.
|
||||
def index_metadata(self, metadata: dict[str, list[str]]) -> None:
|
||||
"""Index pre-existing metadata in the vault. Takes a dictionary as input and merges it with the existing metadata. Does not overwrite existing keys.
|
||||
|
||||
Args:
|
||||
metadata (dict): Metadata to add.
|
||||
|
||||
@@ -64,13 +64,13 @@ class Questions:
|
||||
"""
|
||||
self.style = questionary.Style(
|
||||
[
|
||||
("qmark", "fg:#808080 bold"),
|
||||
("question", "bold"),
|
||||
("qmark", "fg:#729fcf bold"),
|
||||
("question", "fg:#729fcf bold"),
|
||||
("separator", "fg:#808080"),
|
||||
("instruction", "fg:#808080"),
|
||||
("highlighted", "fg:#c0c0c0 bold reverse"),
|
||||
("highlighted", "fg:#729fcf bold underline"),
|
||||
("text", ""),
|
||||
("pointer", "bold"),
|
||||
("pointer", "fg:#729fcf bold"),
|
||||
]
|
||||
)
|
||||
self.vault = vault
|
||||
@@ -174,6 +174,20 @@ class Questions:
|
||||
|
||||
return True
|
||||
|
||||
def _validate_number(self, text: str) -> bool | str:
|
||||
"""Validate a number.
|
||||
|
||||
Args:
|
||||
text (str): The number to validate.
|
||||
|
||||
Returns:
|
||||
bool | str: True if the number is valid, otherwise a string with the error message.
|
||||
"""
|
||||
if not text.isdigit():
|
||||
return "Must be an integer"
|
||||
|
||||
return True
|
||||
|
||||
def _validate_valid_vault_regex(self, text: str) -> bool | str:
|
||||
"""Validates a valid regex.
|
||||
|
||||
@@ -202,8 +216,8 @@ class Questions:
|
||||
Returns:
|
||||
bool | str: True if the value is valid, otherwise a string with the error message.
|
||||
"""
|
||||
if len(text) < 1:
|
||||
return "Value cannot be empty"
|
||||
if len(text) == 0:
|
||||
return True
|
||||
|
||||
if self.key is not None and not self.vault.metadata.contains(self.key, text):
|
||||
return f"{self.key}:{text} does not exist"
|
||||
@@ -408,6 +422,19 @@ class Questions:
|
||||
question, validate=self._validate_new_value, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_number(self, question: str = "Enter a number") -> int:
|
||||
"""Ask the user for a number.
|
||||
|
||||
Args:
|
||||
question (str, optional): The question to ask. Defaults to "Enter a number".
|
||||
|
||||
Returns:
|
||||
int: A number.
|
||||
"""
|
||||
return questionary.text(
|
||||
question, validate=self._validate_number, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_selection(
|
||||
self, choices: list[Any], question: str = "Select an option"
|
||||
) -> Any: # pragma: no cover
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import re
|
||||
import shutil
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import rich.repr
|
||||
@@ -17,6 +18,16 @@ from obsidian_metadata._utils.alerts import logger as log
|
||||
from obsidian_metadata.models import MetadataType, Note, VaultMetadata
|
||||
|
||||
|
||||
@dataclass
|
||||
class VaultFilter:
|
||||
"""Vault filters."""
|
||||
|
||||
path_filter: str = None
|
||||
key_filter: str = None
|
||||
value_filter: str = None
|
||||
tag_filter: str = None
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
class Vault:
|
||||
"""Representation of the Obsidian vault.
|
||||
@@ -28,7 +39,12 @@ class Vault:
|
||||
notes (list[Note]): List of all notes in the vault.
|
||||
"""
|
||||
|
||||
def __init__(self, config: VaultConfig, dry_run: bool = False, path_filter: str = None):
|
||||
def __init__(
|
||||
self,
|
||||
config: VaultConfig,
|
||||
dry_run: bool = False,
|
||||
filters: list[VaultFilter] = [],
|
||||
):
|
||||
self.vault_path: Path = config.path
|
||||
self.dry_run: bool = dry_run
|
||||
self.backup_path: Path = self.vault_path.parent / f"{self.vault_path.name}.bak"
|
||||
@@ -37,8 +53,8 @@ class Vault:
|
||||
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)
|
||||
self.filters = filters
|
||||
self.all_note_paths = self._find_markdown_notes()
|
||||
|
||||
with Progress(
|
||||
SpinnerColumn(),
|
||||
@@ -46,9 +62,10 @@ class Vault:
|
||||
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
|
||||
self.all_notes: list[Note] = [
|
||||
Note(note_path=p, dry_run=self.dry_run) for p in self.all_note_paths
|
||||
]
|
||||
self.notes_in_scope = self._filter_notes()
|
||||
|
||||
self._rebuild_vault_metadata()
|
||||
|
||||
@@ -57,33 +74,54 @@ class Vault:
|
||||
yield "vault_path", self.vault_path
|
||||
yield "dry_run", self.dry_run
|
||||
yield "backup_path", self.backup_path
|
||||
yield "num_notes", self.num_notes()
|
||||
yield "num_notes", len(self.all_notes)
|
||||
yield "num_notes_in_scope", len(self.notes_in_scope)
|
||||
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.
|
||||
def _filter_notes(self) -> list[Note]:
|
||||
"""Filter notes by path and metadata using the filters defined in self.filters.
|
||||
|
||||
Args:
|
||||
path_filter (str, optional): Regex to filter notes by path.
|
||||
Returns:
|
||||
list[Note]: List of notes matching the filters.
|
||||
"""
|
||||
notes_list = self.all_notes.copy()
|
||||
|
||||
for _filter in self.filters:
|
||||
if _filter.path_filter is not None:
|
||||
notes_list = [
|
||||
n
|
||||
for n in notes_list
|
||||
if re.search(_filter.path_filter, str(n.note_path.relative_to(self.vault_path)))
|
||||
]
|
||||
|
||||
if _filter.tag_filter is not None:
|
||||
notes_list = [n for n in notes_list if n.contains_inline_tag(_filter.tag_filter)]
|
||||
|
||||
if _filter.key_filter is not None and _filter.value_filter is not None:
|
||||
notes_list = [
|
||||
n
|
||||
for n in notes_list
|
||||
if n.contains_metadata(_filter.key_filter, _filter.value_filter)
|
||||
]
|
||||
if _filter.key_filter is not None and _filter.value_filter is None:
|
||||
notes_list = [n for n in notes_list if n.contains_metadata(_filter.key_filter)]
|
||||
|
||||
return notes_list
|
||||
|
||||
def _find_markdown_notes(self) -> list[Path]:
|
||||
"""Build list of all markdown files in the vault.
|
||||
|
||||
Returns:
|
||||
list[Path]: List of paths to all matching files in the vault.
|
||||
|
||||
"""
|
||||
notes_list = [
|
||||
return [
|
||||
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 _rebuild_vault_metadata(self) -> None:
|
||||
"""Rebuild vault metadata."""
|
||||
self.metadata = VaultMetadata()
|
||||
@@ -93,10 +131,12 @@ class Vault:
|
||||
transient=True,
|
||||
) as progress:
|
||||
progress.add_task(description="Processing notes...", total=None)
|
||||
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})
|
||||
for _note in self.notes_in_scope:
|
||||
self.metadata.index_metadata(_note.frontmatter.dict)
|
||||
self.metadata.index_metadata(_note.inline_metadata.dict)
|
||||
self.metadata.index_metadata(
|
||||
{_note.inline_tags.metadata_key: _note.inline_tags.list}
|
||||
)
|
||||
|
||||
def add_metadata(self, area: MetadataType, key: str, value: str | list[str] = None) -> int:
|
||||
"""Add metadata to all notes in the vault.
|
||||
@@ -111,7 +151,7 @@ class Vault:
|
||||
"""
|
||||
num_changed = 0
|
||||
|
||||
for _note in self.notes:
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.add_metadata(area, key, value):
|
||||
num_changed += 1
|
||||
|
||||
@@ -153,7 +193,7 @@ class Vault:
|
||||
Returns:
|
||||
bool: True if tag is found in vault.
|
||||
"""
|
||||
return any(_note.contains_inline_tag(tag) for _note in self.notes)
|
||||
return any(_note.contains_inline_tag(tag) for _note in self.notes_in_scope)
|
||||
|
||||
def contains_metadata(self, key: str, value: str = None, is_regex: bool = False) -> bool:
|
||||
"""Check if vault contains the given metadata.
|
||||
@@ -193,7 +233,7 @@ class Vault:
|
||||
"""
|
||||
num_changed = 0
|
||||
|
||||
for _note in self.notes:
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.delete_inline_tag(tag):
|
||||
num_changed += 1
|
||||
|
||||
@@ -214,7 +254,7 @@ class Vault:
|
||||
"""
|
||||
num_changed = 0
|
||||
|
||||
for _note in self.notes:
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.delete_metadata(key, value):
|
||||
num_changed += 1
|
||||
|
||||
@@ -230,7 +270,7 @@ class Vault:
|
||||
list[Note]: List of notes that have changes.
|
||||
"""
|
||||
changed_notes = []
|
||||
for _note in self.notes:
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.has_changes():
|
||||
changed_notes.append(_note)
|
||||
|
||||
@@ -245,36 +285,23 @@ class Vault:
|
||||
table.add_row("Backup path", str(self.backup_path))
|
||||
else:
|
||||
table.add_row("Backup", "None")
|
||||
table.add_row("Notes in scope", str(self.num_notes()))
|
||||
table.add_row("Notes in scope", str(len(self.notes_in_scope)))
|
||||
table.add_row("Notes excluded from scope", str(self.num_excluded_notes()))
|
||||
table.add_row("Active path filter", str(self.path_filter))
|
||||
table.add_row("Notes with updates", str(len(self.get_changed_notes())))
|
||||
table.add_row("Active filters", str(len(self.filters)))
|
||||
table.add_row("Notes with changes", 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."""
|
||||
table = Table(title="Notes in current scope", show_header=False, box=box.HORIZONTALS)
|
||||
for _n, _note in enumerate(self.notes, start=1):
|
||||
for _n, _note in enumerate(self.notes_in_scope, start=1):
|
||||
table.add_row(str(_n), str(_note.note_path.relative_to(self.vault_path)))
|
||||
Console().print(table)
|
||||
|
||||
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)
|
||||
return len(self.all_notes) - len(self.notes_in_scope)
|
||||
|
||||
def rename_metadata(self, key: str, value_1: str, value_2: str = None) -> int:
|
||||
"""Renames a key or key-value pair in the note's metadata.
|
||||
@@ -291,7 +318,7 @@ class Vault:
|
||||
"""
|
||||
num_changed = 0
|
||||
|
||||
for _note in self.notes:
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.rename_metadata(key, value_1, value_2):
|
||||
num_changed += 1
|
||||
|
||||
@@ -312,7 +339,7 @@ class Vault:
|
||||
"""
|
||||
num_changed = 0
|
||||
|
||||
for _note in self.notes:
|
||||
for _note in self.notes_in_scope:
|
||||
if _note.rename_inline_tag(old_tag, new_tag):
|
||||
num_changed += 1
|
||||
|
||||
@@ -325,6 +352,6 @@ class Vault:
|
||||
"""Write changes to the vault."""
|
||||
log.debug("Writing changes to vault...")
|
||||
if self.dry_run is False:
|
||||
for _note in self.notes:
|
||||
for _note in self.notes_in_scope:
|
||||
log.trace(f"writing to {_note.note_path}")
|
||||
_note.write()
|
||||
|
||||
@@ -44,6 +44,36 @@ def test_notice(capsys):
|
||||
assert captured.out == "NOTICE | This prints in notice\n"
|
||||
|
||||
|
||||
def test_alerts_debug(capsys):
|
||||
"""Test debug."""
|
||||
alerts.debug("This prints in debug")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "DEBUG | This prints in debug\n"
|
||||
|
||||
|
||||
def test_usage(capsys):
|
||||
"""Test usage."""
|
||||
alerts.usage("This prints in usage")
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == "USAGE | This prints in usage\n"
|
||||
|
||||
alerts.usage(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua"
|
||||
)
|
||||
captured = capsys.readouterr()
|
||||
assert "USAGE | Lorem ipsum dolor sit amet" in captured.out
|
||||
assert " | incididunt ut labore et dolore magna aliqua" in captured.out
|
||||
|
||||
alerts.usage(
|
||||
"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
|
||||
width=20,
|
||||
)
|
||||
captured = capsys.readouterr()
|
||||
assert "USAGE | Lorem ipsum dolor" in captured.out
|
||||
assert " | sit amet," in captured.out
|
||||
assert " | adipisicing elit," in captured.out
|
||||
|
||||
|
||||
def test_info(capsys):
|
||||
"""Test info."""
|
||||
alerts.info("This prints in info")
|
||||
|
||||
@@ -19,19 +19,19 @@ from tests.helpers import Regex
|
||||
def test_instantiate_application(test_application) -> None:
|
||||
"""Test application."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
|
||||
assert app.dry_run is False
|
||||
assert app.config.name == "command_line_vault"
|
||||
assert app.config.exclude_paths == [".git", ".obsidian"]
|
||||
assert app.dry_run is False
|
||||
assert app.vault.num_notes() == 13
|
||||
assert len(app.vault.all_notes) == 13
|
||||
|
||||
|
||||
def test_abort(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming a key."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
return_value="abort",
|
||||
@@ -45,7 +45,7 @@ def test_abort(test_application, mocker, capsys) -> None:
|
||||
def test_add_metadata_frontmatter_success(test_application, mocker, capsys) -> None:
|
||||
"""Test adding new metadata to the vault."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["add_metadata", KeyError],
|
||||
@@ -72,7 +72,7 @@ def test_add_metadata_frontmatter_success(test_application, mocker, capsys) -> N
|
||||
def test_delete_inline_tag(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["delete_metadata", KeyError],
|
||||
@@ -113,7 +113,7 @@ def test_delete_inline_tag(test_application, mocker, capsys) -> None:
|
||||
def test_delete_key(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["delete_metadata", KeyError],
|
||||
@@ -156,7 +156,7 @@ def test_delete_key(test_application, mocker, capsys) -> None:
|
||||
def test_delete_value(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["delete_metadata", KeyError],
|
||||
@@ -202,17 +202,17 @@ def test_delete_value(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_filter_notes_filter(test_application, mocker, capsys) -> None:
|
||||
def test_filter_notes(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming a key."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["filter_notes", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["apply_filter", "list_notes", "back"],
|
||||
side_effect=["apply_path_filter", "list_notes", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_filter_path",
|
||||
@@ -232,25 +232,61 @@ def test_filter_notes_filter(test_application, mocker, capsys) -> None:
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["apply_filter", "list_notes", "back"],
|
||||
side_effect=["apply_metadata_filter", "list_notes", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_filter_path",
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_key",
|
||||
return_value="on_one_note",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_value",
|
||||
return_value="",
|
||||
)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex(r"SUCCESS +\| Loaded all.*\d+.*total notes", re.DOTALL)
|
||||
assert captured.out == Regex(r"SUCCESS +\| Loaded.*1.*notes from.*\d+.*total", re.DOTALL)
|
||||
assert "02 inline/inline 2.md" in captured.out
|
||||
assert "03 mixed/mixed 1.md" not in captured.out
|
||||
|
||||
|
||||
def test_filter_clear(test_application, mocker, capsys) -> None:
|
||||
"""Test clearing filters."""
|
||||
app = test_application
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["filter_notes", "filter_notes", KeyError],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_selection",
|
||||
side_effect=["apply_metadata_filter", "list_filters", "list_notes", "back"],
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_key",
|
||||
return_value="on_one_note",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_existing_value",
|
||||
return_value="",
|
||||
)
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_number",
|
||||
return_value="1",
|
||||
)
|
||||
with pytest.raises(KeyError):
|
||||
app.application_main()
|
||||
captured = capsys.readouterr()
|
||||
assert "02 inline/inline 2.md" in captured.out
|
||||
assert "03 mixed/mixed 1.md" in captured.out
|
||||
assert "01 frontmatter/frontmatter 4.md" in captured.out
|
||||
assert "04 no metadata/no_metadata_1.md " in captured.out
|
||||
|
||||
|
||||
def test_inspect_metadata_all(test_application, mocker, capsys) -> None:
|
||||
"""Test backing up a vault."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["inspect_metadata", KeyError],
|
||||
@@ -269,7 +305,7 @@ def test_inspect_metadata_all(test_application, mocker, capsys) -> None:
|
||||
def test_rename_inline_tag(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", KeyError],
|
||||
@@ -318,7 +354,7 @@ def test_rename_inline_tag(test_application, mocker, capsys) -> None:
|
||||
def test_rename_key(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming a key."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", KeyError],
|
||||
@@ -367,7 +403,7 @@ def test_rename_key(test_application, mocker, capsys) -> None:
|
||||
def test_rename_value_fail(test_application, mocker, capsys) -> None:
|
||||
"""Test renaming an inline tag."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", KeyError],
|
||||
@@ -425,7 +461,7 @@ def test_rename_value_fail(test_application, mocker, capsys) -> None:
|
||||
def test_review_no_changes(test_application, mocker, capsys) -> None:
|
||||
"""Review changes when no changes to vault."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["review_changes", KeyError],
|
||||
@@ -439,7 +475,7 @@ def test_review_no_changes(test_application, mocker, capsys) -> None:
|
||||
def test_review_changes(test_application, mocker, capsys) -> None:
|
||||
"""Review changes when no changes to vault."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["rename_metadata", "review_changes", KeyError],
|
||||
@@ -471,7 +507,7 @@ def test_review_changes(test_application, mocker, capsys) -> None:
|
||||
def test_vault_backup(test_application, mocker, capsys) -> None:
|
||||
"""Test backing up a vault."""
|
||||
app = test_application
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["vault_actions", KeyError],
|
||||
@@ -492,7 +528,7 @@ def test_vault_delete(test_application, mocker, capsys, tmp_path) -> None:
|
||||
app = test_application
|
||||
backup_path = Path(tmp_path / "application.bak")
|
||||
backup_path.mkdir()
|
||||
app.load_vault()
|
||||
app._load_vault()
|
||||
mocker.patch(
|
||||
"obsidian_metadata.models.application.Questions.ask_application_main",
|
||||
side_effect=["vault_actions", KeyError],
|
||||
|
||||
@@ -6,13 +6,14 @@ author:: John Doe
|
||||
status:: new
|
||||
type:: book
|
||||
type:: article
|
||||
#food/fruit/apple
|
||||
#food/fruit/pear
|
||||
#dinner #lunch #breakfast
|
||||
on_one_note:: one
|
||||
#food/fruit/apple
|
||||
#food/fruit/pear
|
||||
#dinner #lunch #breakfast
|
||||
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
|
||||
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
|
||||
|
||||
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.
|
||||
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est, omnis dolor repellendus. Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus, ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat.
|
||||
|
||||
@@ -69,7 +69,7 @@ def test_vault_metadata(capsys) -> None:
|
||||
vm = VaultMetadata()
|
||||
assert vm.dict == {}
|
||||
|
||||
vm.add_metadata(METADATA)
|
||||
vm.index_metadata(METADATA)
|
||||
assert vm.dict == {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
@@ -97,7 +97,7 @@ def test_vault_metadata(capsys) -> None:
|
||||
assert captured.out == Regex(r"│ frontmatter_Key1 +│ author name +│")
|
||||
|
||||
new_metadata = {"added_key": ["added_value"], "frontmatter_Key2": ["new_value"]}
|
||||
vm.add_metadata(new_metadata)
|
||||
vm.index_metadata(new_metadata)
|
||||
assert vm.dict == {
|
||||
"added_key": ["added_value"],
|
||||
"frontmatter_Key1": ["author name"],
|
||||
@@ -115,7 +115,7 @@ def test_vault_metadata(capsys) -> None:
|
||||
def test_vault_metadata_contains() -> None:
|
||||
"""Test contains method."""
|
||||
vm = VaultMetadata()
|
||||
vm.add_metadata(METADATA)
|
||||
vm.index_metadata(METADATA)
|
||||
assert vm.dict == {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
@@ -141,7 +141,7 @@ def test_vault_metadata_contains() -> None:
|
||||
def test_vault_metadata_delete() -> None:
|
||||
"""Test delete method."""
|
||||
vm = VaultMetadata()
|
||||
vm.add_metadata(METADATA)
|
||||
vm.index_metadata(METADATA)
|
||||
assert vm.dict == {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
@@ -165,7 +165,7 @@ def test_vault_metadata_delete() -> None:
|
||||
def test_vault_metadata_rename() -> None:
|
||||
"""Test rename method."""
|
||||
vm = VaultMetadata()
|
||||
vm.add_metadata(METADATA)
|
||||
vm.index_metadata(METADATA)
|
||||
assert vm.dict == {
|
||||
"frontmatter_Key1": ["author name"],
|
||||
"frontmatter_Key2": ["article", "note"],
|
||||
|
||||
@@ -60,6 +60,14 @@ def test_validate_new_tag() -> None:
|
||||
assert questions._validate_new_tag("new_tag") is True
|
||||
|
||||
|
||||
def test_validate_number() -> None:
|
||||
"""Test number validation."""
|
||||
questions = Questions(vault=VAULT)
|
||||
assert "Must be an integer" in questions._validate_number("test")
|
||||
assert "Must be an integer" in questions._validate_number("1.1")
|
||||
assert questions._validate_number("1") is True
|
||||
|
||||
|
||||
def test_validate_existing_inline_tag() -> None:
|
||||
"""Test existing tag validation."""
|
||||
questions = Questions(vault=VAULT)
|
||||
@@ -80,12 +88,10 @@ def test_validate_key_exists_regex() -> None:
|
||||
def test_validate_value() -> None:
|
||||
"""Test value validation."""
|
||||
questions = Questions(vault=VAULT)
|
||||
assert questions._validate_value("test") is True
|
||||
assert "Value cannot be empty" in questions._validate_value("")
|
||||
|
||||
assert questions._validate_value("test") is True
|
||||
questions2 = Questions(vault=VAULT, key="frontmatter_Key1")
|
||||
assert questions2._validate_value("test") == "frontmatter_Key1:test does not exist"
|
||||
assert "Value cannot be empty" in questions2._validate_value("")
|
||||
assert questions2._validate_value("author name") is True
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from pathlib import Path
|
||||
|
||||
from obsidian_metadata._config import Config
|
||||
from obsidian_metadata.models import Vault
|
||||
from obsidian_metadata.models import Vault, VaultFilter
|
||||
from obsidian_metadata.models.enums import MetadataType
|
||||
from tests.helpers import Regex
|
||||
|
||||
@@ -20,7 +20,7 @@ def test_vault_creation(test_vault):
|
||||
assert vault.backup_path == Path(f"{vault_path}.bak")
|
||||
assert vault.dry_run is False
|
||||
assert str(vault.exclude_paths[0]) == Regex(r".*\.git")
|
||||
assert vault.num_notes() == 3
|
||||
assert len(vault.all_notes) == 3
|
||||
|
||||
assert vault.metadata.dict == {
|
||||
"Inline Tags": [
|
||||
@@ -64,16 +64,36 @@ def test_get_filtered_notes(sample_vault) -> None:
|
||||
vault_path = sample_vault
|
||||
config = Config(config_path="tests/fixtures/sample_vault_config.toml", vault_path=vault_path)
|
||||
vault_config = config.vaults[0]
|
||||
vault = Vault(config=vault_config, path_filter="front")
|
||||
|
||||
assert vault.num_notes() == 4
|
||||
filters = [VaultFilter(path_filter="front")]
|
||||
vault = Vault(config=vault_config, filters=filters)
|
||||
assert len(vault.all_notes) == 13
|
||||
assert len(vault.notes_in_scope) == 4
|
||||
|
||||
vault_path = sample_vault
|
||||
config = Config(config_path="tests/fixtures/sample_vault_config.toml", vault_path=vault_path)
|
||||
vault_config = config.vaults[0]
|
||||
vault2 = Vault(config=vault_config, path_filter="mixed")
|
||||
filters = [VaultFilter(path_filter="mixed")]
|
||||
vault = Vault(config=vault_config, filters=filters)
|
||||
assert len(vault.all_notes) == 13
|
||||
assert len(vault.notes_in_scope) == 1
|
||||
|
||||
assert vault2.num_notes() == 1
|
||||
filters = [VaultFilter(key_filter="on_one_note")]
|
||||
vault = Vault(config=vault_config, filters=filters)
|
||||
assert len(vault.all_notes) == 13
|
||||
assert len(vault.notes_in_scope) == 1
|
||||
|
||||
filters = [VaultFilter(key_filter="type", value_filter="book")]
|
||||
vault = Vault(config=vault_config, filters=filters)
|
||||
assert len(vault.all_notes) == 13
|
||||
assert len(vault.notes_in_scope) == 10
|
||||
|
||||
filters = [VaultFilter(tag_filter="brunch")]
|
||||
vault = Vault(config=vault_config, filters=filters)
|
||||
assert len(vault.all_notes) == 13
|
||||
assert len(vault.notes_in_scope) == 1
|
||||
|
||||
filters = [VaultFilter(tag_filter="brunch"), VaultFilter(path_filter="inbox")]
|
||||
vault = Vault(config=vault_config, filters=filters)
|
||||
assert len(vault.all_notes) == 13
|
||||
assert len(vault.notes_in_scope) == 0
|
||||
|
||||
|
||||
def test_backup(test_vault, capsys):
|
||||
|
||||
Reference in New Issue
Block a user