Skip to main content

Git-shaped sync between Notion database views and folders of markdown files

Project description

n-commit

Git-shaped sync between Notion database views and local folders of markdown files. Notion is the source of truth; you decide when to pull and push.

Install

pip install n-commit

First call opens a browser for OAuth. Tokens cache at ~/.config/n-commit/tokens/.

Onboarding

mkdir my-notion && cd my-notion

# one-shot: folder name is derived from the Notion title
n-commit add "https://www.notion.so/…?v=…"
n-commit add "https://www.notion.so/…?v=…" --as crm/people    # explicit folder

# batch: paste URLs in a loop (blank line or Ctrl-C ends)
n-commit add -i

# the first `add` creates n-commit.toml + .gitignore

To use the same workspace on another machine: copy n-commit.toml to a folder and run n-commit pull. (Git, scp, Dropbox — anything that moves a 1KB text file.)

Mental model

A workspace is a directory tree containing one n-commit.toml manifest at its root and one subfolder per binding. Each binding mirrors one Notion database view.

~/my-notion/
├── n-commit.toml          ← the manifest (committed)
├── .gitignore                ← ignores **/.notion/
├── interviews/
│   ├── _FIELDS.md            ← cheat sheet, regenerated on bind
│   ├── jane.md               ← row ↔ file
│   └── .notion/              ← cache (gitignored)
│       ├── schema.json
│       ├── fingerprints.json
│       └── mirror/jane.md    ← last-pulled bytes
└── crm/people/
    ├── _FIELDS.md
    ├── alice.md
    └── .notion/

Manifest:

[folders."interviews"]
view = "https://www.notion.so/…?v=…"

[folders."crm/people"]
view = "https://www.notion.so/…?v=…"

Workspace is found by walking up from cwd, like git finds .git/. One file describes every binding.

Verbs

add, remove, status, pull, push, restore. Run n-commit <verb> --help for the per-verb reference. API-call cost per verb:

Verb Reads remote Writes remote Writes local Default API calls
add yes (one-time) no yes 1 list_rows + 1 fetch_schema (+ N fetch_row if --init=push)
status only --remote no no 0 (or 1 list_rows per binding)
pull yes no yes (clean only) 1 list_rows per binding + N fetch_row (cache misses only)
push yes (drift check) yes yes (echo) 3/file (drift+update+echo); 2 with --force
restore no no yes 0

Conflict / drift playbook

  • pullConflictError (dirty file + remote also changed):
    • keep local: push <file> (will DriftError if remote also moved → --force)
    • take remote: restore <file> then re-pull
    • both diverged: open file, mirror at .notion/mirror/<rel>, hand-merge, push
  • pushDriftError (mirror diverged from current remote):
    • keep local: pull --force, re-merge, push. Or push --force.
    • take remote: restore <file>
  • status --remote surfaces conflicts early — cheapest place to catch them.

Layout

File Role
n_commit/core.py The five primitives + CLI entrypoint. All filesystem IO.
n_commit/manifest.py n-commit.toml read/write/find. Pure, no Notion deps.
n_commit/frontmatter.py Pure transforms: envelope parse, props ↔ YAML, body normalize, row fingerprint, slug.
n_commit/client.py Typed Notion MCP wrapper.
n_commit/auth.py Generic MCP OAuth (PKCE) with fcntl.flock to serialize refresh-token rotation.
n_commit/mcp_client.py Generic MCP transport + dynamic tool proxy.

Invariants

  • fetch_row is the single source of truth for row bytes. Never mix list_rows props with envelope-fetch props in the same path — they disagree on edge keys.
  • Mirror is always updated on pull, even on conflict. Working files are only overwritten if clean (or --force). Conflicts surface loudly via ConflictError.
  • Canonicalization for hashing. normalize_body strips presigned-S3 query strings so attachment URLs don't cause drift. CRLF → LF. Trailing whitespace stripped. Triple-newlines collapsed.
  • Read-only types are stripped on push: created_time, last_edited_time, rollup, formula.
  • notion_id is always 32-char undashed hex. Normalize at boundaries via url_to_id.
  • Comments don't bump Last edited time. Use pull <file> (always fetches) when you want comments.
  • query-database-view caps at 100 rows. Larger views truncate silently — pull <file> is the escape hatch for the tail.
  • Empty list_rows is a no-op. Not "everything was deleted".
  • OAuth refresh tokens are single-use. FileTokenStorage uses fcntl.flock so concurrent sessions don't race.
  • create_row validates the parent. Rejected property bags silently produce orphan pages on Notion's side; create_row re-fetches and verifies the parent matches.

Practices

  • frontmatter.py and manifest.py are pure. No IO outside their declared file.
  • core.py's primitives take view_url as an argument; only the CLI reads the manifest.
  • No imports inside functions. Module top only.
  • Explicit over compact. Readable loops beat dense comprehensions.

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

n_commit-0.5.0.tar.gz (34.3 kB view details)

Uploaded Source

Built Distribution

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

n_commit-0.5.0-py3-none-any.whl (39.2 kB view details)

Uploaded Python 3

File details

Details for the file n_commit-0.5.0.tar.gz.

File metadata

  • Download URL: n_commit-0.5.0.tar.gz
  • Upload date:
  • Size: 34.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for n_commit-0.5.0.tar.gz
Algorithm Hash digest
SHA256 850b6c99053791f457a10640a53178f5ac1368d3a28e81afc1f484ea1c7231b2
MD5 ab2c4a299e69b300f2127ee76caa265b
BLAKE2b-256 da5e32c9e8a26239cf15d10f1b0c18eb54d0f5e85fb8f41dbc2cfb8d7ec36d28

See more details on using hashes here.

File details

Details for the file n_commit-0.5.0-py3-none-any.whl.

File metadata

  • Download URL: n_commit-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 39.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.3

File hashes

Hashes for n_commit-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 682aa0dc939ce17bf43272251cd4855c566d8931b203de4fe9e2b55c2a7b0fdd
MD5 8ac85c5aecd741a70b3df4c96682f758
BLAKE2b-256 6a4e4191b00a059ddb8a6dc69d2dcbb45d66da08db88e6ba879bcdde9b8e1e13

See more details on using hashes here.

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