feat(notion-writer): markdown endpoint helpers + version-aware client

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 09:47:35 +09:00
parent c5a6c3cda0
commit aeba10cea4
2 changed files with 103 additions and 3 deletions

View File

@@ -16,8 +16,11 @@ from notion_client import Client
from notion_client.errors import APIErrorCode, APIResponseError from notion_client.errors import APIErrorCode, APIResponseError
def make_client(api_key: str) -> Client: def make_client(api_key: str, notion_version: str = None) -> Client:
"""Build a sync Notion client on the SDK default API version (2025-09-03+).""" """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) return Client(auth=api_key)
@@ -210,7 +213,10 @@ def explain_api_error(exc: APIResponseError, context: str = "") -> str:
"https://www.notion.so/my-integrations." "https://www.notion.so/my-integrations."
) )
if code == APIErrorCode.ValidationError: 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: if code == APIErrorCode.RateLimited:
return f"Rate limited{suffix}. Back off and retry." return f"Rate limited{suffix}. Back off and retry."
return f"Notion API error [{code}]{suffix}: {exc}" return f"Notion API error [{code}]{suffix}: {exc}"
@@ -253,3 +259,31 @@ def find_existing_page(
) )
results = response.get("results") or [] results = response.get("results") or []
return results[0] if results else None 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}},
)

View File

@@ -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()