Skip to main content

Sync Notion pages like Git commits. Smart content-based diffing, automatic rate limiting, zero headaches.

Project description

notion-sync-lib

Python 3.10+ License: MIT

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


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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

notion_sync_lib-1.1.1.tar.gz (30.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

notion_sync_lib-1.1.1-py3-none-any.whl (36.8 kB view details)

Uploaded Python 3

File details

Details for the file notion_sync_lib-1.1.1.tar.gz.

File metadata

  • Download URL: notion_sync_lib-1.1.1.tar.gz
  • Upload date:
  • Size: 30.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for notion_sync_lib-1.1.1.tar.gz
Algorithm Hash digest
SHA256 72572f0c023312c74923083f266ad104f086f6398d45344e4bc9030ed22c5290
MD5 550da892ec0bcc54290b4c455a7faee0
BLAKE2b-256 7e666ea05be0f36ba1ac8f1a059029f382ced2af2fa89d70db9879994bca4f14

See more details on using hashes here.

Provenance

The following attestation bundles were made for notion_sync_lib-1.1.1.tar.gz:

Publisher: publish.yml on mvletter/notion-sync-lib

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file notion_sync_lib-1.1.1-py3-none-any.whl.

File metadata

File hashes

Hashes for notion_sync_lib-1.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 164a99d6aa9eba2e95358f8943c9556be3945d72b1edb5935f115d4088a35da0
MD5 92aedb5117ba37c6fc40e11cc11beb2c
BLAKE2b-256 07a56bb02325e3069e85ea9eb26c37b070a0b48ab944c3447b7c20d3b136f58c

See more details on using hashes here.

Provenance

The following attestation bundles were made for notion_sync_lib-1.1.1-py3-none-any.whl:

Publisher: publish.yml on mvletter/notion-sync-lib

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page