Sync Notion pages like Git commits. Smart content-based diffing, automatic rate limiting, zero headaches.
Project description
notion-sync-lib
Sync Notion pages like Git commits. Smart content-based diffing, automatic rate limiting, zero headaches.
Not another CRUD wrapper—this is a sync engine that understands your content and makes minimal changes automatically.
from notion_sync import get_notion_client, generate_diff, execute_diff
client = get_notion_client()
ops = generate_diff(current_blocks, new_blocks)
execute_diff(client, ops, page_id) # Magic happens ✨
What Can You Build With This?
📝 Keep Your Docs in Sync
Sync your GitHub README to Notion automatically. No more copy-paste. Update once, sync everywhere.
# Your CI pipeline
markdown = fetch_github_readme()
blocks = markdown_to_notion(markdown)
sync_to_notion(page_id, blocks) # Only updates what changed
🌍 Translation Workflows
Maintain 20 language versions of your docs. Update master → slaves sync in seconds, not hours.
# Example: sync NL master to EN/DE/FR translations
for lang in ["EN", "DE", "FR"]:
ops = generate_recursive_diff(master, translate(master, lang))
execute_recursive_diff(client, ops) # 10x faster than full sync
🏢 Workspace Migration
Moving 500 pages to a new workspace? Clone everything with preserved structure—toggles, columns, nested content, all intact.
# Clone entire workspace
for page_id in source_pages:
content = fetch_blocks_recursive(client_A, page_id)
clone_to_workspace_B(content) # All nested content preserved
📋 Template System
Generate 100 project pages from one template. Replace placeholders, customize layouts, done.
# Create project pages from template
template = fetch_blocks_recursive(client, template_page)
for project in projects:
customized = replace_placeholders(template, project)
create_page(project.name, customized)
📓 Obsidian/Markdown Sync
Daily sync from your markdown notes to Notion. Only changed files get updated.
# Sync markdown vault
for note in obsidian_vault:
if note.changed_today():
sync_to_notion(note) # Smart diff = minimal API calls
🤖 Automated Reports
Generate weekly reports with 3-column layouts, charts, and metrics—all programmatically.
# Build complex layouts
columns = [
{"children": [make_heading(2, "Summary"), *summary_blocks], "width_ratio": 0.5},
{"children": [make_heading(2, "Metrics"), *metrics], "width_ratio": 0.25},
{"children": [make_heading(2, "Charts"), chart], "width_ratio": 0.25}
]
create_column_list(client, report_page, columns)
Why This Library?
🧠 Smart Diff Engine (Like Git for Notion)
Traditional approach: Match blocks by position → Everything breaks when you add/remove a block.
Our approach: Match blocks by content → Robust to any structural change.
# You have: [A, B, C]
# You want: [A, X, B, C, D]
# Traditional: "Replace B→X, C→B, add C, add D" (4 operations)
# Smart diff: "Insert X after A, append D" (2 operations)
Result: Fewer API calls = faster syncs + lower rate limit risk.
⚡ Two Sync Modes for Different Needs
Structural Sync (generate_diff)
- Add, remove, reorder blocks freely
- Content-based matching with SequenceMatcher
- Use for: Documentation sync, markdown conversion, testing
Content-Only Sync (generate_recursive_diff)
- Update text in identical structures
- 10x faster (only UPDATE operations)
- Use for: Translation workflows, bulk text changes
🛡️ Production-Ready from Day One
- Automatic rate limiting: 3 req/sec with exponential backoff on 429 errors
- Smart batching: Handles 1000+ blocks automatically (100-block API limit)
- Resilient execution: Skips archived blocks, handles edge cases
- Request tracking: Monitor API usage with
client.request_count
🏗️ Build Complex Layouts Easily
from notion_sync import make_paragraph, make_heading, make_toggle, create_column_list
# Create nested structures
page_content = [
make_heading(1, "Project Overview"),
make_toggle("Details", children=[
make_paragraph("Hidden content..."),
make_bulleted_list_item("Nested item")
])
]
# Create column layouts with width ratios
columns = [
{"children": [make_paragraph("Left")], "width_ratio": 0.7},
{"children": [make_paragraph("Right")], "width_ratio": 0.3}
]
create_column_list(client, page_id, columns)
Installation
pip install git+https://github.com/mvletter/notion-sync-lib.git
Set your Notion API token:
export NOTION_API_TOKEN=secret_xxx
Or create a .env file:
NOTION_API_TOKEN=secret_xxx
Quick Examples
Sync Markdown to Notion
from notion_sync import get_notion_client, fetch_page_blocks, generate_diff, execute_diff
def sync_markdown_to_notion(markdown: str, page_id: str):
"""Convert markdown and sync to Notion in one go."""
client = get_notion_client()
# Convert markdown to Notion blocks (your converter)
new_blocks = markdown_to_notion_blocks(markdown)
# Fetch current state and generate diff
current_blocks = fetch_page_blocks(client, page_id)
ops = generate_diff(current_blocks, new_blocks)
# Execute minimal changes
stats = execute_diff(client, ops, page_id)
print(f"✅ Synced: {stats['inserted']} added, {stats['updated']} updated, {stats['deleted']} removed")
Translation Workflow (Content-Only Updates)
Perfect for maintaining translated pages:
from notion_sync import get_notion_client, fetch_blocks_recursive, generate_recursive_diff, execute_recursive_diff
client = get_notion_client()
# Fetch master page structure
master = fetch_blocks_recursive(client, master_page_id)
# Apply translations (preserve structure!)
translated = apply_translations(master, translations)
# Update only changed text (10x faster)
ops = generate_recursive_diff(master, translated)
stats = execute_recursive_diff(client, ops)
print(f"✅ Updated {stats['updated']} blocks")
Clone Page to Another Workspace
from notion_sync import get_notion_client, fetch_blocks_recursive, append_blocks
# Fetch from source workspace
client_A = get_notion_client() # Uses token from workspace A
content = fetch_blocks_recursive(client_A, source_page_id)
# Clone to target workspace
client_B = get_notion_client() # Uses token from workspace B
new_page_id = create_page_in_workspace_B(title)
append_blocks(client_B, new_page_id, content)
print(f"✅ Cloned page with {len(content)} blocks")
Preview Changes Before Applying
from notion_sync import generate_diff, format_diff_preview, execute_diff
ops = generate_diff(current_blocks, new_blocks)
# Show human-readable preview
print(format_diff_preview(ops))
# Output:
# ============================================================
# Diff Preview
# ============================================================
# Summary: 2 new, 1 modified, 0 replaced, 1 deleted, 5 unchanged
# ------------------------------------------------------------
#
# Changes:
#
# + [NEW] paragraph
# "This is new content"
# -> Will be inserted at position 3
#
# ~ [MODIFIED] heading_1
# "Old Title" -> "New Title"
# -> Will update block abc123...
# Execute after confirmation
if confirm():
stats = execute_diff(client, ops, page_id)
When to Use Which Diff?
| Your Situation | Use This | Why |
|---|---|---|
| Syncing markdown/docs to Notion | generate_diff |
Content may be added/removed/reordered |
| Translating existing pages | generate_recursive_diff |
Structure identical, only text changes |
| Migrating workspaces | generate_diff |
Flexible, handles any changes |
| Bulk text updates (find/replace) | generate_recursive_diff |
10x faster, updates only changed blocks |
| Building pages programmatically | Block builders + append_blocks |
Direct construction |
| Testing/prototyping | generate_diff + dry_run=True |
Preview mode |
Pro tip: When in doubt, use generate_diff. It handles everything.
Features
Core Capabilities
- ✅ Smart content-based diffing - Minimal API calls, like Git for Notion
- ✅ Two sync modes - Structural (flexible) + Content-only (fast)
- ✅ Automatic rate limiting - 3 req/sec with exponential backoff
- ✅ Recursive fetching - Get entire page trees with nested content
- ✅ Smart batching - Handles 1000+ blocks automatically
Advanced Features
- ✅ Column layout support - Create/read/unwrap with width ratios
- ✅ Block builders - 10+ block types (paragraphs, headings, toggles, code, etc.)
- ✅ Text extraction - 30+ block types → plain text
- ✅ TypedDict returns - Full IDE autocomplete
- ✅ Dry-run mode - Preview changes before applying
- ✅ Request tracking - Monitor API usage
Production-Ready
- ✅ Error resilience - Handles archived blocks, API errors gracefully
- ✅ Type safety - Full type hints (passes mypy strict mode)
- ✅ Comprehensive tests - 25 integration tests
- ✅ Well documented - Usage guide + API reference + pitfalls doc
Real-World Use Cases
| Use Case | Complexity | Demand | Key Feature |
|---|---|---|---|
| 📝 Documentation sync (GitHub → Notion) | Medium | 🔥🔥🔥 Very High | Smart diff |
| 🌍 Multi-language content management | High | 🔥🔥🔥 Very High | Recursive diff |
| 🏢 Workspace migration | Medium | 🔥🔥 High | Recursive fetch |
| 📋 Template system | Low | 🔥🔥 High | Block builders |
| 📓 Markdown sync (Obsidian/Roam) | Medium | 🔥🔥 High | Smart diff |
| 🔄 Bulk content transformation | High | 🔥 Medium | Recursive diff |
| 🤖 Automated page layouts | Low | 🔥 Medium | Column builders |
| 💾 Backup system | Low | 🔥 Medium | Text extraction |
| 📅 Meeting notes automation | Low | 🔥🔥 High | Block builders |
What Makes This Different?
Other Notion libraries:
# Manual position tracking, full page replacement
for i, block in enumerate(new_blocks):
client.update_block(old_blocks[i].id, block) # Breaks if count differs
This library:
# Content-based matching, minimal operations
ops = generate_diff(old_blocks, new_blocks)
execute_diff(client, ops, page_id) # Handles add/remove/reorder automatically
Result: Your code works when blocks are added/removed/reordered. No manual tracking.
Documentation
📖 Usage Guide - Complete examples and patterns 📚 API Reference - Full API documentation ⚠️ Common Pitfalls - Mistakes to avoid 🛠️ Development Guide - Contributing and testing
Requirements
- Python 3.10+
- Notion API token (get one here)
Contributing
We welcome contributions! See Development Guide for setup and testing.
Quick start:
git clone https://github.com/mvletter/notion-sync-lib.git
cd notion-sync-lib
pip install -e ".[dev]"
pytest -v
License
MIT License - see LICENSE for details.
Credits
Built by Mark Vletter for handling large-scale Notion translation workflows at Voys.
Inspired by Git's diff algorithm and the need for a production-ready Notion sync tool.
⭐ Star this repo if it saved you time!
🐛 Found a bug? Open an issue
💡 Have a use case? Share it in discussions
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file notion_sync_lib-1.2.3.tar.gz.
File metadata
- Download URL: notion_sync_lib-1.2.3.tar.gz
- Upload date:
- Size: 32.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c0acc7f1928fe28895f2396944751e87e0440acd9a5a9264fbcc93db33e36118
|
|
| MD5 |
b29a5fa28f6e1cc65118ef010d4f53e9
|
|
| BLAKE2b-256 |
a0dad60e1448d8e5fa9f1bd0d9799a9bcf82d705d0035ca61ab647a15d6558c7
|
Provenance
The following attestation bundles were made for notion_sync_lib-1.2.3.tar.gz:
Publisher:
publish.yml on mvletter/notion-sync-lib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
notion_sync_lib-1.2.3.tar.gz -
Subject digest:
c0acc7f1928fe28895f2396944751e87e0440acd9a5a9264fbcc93db33e36118 - Sigstore transparency entry: 962736872
- Sigstore integration time:
-
Permalink:
mvletter/notion-sync-lib@8d8dd5d1a4dea6d3d535f703d632e3f5e488207b -
Branch / Tag:
refs/tags/v1.2.3 - Owner: https://github.com/mvletter
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8d8dd5d1a4dea6d3d535f703d632e3f5e488207b -
Trigger Event:
release
-
Statement type:
File details
Details for the file notion_sync_lib-1.2.3-py3-none-any.whl.
File metadata
- Download URL: notion_sync_lib-1.2.3-py3-none-any.whl
- Upload date:
- Size: 38.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49beadd096761a94b0b742353214ef072cf6eb48795737c33f60fdd535205984
|
|
| MD5 |
9939b80603ad6b5c71c17a0c221ce6d1
|
|
| BLAKE2b-256 |
7f754672793fb6a3027ec4fff4963c3a9ad7736d67021fe9e3edfba67cb13822
|
Provenance
The following attestation bundles were made for notion_sync_lib-1.2.3-py3-none-any.whl:
Publisher:
publish.yml on mvletter/notion-sync-lib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
notion_sync_lib-1.2.3-py3-none-any.whl -
Subject digest:
49beadd096761a94b0b742353214ef072cf6eb48795737c33f60fdd535205984 - Sigstore transparency entry: 962736880
- Sigstore integration time:
-
Permalink:
mvletter/notion-sync-lib@8d8dd5d1a4dea6d3d535f703d632e3f5e488207b -
Branch / Tag:
refs/tags/v1.2.3 - Owner: https://github.com/mvletter
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@8d8dd5d1a4dea6d3d535f703d632e3f5e488207b -
Trigger Event:
release
-
Statement type: