Yo! Welcome to my stash of Python scripts built to supercharge Obsidian! 🚀
These scripts leverage the power of the Obsidian Python Bridge V2 plugin — the thing that lets Python folks automate and extend Obsidian without touching JavaScript/TypeScript.
This repo is also a "why Python?" showcase: half of these scripts do things that would be genuinely painful in TypeScript — hand-rolled TF-IDF and PageRank over your vault, executable code blocks, checksum algorithms, fuzzy diffing — most of it with zero third-party dependencies, just the standard library doing what it does best.
Everything is:
- ✅ aligned with the current V2 API (verified against the bridge source),
- ✅ following the recommended script structure (
define_settings/_handle_cli_args, event guard), - ✅ covered by a real unit-test suite plus integration tests running against the actual bridge library,
- ✅ linted & formatted with ruff.
- Install & enable the Obsidian Python Bridge V2 plugin.
- Point its Python scripts folder setting at a folder containing these scripts.
- Ensure Python 3.x +
requestsare available (the plugin checks on startup). - Run scripts from the command palette; configure their settings in the plugin tab.
Optional dependencies unlock extra powers:
pip install pandas matplotlib # vault-analytics: DataFrame stats + PNG chart
pip install pypdf # pdf-to-note
pip install qrcode[pil] # qr-code-generator
pip install pytesseract pillow # ocr-image-transcriber (+ system tesseract binary)Every optional-dependency script degrades gracefully: missing deps produce a friendly notification, never a crash.
1. script-auto-linker.py — V2.4, the heavy artillery
Scans the active note for text matching other note titles in your vault and links it automatically.
- Configurable link type: piped wikilink
[[Title|match]], simple[[Title]], or markdown[match](path). - Case-preserving aliases, accent-insensitive matching, punctuation handling — all exposed as UI settings.
- Find-all-then-replace strategy: longest titles win, overlaps resolved deterministically, frontmatter preserved.
- Orchestration in injectable
run(client); every pure helper unit-tested.
Known limitations (pinned by tests, preserved on purpose):
| Limitation | Detail |
|---|---|
] never counts as trailing punctuation |
The punctuation class \[\s.,;!?()] contains an unescaped ], which silently closes the class. A title directly followed by ] produces no match at all. Side-effect: existing wikilinks/markdown links ending in brackets are naturally protected — fragile, so we pin rather than silently "fix". |
| Conservative fenced-code heuristic | is_inside_link_or_code treats everything after a closed ``` block as still-inside code. One fenced block ⇒ rest of the note is skipped. |
| Midpoint protection check | Matches straddling two structures can slip through the inside-link test. |
| Ligatures are not accents | NFD doesn't decompose ligatures: cœur never matches coeur. |
Finds simple links [[My Note]] in the active note and pipes them into [[My Note|My Note]]. Frontmatter untouched; pure transform in process_lines(lines) -> (new_lines, changed).
3. define_word_en_concise.py — the minimal demo
Select an English word → definition from dictionaryapi.dev in a notification. Intentionally tiny, but robust: malformed payloads degrade gracefully instead of crashing.
| Script | The flex | Deps |
|---|---|---|
semantic-similar-notes.py |
Hand-rolled TF-IDF + cosine similarity across the whole vault; suggests related notes you haven't linked yet. Pure stdlib math. | none |
link-graph-insights.py |
Google's PageRank, implemented by hand on your vault's link graph + degree centrality + orphan detection → dashboard note. No networkx. | none |
executable-code-blocks.py |
Jupyter-lite: ```python #run fences execute, stdout captured into idempotent result callouts. |
none |
fuzzy-duplicate-finder.py |
Near-duplicate notes via difflib.SequenceMatcher + exact body clones via MD5 fingerprints. |
none |
csv-to-markdown-table.py |
Select raw CSV → aligned Markdown table. Delimiter sniffing, quoted fields, pipe escaping — stdlib csv flex. |
none |
isbn-book-card.py |
ISBN selection → card with cover. Hand-coded ISBN-10/13 checksum validation + OpenLibrary/GoogleBooks mashup. | requests |
vault-analytics.py |
Word-count analytics dashboard. With pandas/matplotlib: real stats + PNG histogram; without: pure-stdlib fallback report. | optional pandas+matplotlib |
qr-code-generator.py |
Text/link → QR PNG in attachments, embedded in note. Shows bridge FS ops (check_path_exists, create_folder). |
optional qrcode[pil] |
pdf-to-note.py |
PDF embeds in the active note → sibling extracted markdown notes with per-page sections + heading heuristic. | optional pypdf |
ocr-image-transcriber.py |
Image embeds get Tesseract transcriptions injected as idempotent callouts. Engine injectable = fully testable. | optional pytesseract+pillow |
Common pattern across all ten: pure core functions + injectable run(client) — every script ships with its own test file, heavy engines are injectable seams, optional imports degrade loudly-but-politely.
Layout:
├── *.py ← 13 scripts at the root (the bridge discovers .py files)
├── pyproject.toml # metadata + ruff + pytest config
├── tests/
│ ├── conftest.py # bridge stub injected before any import + fake client fixtures
│ ├── test_*.py # one suite per script
│ └── test_bridge_integration.py # subprocess runs against the REAL bridge library
├── LICENSE
└── README.md
Commands (uv manages everything):
uv sync # create .venv + dev tools (ruff, pytest)
uv run ruff format . # format
uv run ruff check . # lint (zero violations expected)
uv run pytest # whole suite (~5 s, no Obsidian needed)How tests work without Obsidian running:
tests/conftest.pyinjects a stub ofObsidianPluginDevPythonToJSintosys.modulesbefore any script import, then hands out a configurable mock client (bind_client(mod)+fake_bridge).test_bridge_integration.pyexecutes scripts in subprocesses against a local clone of the bridge repo, verifying the event guard exits 0 and--get-settings-jsonprints valid definitions using the real current library. Self-skips otherwise.
💡 Found while testing: some recent bridge builds' shim does not re-export
is_handling_event()even though docs mention it. These scripts therefore use a graceful fallback chain (shim → package module → plain env check).
Issues and PRs welcome! For questions about the bridge plugin itself, head to mathe00/obsidian-plugin-python-bridge.
Happy scripting! 🤘