diff --git a/README.md b/README.md index 14c5092..4394bb9 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ pip install obsidian-metadata - `--dry-run`: Make no destructive changes - `--export-csv`: Specify a path and create a CSV export of all metadata - `--export-json`: Specify a path and create a JSON export of all metadata +- `--export-template`: Specify a path and export all notes with their associated metadata to a CSV file for use as a bulk import template - `--help`: Shows interactive help and exits - `--log-file`: Specify a log file location - `--log-to-file`: Will log to a file @@ -64,7 +65,7 @@ Once installed, run `obsidian-metadata` in your terminal to enter an interactive - **List and clear filters**: List all current filters and clear one or all - **List notes in scope**: List notes that will be processed. -**Bulk Edit Metadata** from a CSV file (See the _making bulk edits_ section below) +**Bulk Edit Metadata** from a CSV file (See the _[Make Bulk Updates](https://github.com/natelandau/obsidian-metadata#make-bulk-updates)_ section below) **Add Metadata**: Add new metadata to your vault. @@ -139,7 +140,7 @@ Below is an example with two vaults. To bypass the configuration file and specify a vault to use at runtime use the `--vault-path` option. -### Making bulk edits +### Make Bulk Updates Bulk edits are supported by importing a CSV file containing the following columns. Column headers must be lowercase. @@ -167,7 +168,10 @@ How bulk imports work: - **Existing metadata in a matching note will be rewritten**. This may result in it's location and/or formatting within the note being changed - Inline tags ignore any value added to the `key` column -You can export all your notes with their associated metadata in this format from the "Export Metadata" section of the script to be used as a template for your bulk changes. +Create a CSV template for making bulk updates containing all your notes and their associated metadata by + +1. Using the `--export-template` cli command; or +2. Selecting the `Metadata by note` option within the `Export Metadata` section of the app # Contributing diff --git a/poetry.lock b/poetry.lock index 31193c9..dc75305 100644 --- a/poetry.lock +++ b/poetry.lock @@ -4,7 +4,7 @@ name = "argcomplete" version = "2.0.6" description = "Bash tab completion for argparse" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -100,7 +100,7 @@ files = [ name = "charset-normalizer" version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" +category = "dev" optional = false python-versions = ">=3.6.0" files = [ @@ -142,7 +142,7 @@ files = [ name = "commitizen" version = "2.42.1" description = "Python commitizen client tool" -category = "main" +category = "dev" optional = false python-versions = ">=3.6.2,<4.0.0" files = [ @@ -231,7 +231,7 @@ toml = ["tomli"] name = "decli" version = "0.5.2" description = "Minimal, easy-to-use, declarative cli tool" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -354,7 +354,7 @@ tests = ["pytest", "pytest-cov", "pytest-mock"] name = "jinja2" version = "3.1.2" description = "A very fast and expressive template engine." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -416,7 +416,7 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] name = "markupsafe" version = "2.1.2" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -562,7 +562,7 @@ setuptools = "*" name = "packaging" version = "23.0" description = "Core utilities for Python packages" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -596,14 +596,14 @@ files = [ [[package]] name = "pdoc" -version = "13.0.0" +version = "13.0.1" description = "API Documentation for Python Projects" category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "pdoc-13.0.0-py3-none-any.whl", hash = "sha256:f9088b1c10f3296f46a08796e05e307470af5f4253f71d536781f6c305baf912"}, - {file = "pdoc-13.0.0.tar.gz", hash = "sha256:aadbf6c757c6e65c4754d6c26c4eb6c1bf8c7a9fb893f1fbe5a7b879dde59e46"}, + {file = "pdoc-13.0.1-py3-none-any.whl", hash = "sha256:16a24914280ed318896ad798674e2b0d11832297fdea95632fa472e3d171e247"}, + {file = "pdoc-13.0.1.tar.gz", hash = "sha256:4d84056847728203b8789ca8a8d0c8003f25002b3caef3365f6f21a1e4228a1b"}, ] [package.dependencies] @@ -839,7 +839,7 @@ testing = ["filelock"] name = "pyyaml" version = "6.0" description = "YAML parser and emitter for Python" -category = "main" +category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1172,7 +1172,7 @@ widechars = ["wcwidth"] name = "termcolor" version = "2.2.0" description = "ANSI color formatting for output in terminal" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1275,7 +1275,7 @@ files = [ name = "typing-extensions" version = "4.5.0" description = "Backported and Experimental Type Hints for Python 3.7+" -category = "main" +category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1349,4 +1349,4 @@ dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "e9e2ff35a5ae15991d1d123dffa9f15fdf5afaf00624c26577412555d0464eaf" +content-hash = "8fa62f96cc77eac773497573dcbdd5666173cbec56374fea73a814f3fb7f5338" diff --git a/pyproject.toml b/pyproject.toml index c0b3e8f..e7e6753 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,6 @@ shellingham = "^1.5.0.post1" tomlkit = "^0.11.6" typer = "^0.7.0" - commitizen = "^2.42.1" [tool.poetry.group.test.dependencies] pytest = "^7.2.2" @@ -41,14 +40,14 @@ coverage = "^7.2.2" interrogate = "^1.5.0" mypy = "^1.1.1" - pdoc = "^13.0.0" + pdoc = "^13.0.1" poethepoet = "^0.18.1" pre-commit = "^3.2.0" - ruff = "0.0.257" + ruff = "^0.0.257" + sh = "2.0.3" typeguard = "^3.0.1" types-python-dateutil = "^2.8.19.10" vulture = "^2.7" - sh = "2.0.3" [tool.black] line-length = 100 diff --git a/scripts/update_dependencies.py b/scripts/update_dependencies.py index fb00d30..479ab90 100755 --- a/scripts/update_dependencies.py +++ b/scripts/update_dependencies.py @@ -143,7 +143,7 @@ for group in groups: notice( f"Updating {p} from {packages[p]['current_version']} to {packages[p]['new_version']}" ) - sh.poetry("add", f"{p}@{packages[p]['new_version']}", "--group", group, _fg=True) + sh.poetry("add", f"{p}@latest", "--group", group, _fg=True) sh.poetry("update", _fg=True) success("All dependencies are up to date") diff --git a/src/obsidian_metadata/cli.py b/src/obsidian_metadata/cli.py index 7b5caf4..ed799a2 100644 --- a/src/obsidian_metadata/cli.py +++ b/src/obsidian_metadata/cli.py @@ -34,14 +34,21 @@ def main( ), export_csv: Path = typer.Option( None, - help="Exports all metadata to a specified CSV file and exits. (Will overwrite any existing file)", + help="Exports all metadata to a specified CSV file and exits.", show_default=False, dir_okay=False, file_okay=True, ), export_json: Path = typer.Option( None, - help="Exports all metadata to a specified JSON file and exits. (Will overwrite any existing file)", + help="Exports all metadata to a specified JSON file and exits.", + show_default=False, + dir_okay=False, + file_okay=True, + ), + export_template: Path = typer.Option( + None, + help="Exports all notes and their metadata to a specified CSV file and exits. Use to create a template for batch updates.", show_default=False, dir_okay=False, file_okay=True, @@ -142,6 +149,10 @@ def main( path = Path(export_json).expanduser().resolve() application.noninteractive_export_csv(path) raise typer.Exit(code=0) + if export_template is not None: + path = Path(export_template).expanduser().resolve() + application.noninteractive_export_csv(path) + raise typer.Exit(code=0) application.application_main() diff --git a/tests/cli_test.py b/tests/cli_test.py index 9389934..4638d1e 100644 --- a/tests/cli_test.py +++ b/tests/cli_test.py @@ -17,7 +17,7 @@ def test_version() -> None: """Test printing version and then exiting.""" result = runner.invoke(app, ["--version"]) assert result.exit_code == 0 - assert result.output == Regex(r"obsidian_metadata: v\d+\.\d+\.\d+$") + assert "obsidian_metadata: v" in result.output def test_application(tmp_path) -> None: @@ -51,3 +51,25 @@ def test_application(tmp_path) -> None: assert banner in result.output assert result.exit_code == 1 + + +def test_export_template(tmp_path) -> None: + """Test the export template command.""" + source_dir = Path(__file__).parent / "fixtures" / "test_vault" + dest_dir = Path(tmp_path / "vault") + + if not source_dir.exists(): + raise FileNotFoundError(f"Sample vault not found: {source_dir}") + + shutil.copytree(source_dir, dest_dir) + + config_path = tmp_path / "config.toml" + export_path = tmp_path / "export_template.csv" + result = runner.invoke( + app, + ["--vault-path", dest_dir, "--config-file", config_path, "--export-template", export_path], + ) + + assert "SUCCESS | Exported metadata to" in result.output + assert result.exit_code == 0 + assert export_path.exists()