From cfbca6cc1563df61e2f24a5ec7967b44a77cdd65 Mon Sep 17 00:00:00 2001 From: Andrew Yim Date: Sat, 27 Jun 2026 09:47:35 +0900 Subject: [PATCH] feat(notion-writer): markdown endpoint helpers + version-aware client Co-Authored-By: Claude Opus 4.8 (1M context) --- .../code/scripts/_notion_compat.py | 40 ++++++++++- .../code/scripts/test_engine_routing.py | 66 +++++++++++++++++++ 2 files changed, 103 insertions(+), 3 deletions(-) create mode 100644 custom-skills/32-notion-writer/code/scripts/test_engine_routing.py diff --git a/custom-skills/32-notion-writer/code/scripts/_notion_compat.py b/custom-skills/32-notion-writer/code/scripts/_notion_compat.py index 4f8b88f..281fda8 100644 --- a/custom-skills/32-notion-writer/code/scripts/_notion_compat.py +++ b/custom-skills/32-notion-writer/code/scripts/_notion_compat.py @@ -16,8 +16,11 @@ from notion_client import Client from notion_client.errors import APIErrorCode, APIResponseError -def make_client(api_key: str) -> Client: - """Build a sync Notion client on the SDK default API version (2025-09-03+).""" +def make_client(api_key: str, notion_version: str = None) -> Client: + """Build a sync Notion client. Pass notion_version to override the SDK + default (needed: 2026-03-11 for the markdown content endpoints).""" + if notion_version: + return Client(auth=api_key, notion_version=notion_version) return Client(auth=api_key) @@ -210,7 +213,10 @@ def explain_api_error(exc: APIResponseError, context: str = "") -> str: "https://www.notion.so/my-integrations." ) if code == APIErrorCode.ValidationError: - return f"Validation error{suffix}: {exc.body.get('message', str(exc))}" + msg = exc.body.get('message', str(exc)) + if 'delet' in msg.lower(): + msg += " — re-run with --allow-deleting-content to permit this." + return f"Validation error{suffix}: {msg}" if code == APIErrorCode.RateLimited: return f"Rate limited{suffix}. Back off and retry." return f"Notion API error [{code}]{suffix}: {exc}" @@ -253,3 +259,31 @@ def find_existing_page( ) results = response.get("results") or [] return results[0] if results else None + + +def create_page_markdown(client, parent, properties, markdown): + """POST /v1/pages with the enhanced-markdown body param.""" + return client.request( + path="pages", method="POST", + body={"parent": parent, "properties": properties, "markdown": markdown}, + ) + + +def append_markdown(client, page_id, markdown): + """PATCH /v1/pages/:id/markdown — append at end (insert_content).""" + return client.request( + path=f"pages/{page_id}/markdown", method="PATCH", + body={"type": "insert_content", + "insert_content": {"content": markdown, + "position": {"type": "end"}}}, + ) + + +def replace_markdown(client, page_id, markdown, allow_deleting=False): + """PATCH /v1/pages/:id/markdown — replace all content.""" + return client.request( + path=f"pages/{page_id}/markdown", method="PATCH", + body={"type": "replace_content", + "replace_content": {"new_str": markdown, + "allow_deleting_content": allow_deleting}}, + ) diff --git a/custom-skills/32-notion-writer/code/scripts/test_engine_routing.py b/custom-skills/32-notion-writer/code/scripts/test_engine_routing.py new file mode 100644 index 0000000..544c7f2 --- /dev/null +++ b/custom-skills/32-notion-writer/code/scripts/test_engine_routing.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Tests for markdown transport helpers — run with `python test_engine_routing.py`.""" + +import sys +from pathlib import Path +from unittest import mock + +sys.path.insert(0, str(Path(__file__).parent)) + +import _notion_compat as compat + + +def _fake_client(): + c = mock.MagicMock() + c.request.return_value = {"object": "page", "id": "p1"} + return c + + +def test_create_page_markdown(): + c = _fake_client() + parent = {"type": "data_source_id", "data_source_id": "ds1"} + compat.create_page_markdown(c, parent, {"Name": {"title": []}}, "# Hi") + _, kwargs = c.request.call_args + assert kwargs["path"] == "pages" + assert kwargs["method"] == "POST" + assert kwargs["body"]["markdown"] == "# Hi" + assert kwargs["body"]["parent"] == parent + + +def test_append_markdown(): + c = _fake_client() + compat.append_markdown(c, "page-123", "## More") + _, kwargs = c.request.call_args + assert kwargs["path"] == "pages/page-123/markdown" + assert kwargs["method"] == "PATCH" + assert kwargs["body"]["type"] == "insert_content" + assert kwargs["body"]["insert_content"]["content"] == "## More" + assert kwargs["body"]["insert_content"]["position"] == {"type": "end"} + + +def test_replace_markdown(): + c = _fake_client() + compat.replace_markdown(c, "page-123", "# Fresh", allow_deleting=True) + _, kwargs = c.request.call_args + assert kwargs["path"] == "pages/page-123/markdown" + assert kwargs["body"]["type"] == "replace_content" + assert kwargs["body"]["replace_content"]["new_str"] == "# Fresh" + assert kwargs["body"]["replace_content"]["allow_deleting_content"] is True + + +def run_all(): + tests = [ + test_create_page_markdown, + test_append_markdown, + test_replace_markdown, + ] + for t in tests: + print(f"\n{t.__name__}") + t() + print("\n" + "=" * 50) + print(f"✅ All {len(tests)} tests passed") + print("=" * 50) + + +if __name__ == "__main__": + run_all()