notionary 0.2.6__py3-none-any.whl → 0.2.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notionary/database/database_discovery.py +24 -24
- notionary/elements/code_block_element.py +59 -18
- notionary/elements/column_element.py +353 -0
- notionary/elements/divider_element.py +1 -1
- notionary/elements/heading_element.py +4 -1
- notionary/elements/registry/block_registry.py +1 -1
- notionary/elements/registry/block_registry_builder.py +8 -0
- notionary/page/content/page_content_writer.py +103 -3
- notionary/page/markdown_to_notion_converter.py +145 -13
- notionary/prompting/markdown_syntax_prompt_generator.py +33 -29
- notionary-0.2.8.dist-info/METADATA +271 -0
- {notionary-0.2.6.dist-info → notionary-0.2.8.dist-info}/RECORD +15 -14
- {notionary-0.2.6.dist-info → notionary-0.2.8.dist-info}/WHEEL +1 -1
- notionary-0.2.6.dist-info/METADATA +0 -256
- {notionary-0.2.6.dist-info → notionary-0.2.8.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.2.6.dist-info → notionary-0.2.8.dist-info}/top_level.txt +0 -0
@@ -37,7 +37,6 @@ class PageContentWriter(LoggingMixin):
|
|
37
37
|
async def append_markdown(self, markdown_text: str, append_divider=False) -> bool:
|
38
38
|
"""
|
39
39
|
Append markdown text to a Notion page, automatically handling content length limits.
|
40
|
-
|
41
40
|
"""
|
42
41
|
if append_divider and not self.block_registry.contains(DividerElement):
|
43
42
|
self.logger.warning(
|
@@ -45,9 +44,11 @@ class PageContentWriter(LoggingMixin):
|
|
45
44
|
)
|
46
45
|
append_divider = False
|
47
46
|
|
48
|
-
# Append divider in
|
47
|
+
# Append divider in markdown format as it will be converted to a Notion divider block
|
49
48
|
if append_divider:
|
50
|
-
markdown_text = markdown_text + "\n
|
49
|
+
markdown_text = markdown_text + "\n---"
|
50
|
+
|
51
|
+
markdown_text = self._process_markdown_whitespace(markdown_text)
|
51
52
|
|
52
53
|
try:
|
53
54
|
blocks = self._markdown_to_notion_converter.convert(markdown_text)
|
@@ -101,3 +102,102 @@ class PageContentWriter(LoggingMixin):
|
|
101
102
|
except Exception as e:
|
102
103
|
self.logger.error("Failed to delete block: %s", str(e))
|
103
104
|
return False
|
105
|
+
|
106
|
+
def _process_markdown_whitespace(self, markdown_text: str) -> str:
|
107
|
+
"""
|
108
|
+
Process markdown text to preserve code structure while removing unnecessary indentation.
|
109
|
+
Strips all leading whitespace from regular lines, but preserves relative indentation
|
110
|
+
within code blocks.
|
111
|
+
|
112
|
+
Args:
|
113
|
+
markdown_text: Original markdown text with potential leading whitespace
|
114
|
+
|
115
|
+
Returns:
|
116
|
+
Processed markdown text with corrected whitespace
|
117
|
+
"""
|
118
|
+
lines = markdown_text.split("\n")
|
119
|
+
if not lines:
|
120
|
+
return ""
|
121
|
+
|
122
|
+
processed_lines = []
|
123
|
+
in_code_block = False
|
124
|
+
current_code_block = []
|
125
|
+
|
126
|
+
for line in lines:
|
127
|
+
# Handle code block markers
|
128
|
+
if self._is_code_block_marker(line):
|
129
|
+
if not in_code_block:
|
130
|
+
# Starting a new code block
|
131
|
+
in_code_block = True
|
132
|
+
processed_lines.append(self._process_code_block_start(line))
|
133
|
+
current_code_block = []
|
134
|
+
continue
|
135
|
+
|
136
|
+
# Ending a code block
|
137
|
+
processed_lines.extend(
|
138
|
+
self._process_code_block_content(current_code_block)
|
139
|
+
)
|
140
|
+
processed_lines.append("```")
|
141
|
+
in_code_block = False
|
142
|
+
continue
|
143
|
+
|
144
|
+
# Handle code block content
|
145
|
+
if in_code_block:
|
146
|
+
current_code_block.append(line)
|
147
|
+
continue
|
148
|
+
|
149
|
+
# Handle regular text
|
150
|
+
processed_lines.append(line.lstrip())
|
151
|
+
|
152
|
+
# Handle unclosed code block
|
153
|
+
if in_code_block and current_code_block:
|
154
|
+
processed_lines.extend(self._process_code_block_content(current_code_block))
|
155
|
+
processed_lines.append("```")
|
156
|
+
|
157
|
+
return "\n".join(processed_lines)
|
158
|
+
|
159
|
+
def _is_code_block_marker(self, line: str) -> bool:
|
160
|
+
"""Check if a line is a code block marker."""
|
161
|
+
return line.lstrip().startswith("```")
|
162
|
+
|
163
|
+
def _process_code_block_start(self, line: str) -> str:
|
164
|
+
"""Extract and normalize the code block opening marker."""
|
165
|
+
language = line.lstrip().replace("```", "", 1).strip()
|
166
|
+
return "```" + language
|
167
|
+
|
168
|
+
def _process_code_block_content(self, code_lines: list) -> list:
|
169
|
+
"""
|
170
|
+
Normalize code block indentation by removing the minimum common indentation.
|
171
|
+
|
172
|
+
Args:
|
173
|
+
code_lines: List of code block content lines
|
174
|
+
|
175
|
+
Returns:
|
176
|
+
List of processed code lines with normalized indentation
|
177
|
+
"""
|
178
|
+
if not code_lines:
|
179
|
+
return []
|
180
|
+
|
181
|
+
# Find non-empty lines to determine minimum indentation
|
182
|
+
non_empty_code_lines = [line for line in code_lines if line.strip()]
|
183
|
+
if not non_empty_code_lines:
|
184
|
+
return [""] * len(code_lines) # All empty lines stay empty
|
185
|
+
|
186
|
+
# Calculate minimum indentation
|
187
|
+
min_indent = min(
|
188
|
+
len(line) - len(line.lstrip()) for line in non_empty_code_lines
|
189
|
+
)
|
190
|
+
if min_indent == 0:
|
191
|
+
return code_lines # No common indentation to remove
|
192
|
+
|
193
|
+
# Process each line
|
194
|
+
processed_code_lines = []
|
195
|
+
for line in code_lines:
|
196
|
+
if not line.strip():
|
197
|
+
processed_code_lines.append("") # Keep empty lines empty
|
198
|
+
continue
|
199
|
+
|
200
|
+
# Remove exactly the minimum indentation
|
201
|
+
processed_code_lines.append(line[min_indent:])
|
202
|
+
|
203
|
+
return processed_code_lines
|
@@ -1,5 +1,7 @@
|
|
1
1
|
from typing import Dict, Any, List, Optional, Tuple
|
2
|
+
import re
|
2
3
|
|
4
|
+
from notionary.elements.column_element import ColumnElement
|
3
5
|
from notionary.elements.registry.block_registry import BlockRegistry
|
4
6
|
from notionary.elements.registry.block_registry_builder import (
|
5
7
|
BlockRegistryBuilder,
|
@@ -9,9 +11,11 @@ from notionary.elements.registry.block_registry_builder import (
|
|
9
11
|
class MarkdownToNotionConverter:
|
10
12
|
"""Converts Markdown text to Notion API block format with support for pipe syntax for nested structures."""
|
11
13
|
|
12
|
-
SPACER_MARKER = "
|
14
|
+
SPACER_MARKER = "---spacer---"
|
13
15
|
TOGGLE_ELEMENT_TYPES = ["ToggleElement", "ToggleableHeadingElement"]
|
14
16
|
PIPE_CONTENT_PATTERN = r"^\|\s?(.*)$"
|
17
|
+
HEADING_PATTERN = r"^(#{1,6})\s+(.+)$"
|
18
|
+
DIVIDER_PATTERN = r"^-{3,}$"
|
15
19
|
|
16
20
|
def __init__(self, block_registry: Optional[BlockRegistry] = None):
|
17
21
|
"""Initialize the converter with an optional custom block registry."""
|
@@ -19,14 +23,21 @@ class MarkdownToNotionConverter:
|
|
19
23
|
block_registry or BlockRegistryBuilder().create_full_registry()
|
20
24
|
)
|
21
25
|
|
26
|
+
if self._block_registry.contains(ColumnElement):
|
27
|
+
ColumnElement.set_converter_callback(self.convert)
|
28
|
+
|
22
29
|
def convert(self, markdown_text: str) -> List[Dict[str, Any]]:
|
23
30
|
"""Convert markdown text to Notion API block format."""
|
24
31
|
if not markdown_text:
|
25
32
|
return []
|
26
33
|
|
34
|
+
# Preprocess markdown to add spacers before headings and dividers
|
35
|
+
processed_markdown = self._add_spacers_before_elements(markdown_text)
|
36
|
+
print("Processed Markdown:", processed_markdown)
|
37
|
+
|
27
38
|
# Collect all blocks with their positions in the text
|
28
39
|
all_blocks_with_positions = self._collect_all_blocks_with_positions(
|
29
|
-
|
40
|
+
processed_markdown
|
30
41
|
)
|
31
42
|
|
32
43
|
# Sort all blocks by their position in the text
|
@@ -38,6 +49,135 @@ class MarkdownToNotionConverter:
|
|
38
49
|
# Process spacing between blocks
|
39
50
|
return self._process_block_spacing(blocks)
|
40
51
|
|
52
|
+
def _add_spacers_before_elements(self, markdown_text: str) -> str:
|
53
|
+
"""Add spacer markers before every heading (except the first one) and before every divider,
|
54
|
+
but ignore content inside code blocks and consecutive headings."""
|
55
|
+
lines = markdown_text.split("\n")
|
56
|
+
processed_lines = []
|
57
|
+
found_first_heading = False
|
58
|
+
in_code_block = False
|
59
|
+
last_line_was_spacer = False
|
60
|
+
last_non_empty_was_heading = False
|
61
|
+
|
62
|
+
i = 0
|
63
|
+
while i < len(lines):
|
64
|
+
line = lines[i]
|
65
|
+
|
66
|
+
# Check for code block boundaries and handle accordingly
|
67
|
+
if self._is_code_block_marker(line):
|
68
|
+
in_code_block = not in_code_block
|
69
|
+
processed_lines.append(line)
|
70
|
+
if line.strip(): # If not empty
|
71
|
+
last_non_empty_was_heading = False
|
72
|
+
last_line_was_spacer = False
|
73
|
+
i += 1
|
74
|
+
continue
|
75
|
+
|
76
|
+
# Skip processing markdown inside code blocks
|
77
|
+
if in_code_block:
|
78
|
+
processed_lines.append(line)
|
79
|
+
if line.strip(): # If not empty
|
80
|
+
last_non_empty_was_heading = False
|
81
|
+
last_line_was_spacer = False
|
82
|
+
i += 1
|
83
|
+
continue
|
84
|
+
|
85
|
+
# Process line with context about consecutive headings
|
86
|
+
result = self._process_line_for_spacers(
|
87
|
+
line,
|
88
|
+
processed_lines,
|
89
|
+
found_first_heading,
|
90
|
+
last_line_was_spacer,
|
91
|
+
last_non_empty_was_heading,
|
92
|
+
)
|
93
|
+
|
94
|
+
last_line_was_spacer = result["added_spacer"]
|
95
|
+
|
96
|
+
# Update tracking of consecutive headings and first heading
|
97
|
+
if line.strip(): # Not empty line
|
98
|
+
is_heading = re.match(self.HEADING_PATTERN, line) is not None
|
99
|
+
if is_heading:
|
100
|
+
if not found_first_heading:
|
101
|
+
found_first_heading = True
|
102
|
+
last_non_empty_was_heading = True
|
103
|
+
elif line.strip() != self.SPACER_MARKER: # Not a spacer or heading
|
104
|
+
last_non_empty_was_heading = False
|
105
|
+
|
106
|
+
i += 1
|
107
|
+
|
108
|
+
return "\n".join(processed_lines)
|
109
|
+
|
110
|
+
def _is_code_block_marker(self, line: str) -> bool:
|
111
|
+
"""Check if a line is a code block marker (start or end)."""
|
112
|
+
return line.strip().startswith("```")
|
113
|
+
|
114
|
+
def _process_line_for_spacers(
|
115
|
+
self,
|
116
|
+
line: str,
|
117
|
+
processed_lines: List[str],
|
118
|
+
found_first_heading: bool,
|
119
|
+
last_line_was_spacer: bool,
|
120
|
+
last_non_empty_was_heading: bool,
|
121
|
+
) -> Dict[str, bool]:
|
122
|
+
"""
|
123
|
+
Process a single line to add spacers before headings and dividers if needed.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
line: The line to process
|
127
|
+
processed_lines: List of already processed lines to append to
|
128
|
+
found_first_heading: Whether the first heading has been found
|
129
|
+
last_line_was_spacer: Whether the last added line was a spacer
|
130
|
+
last_non_empty_was_heading: Whether the last non-empty line was a heading
|
131
|
+
|
132
|
+
Returns:
|
133
|
+
Dictionary with processing results
|
134
|
+
"""
|
135
|
+
added_spacer = False
|
136
|
+
line_stripped = line.strip()
|
137
|
+
is_empty = not line_stripped
|
138
|
+
|
139
|
+
# Skip empty lines
|
140
|
+
if is_empty:
|
141
|
+
processed_lines.append(line)
|
142
|
+
return {"added_spacer": False}
|
143
|
+
|
144
|
+
# Check if line is a heading
|
145
|
+
if re.match(self.HEADING_PATTERN, line):
|
146
|
+
if (
|
147
|
+
found_first_heading
|
148
|
+
and not last_line_was_spacer
|
149
|
+
and not last_non_empty_was_heading
|
150
|
+
):
|
151
|
+
# Add spacer only if:
|
152
|
+
# 1. Not the first heading
|
153
|
+
# 2. Last non-empty line was not a heading
|
154
|
+
# 3. Last line was not already a spacer
|
155
|
+
processed_lines.append(self.SPACER_MARKER)
|
156
|
+
added_spacer = True
|
157
|
+
|
158
|
+
processed_lines.append(line)
|
159
|
+
|
160
|
+
# Check if line is a divider
|
161
|
+
elif re.match(self.DIVIDER_PATTERN, line):
|
162
|
+
if not last_line_was_spacer:
|
163
|
+
# Only add a single spacer line before dividers (no extra line breaks)
|
164
|
+
processed_lines.append(self.SPACER_MARKER)
|
165
|
+
added_spacer = True
|
166
|
+
|
167
|
+
processed_lines.append(line)
|
168
|
+
|
169
|
+
# Check if this line itself is a spacer
|
170
|
+
elif line_stripped == self.SPACER_MARKER:
|
171
|
+
# Never add consecutive spacers
|
172
|
+
if not last_line_was_spacer:
|
173
|
+
processed_lines.append(line)
|
174
|
+
added_spacer = True
|
175
|
+
|
176
|
+
else:
|
177
|
+
processed_lines.append(line)
|
178
|
+
|
179
|
+
return {"added_spacer": added_spacer}
|
180
|
+
|
41
181
|
def _collect_all_blocks_with_positions(
|
42
182
|
self, markdown_text: str
|
43
183
|
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
@@ -75,13 +215,10 @@ class MarkdownToNotionConverter:
|
|
75
215
|
if not toggleable_elements:
|
76
216
|
return []
|
77
217
|
|
78
|
-
# Process each toggleable element type
|
79
218
|
for element in toggleable_elements:
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
if matches:
|
84
|
-
toggleable_blocks.extend(matches)
|
219
|
+
matches = element.find_matches(text, self.convert, context_aware=True)
|
220
|
+
if matches:
|
221
|
+
toggleable_blocks.extend(matches)
|
85
222
|
|
86
223
|
return toggleable_blocks
|
87
224
|
|
@@ -112,9 +249,6 @@ class MarkdownToNotionConverter:
|
|
112
249
|
|
113
250
|
multiline_blocks = []
|
114
251
|
for element in multiline_elements:
|
115
|
-
if not hasattr(element, "find_matches"):
|
116
|
-
continue
|
117
|
-
|
118
252
|
matches = element.find_matches(text)
|
119
253
|
|
120
254
|
if not matches:
|
@@ -202,8 +336,6 @@ class MarkdownToNotionConverter:
|
|
202
336
|
|
203
337
|
def _is_pipe_syntax_line(self, line: str) -> bool:
|
204
338
|
"""Check if a line uses pipe syntax (for nested content)."""
|
205
|
-
import re
|
206
|
-
|
207
339
|
return bool(re.match(self.PIPE_CONTENT_PATTERN, line))
|
208
340
|
|
209
341
|
def _process_line(
|
@@ -1,6 +1,6 @@
|
|
1
|
-
from textwrap import dedent
|
2
1
|
from typing import Type, List
|
3
2
|
from notionary.elements.notion_block_element import NotionBlockElement
|
3
|
+
from notionary.elements.text_inline_formatter import TextInlineFormatter
|
4
4
|
|
5
5
|
|
6
6
|
class MarkdownSyntaxPromptGenerator:
|
@@ -11,10 +11,8 @@ class MarkdownSyntaxPromptGenerator:
|
|
11
11
|
and formats them optimally for LLMs.
|
12
12
|
"""
|
13
13
|
|
14
|
-
SYSTEM_PROMPT_TEMPLATE =
|
15
|
-
|
16
|
-
You are a knowledgeable assistant that helps users create content for Notion pages.
|
17
|
-
Notion supports standard Markdown with some special extensions for creating rich content.
|
14
|
+
SYSTEM_PROMPT_TEMPLATE = """
|
15
|
+
You create content for Notion pages using Markdown syntax with special Notion extensions.
|
18
16
|
|
19
17
|
# Understanding Notion Blocks
|
20
18
|
|
@@ -27,30 +25,36 @@ class MarkdownSyntaxPromptGenerator:
|
|
27
25
|
|
28
26
|
1. Do NOT start content with a level 1 heading (# Heading). In Notion, the page title is already displayed in the metadata, so starting with an H1 heading is redundant. Begin with H2 (## Heading) or lower for section headings.
|
29
27
|
|
30
|
-
2.
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
28
|
+
2. BACKTICK HANDLING - EXTREMELY IMPORTANT:
|
29
|
+
- NEVER wrap entire content or responses in triple backticks (```).
|
30
|
+
- DO NOT use triple backticks (```) for anything except CODE BLOCKS or DIAGRAMS.
|
31
|
+
- DO NOT use triple backticks to mark or highlight regular text or examples.
|
32
|
+
- USE triple backticks ONLY for actual programming code, pseudocode, or specialized notation.
|
33
|
+
- For inline code, use single backticks (`code`).
|
34
|
+
- When showing Markdown syntax examples, use inline code formatting with single backticks.
|
35
|
+
|
36
|
+
3. CONTENT FORMATTING - CRITICAL:
|
37
|
+
- DO NOT include introductory phrases like "I understand that..." or "Here's the content...".
|
38
|
+
- Provide ONLY the requested content directly without any prefacing text or meta-commentary.
|
39
|
+
- Generate just the content itself, formatted according to these guidelines.
|
40
|
+
- USE INLINE FORMATTING to enhance readability:
|
41
|
+
- Use *italic* for emphasis, terminology, and definitions
|
42
|
+
- Use `code` for technical terms, file paths, variables, and commands
|
43
|
+
- Use **bold** sparingly for truly important information
|
44
|
+
- Use appropriate inline formatting naturally throughout the content, but don't overuse it
|
45
|
+
|
46
|
+
4. USER INSTRUCTIONS - VERY IMPORTANT:
|
47
|
+
- Follow the user's formatting instructions EXACTLY and in the specified order
|
48
|
+
- When the user requests specific elements (e.g., "first a callout, then 4 bullet points"), create them in that precise sequence
|
49
|
+
- Adhere strictly to any structural requirements provided by the user
|
50
|
+
- Do not deviate from or reinterpret the user's formatting requests
|
51
|
+
|
52
|
+
5. ADD EMOJIS TO HEADINGS - REQUIRED UNLESS EXPLICITLY TOLD NOT TO:
|
53
|
+
- ALWAYS add appropriate emojis at the beginning of headings to improve structure and readability
|
54
|
+
- Choose emojis that represent the content or theme of each section
|
55
|
+
- Format as: ## 🚀 Heading Text (with space after emoji)
|
56
|
+
- Only omit emojis if the user explicitly instructs you not to use them
|
57
|
+
"""
|
54
58
|
|
55
59
|
@staticmethod
|
56
60
|
def generate_element_doc(element_class: Type[NotionBlockElement]) -> str:
|
@@ -0,0 +1,271 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: notionary
|
3
|
+
Version: 0.2.8
|
4
|
+
Summary: A toolkit to convert between Markdown and Notion blocks
|
5
|
+
Home-page: https://github.com/mathisarends/notionary
|
6
|
+
Author: Mathis Arends
|
7
|
+
Author-email: mathisarends27@gmail.com
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
10
|
+
Requires-Python: >=3.7
|
11
|
+
Description-Content-Type: text/markdown
|
12
|
+
License-File: LICENSE
|
13
|
+
Requires-Dist: httpx>=0.28.0
|
14
|
+
Requires-Dist: python-dotenv>=1.1.0
|
15
|
+
Requires-Dist: pydantic>=2.11.4
|
16
|
+
Dynamic: author
|
17
|
+
Dynamic: author-email
|
18
|
+
Dynamic: classifier
|
19
|
+
Dynamic: description
|
20
|
+
Dynamic: description-content-type
|
21
|
+
Dynamic: home-page
|
22
|
+
Dynamic: license-file
|
23
|
+
Dynamic: requires-dist
|
24
|
+
Dynamic: requires-python
|
25
|
+
Dynamic: summary
|
26
|
+
|
27
|
+
# Notionary 📝
|
28
|
+
|
29
|
+
[](https://www.python.org/downloads/)
|
30
|
+
[](LICENSE)
|
31
|
+
|
32
|
+
**Notionary** is a powerful Python library for interacting with the Notion API, making it easy to create, update, and manage Notion pages and databases programmatically with a clean, intuitive interface. It's specifically designed to be the foundation for AI-driven Notion content generation.
|
33
|
+
|
34
|
+
---
|
35
|
+
|
36
|
+
## Features
|
37
|
+
|
38
|
+
- **Rich Markdown Support**: Create Notion pages using intuitive Markdown syntax with custom extensions
|
39
|
+
- **Dynamic Database Operations**: Create, update, and query database entries with schema auto-detection
|
40
|
+
- **Extensible Block Registry**: Add, customize, or remove Notion block elements with a flexible registry pattern
|
41
|
+
- **LLM-Ready Prompts**: Generate system prompts explaining Markdown syntax for LLMs to create Notion content
|
42
|
+
- **Async-First Design**: Built for modern Python with full async/await support
|
43
|
+
- **Schema-Based Validation**: Automatic property validation based on database schemas
|
44
|
+
- **Intelligent Content Conversion**: Bidirectional conversion between Markdown and Notion blocks
|
45
|
+
|
46
|
+
---
|
47
|
+
|
48
|
+
## Installation
|
49
|
+
|
50
|
+
```bash
|
51
|
+
pip install notionary
|
52
|
+
```
|
53
|
+
|
54
|
+
---
|
55
|
+
|
56
|
+
## Quick Start
|
57
|
+
|
58
|
+
### Creating and Managing Pages
|
59
|
+
|
60
|
+
```python
|
61
|
+
import asyncio
|
62
|
+
from notionary import NotionPage
|
63
|
+
|
64
|
+
async def main():
|
65
|
+
# Create a page from URL
|
66
|
+
page = NotionPage.from_url("https://www.notion.so/your-page-url")
|
67
|
+
|
68
|
+
# Or find by name
|
69
|
+
page = await NotionPage.from_page_name("My Project Page")
|
70
|
+
|
71
|
+
# Update page metadata
|
72
|
+
await page.set_title("Updated Title")
|
73
|
+
await page.set_emoji_icon("🚀")
|
74
|
+
await page.set_random_gradient_cover()
|
75
|
+
|
76
|
+
# Add markdown content
|
77
|
+
markdown = """
|
78
|
+
# Project Overview
|
79
|
+
|
80
|
+
!> [💡] This page was created programmatically using Notionary.
|
81
|
+
|
82
|
+
## Features
|
83
|
+
- **Rich** Markdown support
|
84
|
+
- Async functionality
|
85
|
+
- Custom syntax extensions
|
86
|
+
|
87
|
+
+++ Implementation Details
|
88
|
+
| Notionary uses a custom converter to transform Markdown into Notion blocks.
|
89
|
+
| This makes it easy to create rich content programmatically.
|
90
|
+
"""
|
91
|
+
|
92
|
+
await page.replace_content(markdown)
|
93
|
+
|
94
|
+
if __name__ == "__main__":
|
95
|
+
asyncio.run(main())
|
96
|
+
```
|
97
|
+
|
98
|
+
### Working with Databases
|
99
|
+
|
100
|
+
```python
|
101
|
+
import asyncio
|
102
|
+
from notionary import NotionDatabase, DatabaseDiscovery
|
103
|
+
|
104
|
+
async def main():
|
105
|
+
# Discover available databases
|
106
|
+
discovery = DatabaseDiscovery()
|
107
|
+
await discovery()
|
108
|
+
|
109
|
+
# Connect to a database by name
|
110
|
+
db = await NotionDatabase.from_database_name("Projects")
|
111
|
+
|
112
|
+
# Create a new page in the database
|
113
|
+
page = await db.create_blank_page()
|
114
|
+
|
115
|
+
# Set properties
|
116
|
+
await page.set_property_value_by_name("Status", "In Progress")
|
117
|
+
await page.set_property_value_by_name("Priority", "High")
|
118
|
+
|
119
|
+
# Query pages from database
|
120
|
+
async for page in db.iter_pages():
|
121
|
+
title = await page.get_title()
|
122
|
+
print(f"Page: {title}")
|
123
|
+
|
124
|
+
if __name__ == "__main__":
|
125
|
+
asyncio.run(main())
|
126
|
+
```
|
127
|
+
|
128
|
+
## Custom Markdown Syntax
|
129
|
+
|
130
|
+
Notionary extends standard Markdown with special syntax to support Notion-specific features:
|
131
|
+
|
132
|
+
### Text Formatting
|
133
|
+
|
134
|
+
- Standard: `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``
|
135
|
+
- Links: `[text](url)`
|
136
|
+
- Quotes: `> This is a quote`
|
137
|
+
- Divider: `---`
|
138
|
+
|
139
|
+
### Callouts
|
140
|
+
|
141
|
+
```markdown
|
142
|
+
!> [💡] This is a default callout with the light bulb emoji
|
143
|
+
!> [🔔] This is a notification with a bell emoji
|
144
|
+
!> [⚠️] Warning: This is an important note
|
145
|
+
```
|
146
|
+
|
147
|
+
### Toggles
|
148
|
+
|
149
|
+
```markdown
|
150
|
+
+++ How to use Notionary
|
151
|
+
| 1. Initialize with NotionPage
|
152
|
+
| 2. Update metadata with set_title(), set_emoji_icon(), etc.
|
153
|
+
| 3. Add content with replace_content() or append_markdown()
|
154
|
+
```
|
155
|
+
|
156
|
+
### Multi-Column Layout
|
157
|
+
|
158
|
+
```markdown
|
159
|
+
::: columns
|
160
|
+
::: column
|
161
|
+
|
162
|
+
## Left Column
|
163
|
+
|
164
|
+
- Item 1
|
165
|
+
- Item 2
|
166
|
+
- Item 3
|
167
|
+
:::
|
168
|
+
::: column
|
169
|
+
|
170
|
+
## Right Column
|
171
|
+
|
172
|
+
This text appears in the second column. Multi-column layouts are perfect for:
|
173
|
+
|
174
|
+
- Comparing features
|
175
|
+
- Creating side-by-side content
|
176
|
+
- Improving readability of wide content
|
177
|
+
:::
|
178
|
+
:::
|
179
|
+
```
|
180
|
+
|
181
|
+
### Code Blocks
|
182
|
+
|
183
|
+
```python
|
184
|
+
def hello_world():
|
185
|
+
print("Hello from Notionary!")
|
186
|
+
```
|
187
|
+
|
188
|
+
### To-do Lists
|
189
|
+
|
190
|
+
```markdown
|
191
|
+
- [ ] Define project scope
|
192
|
+
- [x] Create timeline
|
193
|
+
- [ ] Assign resources
|
194
|
+
```
|
195
|
+
|
196
|
+
### Tables
|
197
|
+
|
198
|
+
```markdown
|
199
|
+
| Feature | Status | Priority |
|
200
|
+
| --------------- | ----------- | -------- |
|
201
|
+
| API Integration | Complete | High |
|
202
|
+
| Documentation | In Progress | Medium |
|
203
|
+
```
|
204
|
+
|
205
|
+
### More Elements
|
206
|
+
|
207
|
+
```markdown
|
208
|
+

|
209
|
+
@[Caption](https://youtube.com/watch?v=...)
|
210
|
+
[bookmark](https://example.com "Title" "Description")
|
211
|
+
```
|
212
|
+
|
213
|
+
## Block Registry & Customization
|
214
|
+
|
215
|
+
```python
|
216
|
+
from notionary import NotionPage, BlockRegistryBuilder
|
217
|
+
|
218
|
+
# Create a custom registry with only the elements you need
|
219
|
+
custom_registry = (
|
220
|
+
BlockRegistryBuilder()
|
221
|
+
.with_headings()
|
222
|
+
.with_callouts()
|
223
|
+
.with_toggles()
|
224
|
+
.with_columns() # Include multi-column support
|
225
|
+
.with_code()
|
226
|
+
.with_todos()
|
227
|
+
.with_paragraphs()
|
228
|
+
.build()
|
229
|
+
)
|
230
|
+
|
231
|
+
# Apply this registry to a page
|
232
|
+
page = NotionPage.from_url("https://www.notion.so/your-page-url")
|
233
|
+
page.block_registry = custom_registry
|
234
|
+
|
235
|
+
# Replace content using only supported elements
|
236
|
+
await page.replace_content("# Custom heading with selected elements only")
|
237
|
+
```
|
238
|
+
|
239
|
+
## AI-Ready: Generate LLM Prompts
|
240
|
+
|
241
|
+
```python
|
242
|
+
from notionary import BlockRegistryBuilder
|
243
|
+
|
244
|
+
# Create a registry with all standard elements
|
245
|
+
registry = BlockRegistryBuilder.create_full_registry()
|
246
|
+
|
247
|
+
# Generate the LLM system prompt
|
248
|
+
llm_system_prompt = registry.get_notion_markdown_syntax_prompt()
|
249
|
+
print(llm_system_prompt)
|
250
|
+
```
|
251
|
+
|
252
|
+
## Examples
|
253
|
+
|
254
|
+
See the `examples/` folder for:
|
255
|
+
|
256
|
+
- [Database discovery and querying](examples/database_discovery_example.py)
|
257
|
+
- [Rich page creation with Markdown](examples/page_example.py)
|
258
|
+
- [Database management](examples/database_management_example.py)
|
259
|
+
- [Iterating through database entries](examples/database_iteration_example.py)
|
260
|
+
- [Temporary usage & debugging](examples/temp.py)
|
261
|
+
|
262
|
+
## Perfect for AI Agents and Automation
|
263
|
+
|
264
|
+
- **LLM Integration**: Generate Notion-compatible content with any LLM using the system prompt generator
|
265
|
+
- **Dynamic Content Generation**: AI agents can generate content in Markdown and render it directly as Notion pages
|
266
|
+
- **Schema-Aware Operations**: Automatically validate and format properties based on database schemas
|
267
|
+
- **Simplified API**: Clean, intuitive interface for both human developers and AI systems
|
268
|
+
|
269
|
+
## Contributing
|
270
|
+
|
271
|
+
Contributions welcome — feel free to submit a pull request!
|