obsidian-parse
Extract knowledge graph metadata from Obsidian-style markdown vaults.
A helper library for Obsidian parsing — covering only what standard code can't: Obsidian-specific syntax (wikilinks, embeds, nested tags), vault ignore rules, shortest-path link resolution, and graph output. Filesystem traversal, text search, and other general-purpose tasks are intentionally left to the caller.
Parses .md, .canvas, and .base files to extract wikilinks, embeds, tags, and frontmatter — then converts them into a D3-compatible graph structure for visualization or downstream analysis.
Installation
uv add obsidian-parse
Or with pip:
pip install obsidian-parse
Quick Start
from obsidian_parse import parse, results_to_d3
# Parse an entire vault directory
results = parse(["/path/to/your/vault"])
# Convert to D3 graph format
graph = results_to_d3(results)
# graph = {"nodes": [...], "links": [...]}
What It Extracts
| Element | Syntax | Example |
|---|---|---|
| WikiLink | [[Note]] |
[[Project Ideas|alias]] |
| Embed | ![[file]] |
![[image.png]] |
| Tag | #tagname |
#topic/subtopic |
| Frontmatter | YAML header | tags: [python, tools] |
Extraction is block-aware: wikilinks and tags inside code fences or HTML blocks are intentionally ignored.
API
parse(paths)
Accepts a list of file or directory paths. Directories are scanned recursively. Respects .obsidian/app.json ignore rules and skips dotfiles/dotfolders.
Returns a list of ParseResult objects.
Raises:
NoPathsProvidedError— ifpathsis emptyPathNotFoundError— if none of the paths existNoMarkdownFilesError— if paths exist but contain no parseable files
ParseResult
| Property | Type | Description |
|---|---|---|
file_id |
str |
Filename used as node ID — .md extension omitted, .canvas/.base kept (e.g. "Note", "Board.canvas") |
path |
Path |
Original file path |
frontmatter |
dict |
Parsed YAML frontmatter |
wikilinks |
list[WikiLink] |
Wikilinks with line/col positions |
embeds |
list[Embed] |
Embeds with line/col positions |
tags |
list[TagRef] |
Tags with line/col positions |
wikilink_targets |
list[str] |
Deduplicated link targets (computed) |
embed_targets |
list[str] |
Deduplicated embed targets (computed) |
tag_names |
list[str] |
Merged body + frontmatter tags (computed) |
parse_file(file_path)
Parses a single file by dispatching to the correct parser based on extension.
Raises UnsupportedFileTypeError for unregistered extensions.
parse_markdown_file(file_path)
Reads and parses a single .md file directly, returning a ParseResult.
parse_markdown_content(content, file_id, path)
Parses raw markdown string content without reading from disk. Useful for testing or in-memory workflows.
WikiLink
| Field | Type | Description |
|---|---|---|
target |
str |
Link target — .md extension omitted, other extensions kept (e.g. "Note", "Board.canvas") |
section |
str | None |
Heading (#Section) or block ref (^id) |
alias |
str | None |
Display alias after | |
line |
int | None |
Source line number |
col |
int | None |
Source column number |
Embed
| Field | Type | Description |
|---|---|---|
target |
str |
Embed target filename — .md extension omitted, other extensions kept |
section |
str | None |
Heading or block id |
line |
int | None |
Source line number |
col |
int | None |
Source column number |
TagRef
| Field | Type | Description |
|---|---|---|
name |
str |
Tag name without leading # |
line |
int | None |
Source line number |
col |
int | None |
Source column number |
find_file_by_id(vault_root, file_id, *, known_files=None)
Resolves a file_id to a path relative to vault_root.
- Bare stem or
"stem.md"→ matches.mdfiles only "stem.canvas"/"stem.base"→ matches that exact extension
When multiple files match, the shallowest path wins (Obsidian's shortest-path behavior). Pass known_files (from discover_files()) to avoid repeated filesystem traversal.
Returns a Path relative to vault_root, or None if no file is found.
from pathlib import Path
from obsidian_parse import find_file_by_id
vault = Path("/path/to/vault")
find_file_by_id(vault, "Note") # → Path("folder/Note.md") or None
find_file_by_id(vault, "Board.canvas") # → Path("Board.canvas") or None
expand_nested_tag(tag)
Expands a nested tag string into all ancestor tags.
from obsidian_parse.utils.tags import expand_nested_tag
expand_nested_tag("a/b/c") # ["a", "a/b", "a/b/c"]
expand_nested_tag("/foo/bar") # ["/foo", "/foo/bar"]
expand_nested_tag("a//b/c") # ["a", "a//b", "a//b/c"]
The first / of each consecutive slash run is the hierarchy separator; remaining slashes become part of the next segment's name. A leading slash run is part of the first segment's name, never a separator.
results_to_d3(results)
Converts a list of ParseResult into a dict:
{
"nodes": [
{"id": "note-a", "type": "file", "label": "note-a"},
{"id": "#python", "type": "tag", "label": "python"},
],
"links": [
{"source": "note-a", "target": "note-b", "relation": "wikilink"},
{"source": "note-a", "target": "#python", "relation": "tag"},
{"source": "#python/tools", "target": "#python", "relation": "parent"},
]
}
Link relations: wikilink, embed, tag, parent (tag hierarchy).
Supported File Types
.md— Markdown with YAML frontmatter.canvas— Obsidian canvas JSON; extracts wikilinks from file-type nodes and all elements from text nodes.base— Obsidian base files; recorded as graph nodes (filename/path only, no link extraction)
Development
# Install with dev dependencies
uv sync --group dev
# Lint
ruff check src/
# Type check
mypy src/
License
MIT
Metadata
Release files for obsidian-parse 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| obsidian_parse-0.1.1.tar.gz | 13.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| obsidian_parse-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.5 kB
Release files / obsidian_parse-0.1.1.tar.gz
| Download URL | obsidian_parse-0.1.1.tar.gz |
|---|---|
| Size | 13.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
3fee471365c70814f71764bc445ea72b9183a2e3373587c8f5ceb40cfa18d87a
|
|
BLAKE2b-256 checksum How to use checksums |
c3a3d5f94c5ae658096424f9b9814356cc3a580ab625ee6380c469c7135266b6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / obsidian_parse-0.1.1-py3-none-any.whl
| Download URL | obsidian_parse-0.1.1-py3-none-any.whl |
|---|---|
| Size | 19.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
af8d237d40fb700077bebe1d9b9ad1da9d73a9bc85ddc576a71f7cb3e6c2b5c0
|
|
BLAKE2b-256 checksum How to use checksums |
872732d5f46f2f97e82ee3b485e327bceebc361a1aedac407ed3e3525aaa2f51
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|