notionary 0.2.6__py3-none-any.whl → 0.2.7__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 +204 -0
- notionary/elements/divider_element.py +2 -2
- notionary/elements/registry/block_registry.py +1 -1
- notionary/elements/registry/block_registry_builder.py +7 -0
- notionary/page/content/page_content_writer.py +12 -2
- notionary/page/markdown_to_notion_converter.py +45 -14
- notionary/prompting/markdown_syntax_prompt_generator.py +35 -29
- notionary-0.2.7.dist-info/METADATA +245 -0
- {notionary-0.2.6.dist-info → notionary-0.2.7.dist-info}/RECORD +14 -13
- notionary-0.2.6.dist-info/METADATA +0 -256
- {notionary-0.2.6.dist-info → notionary-0.2.7.dist-info}/WHEEL +0 -0
- {notionary-0.2.6.dist-info → notionary-0.2.7.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.2.6.dist-info → notionary-0.2.7.dist-info}/top_level.txt +0 -0
@@ -26,29 +26,7 @@ class DatabaseDiscovery(LoggingMixin):
|
|
26
26
|
self._client = client if client else NotionClient()
|
27
27
|
self.logger.info("DatabaseDiscovery initialized")
|
28
28
|
|
29
|
-
async def
|
30
|
-
"""
|
31
|
-
Discover all accessible databases and return their titles and IDs.
|
32
|
-
|
33
|
-
Args:
|
34
|
-
page_size: The number of databases to fetch per request
|
35
|
-
|
36
|
-
Returns:
|
37
|
-
List of tuples containing (database_title, database_id)
|
38
|
-
"""
|
39
|
-
databases = []
|
40
|
-
|
41
|
-
async for database in self._iter_databases(page_size):
|
42
|
-
db_id = database.get("id")
|
43
|
-
if not db_id:
|
44
|
-
continue
|
45
|
-
|
46
|
-
title = self._extract_database_title(database)
|
47
|
-
databases.append((title, db_id))
|
48
|
-
|
49
|
-
return databases
|
50
|
-
|
51
|
-
async def discover_and_print(self, page_size: int = 100) -> List[Tuple[str, str]]:
|
29
|
+
async def __call__(self, page_size: int = 100) -> List[Tuple[str, str]]:
|
52
30
|
"""
|
53
31
|
Discover databases and print the results in a nicely formatted way.
|
54
32
|
|
@@ -61,7 +39,7 @@ class DatabaseDiscovery(LoggingMixin):
|
|
61
39
|
Returns:
|
62
40
|
The same list of databases as discover() for further processing
|
63
41
|
"""
|
64
|
-
databases = await self.
|
42
|
+
databases = await self._discover(page_size)
|
65
43
|
|
66
44
|
if not databases:
|
67
45
|
print("\n⚠️ No databases found!")
|
@@ -78,6 +56,28 @@ class DatabaseDiscovery(LoggingMixin):
|
|
78
56
|
|
79
57
|
return databases
|
80
58
|
|
59
|
+
async def _discover(self, page_size: int = 100) -> List[Tuple[str, str]]:
|
60
|
+
"""
|
61
|
+
Discover all accessible databases and return their titles and IDs.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
page_size: The number of databases to fetch per request
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
List of tuples containing (database_title, database_id)
|
68
|
+
"""
|
69
|
+
databases = []
|
70
|
+
|
71
|
+
async for database in self._iter_databases(page_size):
|
72
|
+
db_id = database.get("id")
|
73
|
+
if not db_id:
|
74
|
+
continue
|
75
|
+
|
76
|
+
title = self._extract_database_title(database)
|
77
|
+
databases.append((title, db_id))
|
78
|
+
|
79
|
+
return databases
|
80
|
+
|
81
81
|
async def _iter_databases(
|
82
82
|
self, page_size: int = 100
|
83
83
|
) -> AsyncGenerator[Dict[str, Any], None]:
|
@@ -15,13 +15,17 @@ class CodeBlockElement(NotionBlockElement):
|
|
15
15
|
```language
|
16
16
|
code content
|
17
17
|
```
|
18
|
+
Caption: optional caption text
|
18
19
|
|
19
20
|
Where:
|
20
21
|
- language is optional and specifies the programming language
|
21
22
|
- code content is the code to be displayed
|
23
|
+
- Caption line is optional and must appear immediately after the closing ```
|
22
24
|
"""
|
23
25
|
|
24
|
-
PATTERN = re.compile(
|
26
|
+
PATTERN = re.compile(
|
27
|
+
r"```(\w*)\n([\s\S]+?)```(?:\n(?:Caption|caption):\s*(.+))?", re.MULTILINE
|
28
|
+
)
|
25
29
|
|
26
30
|
@classmethod
|
27
31
|
def match_markdown(cls, text: str) -> bool:
|
@@ -42,25 +46,18 @@ class CodeBlockElement(NotionBlockElement):
|
|
42
46
|
|
43
47
|
language = match.group(1) or "plain text"
|
44
48
|
content = match.group(2)
|
49
|
+
caption = match.group(3)
|
45
50
|
|
46
51
|
if content.endswith("\n"):
|
47
52
|
content = content[:-1]
|
48
53
|
|
49
|
-
|
54
|
+
block = {
|
50
55
|
"type": "code",
|
51
56
|
"code": {
|
52
57
|
"rich_text": [
|
53
58
|
{
|
54
59
|
"type": "text",
|
55
60
|
"text": {"content": content},
|
56
|
-
"annotations": {
|
57
|
-
"bold": False,
|
58
|
-
"italic": False,
|
59
|
-
"strikethrough": False,
|
60
|
-
"underline": False,
|
61
|
-
"code": False,
|
62
|
-
"color": "default",
|
63
|
-
},
|
64
61
|
"plain_text": content,
|
65
62
|
}
|
66
63
|
],
|
@@ -68,6 +65,18 @@ class CodeBlockElement(NotionBlockElement):
|
|
68
65
|
},
|
69
66
|
}
|
70
67
|
|
68
|
+
# Add caption if provided
|
69
|
+
if caption and caption.strip():
|
70
|
+
block["code"]["caption"] = [
|
71
|
+
{
|
72
|
+
"type": "text",
|
73
|
+
"text": {"content": caption.strip()},
|
74
|
+
"plain_text": caption.strip(),
|
75
|
+
}
|
76
|
+
]
|
77
|
+
|
78
|
+
return block
|
79
|
+
|
71
80
|
@classmethod
|
72
81
|
def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
|
73
82
|
"""Convert Notion code block to markdown code block."""
|
@@ -84,8 +93,20 @@ class CodeBlockElement(NotionBlockElement):
|
|
84
93
|
|
85
94
|
language = code_data.get("language", "")
|
86
95
|
|
96
|
+
# Extract caption if present
|
97
|
+
caption_text = ""
|
98
|
+
caption_data = code_data.get("caption", [])
|
99
|
+
for caption_block in caption_data:
|
100
|
+
caption_text += caption_block.get("plain_text", "")
|
101
|
+
|
87
102
|
# Format as a markdown code block
|
88
|
-
|
103
|
+
result = f"```{language}\n{content}\n```"
|
104
|
+
|
105
|
+
# Add caption if present
|
106
|
+
if caption_text.strip():
|
107
|
+
result += f"\nCaption: {caption_text}"
|
108
|
+
|
109
|
+
return result
|
89
110
|
|
90
111
|
@classmethod
|
91
112
|
def find_matches(cls, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
|
@@ -102,6 +123,7 @@ class CodeBlockElement(NotionBlockElement):
|
|
102
123
|
for match in CodeBlockElement.PATTERN.finditer(text):
|
103
124
|
language = match.group(1) or "plain text"
|
104
125
|
content = match.group(2)
|
126
|
+
caption = match.group(3)
|
105
127
|
|
106
128
|
# Remove trailing newline if present
|
107
129
|
if content.endswith("\n"):
|
@@ -121,6 +143,16 @@ class CodeBlockElement(NotionBlockElement):
|
|
121
143
|
},
|
122
144
|
}
|
123
145
|
|
146
|
+
# Add caption if provided
|
147
|
+
if caption and caption.strip():
|
148
|
+
block["code"]["caption"] = [
|
149
|
+
{
|
150
|
+
"type": "text",
|
151
|
+
"text": {"content": caption.strip()},
|
152
|
+
"plain_text": caption.strip(),
|
153
|
+
}
|
154
|
+
]
|
155
|
+
|
124
156
|
matches.append((match.start(), match.end(), block))
|
125
157
|
|
126
158
|
return matches
|
@@ -139,25 +171,34 @@ class CodeBlockElement(NotionBlockElement):
|
|
139
171
|
.with_description(
|
140
172
|
"Use fenced code blocks to format content as code. Supports language annotations like "
|
141
173
|
"'python', 'json', or 'mermaid'. Useful for displaying code, configurations, command-line "
|
142
|
-
"examples, or diagram syntax. Also suitable for explaining or visualizing systems with diagram languages."
|
174
|
+
"examples, or diagram syntax. Also suitable for explaining or visualizing systems with diagram languages. "
|
175
|
+
"Code blocks can include optional captions for better documentation."
|
143
176
|
)
|
144
177
|
.with_usage_guidelines(
|
145
178
|
"Use code blocks when you want to present technical content like code snippets, terminal commands, "
|
146
|
-
"JSON structures, or system diagrams. Especially helpful when structure and formatting are essential."
|
179
|
+
"JSON structures, or system diagrams. Especially helpful when structure and formatting are essential. "
|
180
|
+
"Add captions to provide context, explanations, or titles for your code blocks."
|
181
|
+
)
|
182
|
+
.with_syntax(
|
183
|
+
"```language\ncode content\n```\nCaption: optional caption text\n\n"
|
184
|
+
"OR\n\n"
|
185
|
+
"```language\ncode content\n```"
|
147
186
|
)
|
148
|
-
.with_syntax("```language\ncode content\n```")
|
149
187
|
.with_examples(
|
150
188
|
[
|
151
|
-
"```python\nprint('Hello, world!')\n
|
152
|
-
'```json\n{"name": "Alice", "age": 30}\n
|
153
|
-
"```mermaid\nflowchart TD\n A --> B\n
|
189
|
+
"```python\nprint('Hello, world!')\n```\nCaption: Basic Python greeting example",
|
190
|
+
'```json\n{"name": "Alice", "age": 30}\n```\nCaption: User data structure',
|
191
|
+
"```mermaid\nflowchart TD\n A --> B\n```\nCaption: Simple flow diagram",
|
192
|
+
'```bash\ngit commit -m "Initial commit"\n```', # Without caption
|
154
193
|
]
|
155
194
|
)
|
156
195
|
.with_avoidance_guidelines(
|
157
196
|
"NEVER EVER wrap markdown content with ```markdown. Markdown should be written directly without code block formatting. "
|
158
197
|
"NEVER use ```markdown under any circumstances. "
|
159
198
|
"For Mermaid diagrams, use ONLY the default styling without colors, backgrounds, or custom styling attributes. "
|
160
|
-
"Keep Mermaid diagrams simple and minimal without any styling or color modifications."
|
199
|
+
"Keep Mermaid diagrams simple and minimal without any styling or color modifications. "
|
200
|
+
"Captions must appear immediately after the closing ``` on a new line starting with 'Caption:' - "
|
201
|
+
"no empty lines between the code block and the caption."
|
161
202
|
)
|
162
203
|
.build()
|
163
204
|
)
|
@@ -0,0 +1,204 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, List, Tuple
|
3
|
+
from notionary.elements.notion_block_element import NotionBlockElement
|
4
|
+
from notionary.prompting.element_prompt_content import (
|
5
|
+
ElementPromptBuilder,
|
6
|
+
ElementPromptContent,
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
# Fix Column Element
|
11
|
+
class ColumnsElement(NotionBlockElement):
|
12
|
+
"""
|
13
|
+
Handles conversion between Markdown column syntax and Notion column_list blocks.
|
14
|
+
|
15
|
+
Note: Due to Notion's column structure, this element requires special handling.
|
16
|
+
It returns a column_list block with placeholder content, as the actual columns
|
17
|
+
must be added as children after the column_list is created.
|
18
|
+
"""
|
19
|
+
|
20
|
+
PATTERN = re.compile(
|
21
|
+
r"^::: columns\n((?:::: column\n(?:.*?\n)*?:::\n?)+):::\s*$",
|
22
|
+
re.MULTILINE | re.DOTALL,
|
23
|
+
)
|
24
|
+
|
25
|
+
COLUMN_PATTERN = re.compile(r"::: column\n(.*?):::", re.DOTALL)
|
26
|
+
|
27
|
+
@classmethod
|
28
|
+
def match_markdown(cls, text: str) -> bool:
|
29
|
+
"""Check if text contains a columns block."""
|
30
|
+
return bool(cls.PATTERN.search(text))
|
31
|
+
|
32
|
+
@classmethod
|
33
|
+
def match_notion(cls, block: Dict[str, Any]) -> bool:
|
34
|
+
"""Check if block is a Notion column_list block."""
|
35
|
+
return block.get("type") == "column_list"
|
36
|
+
|
37
|
+
@classmethod
|
38
|
+
def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
|
39
|
+
"""Convert markdown columns to Notion column_list block."""
|
40
|
+
match = cls.PATTERN.search(text)
|
41
|
+
if not match:
|
42
|
+
return None
|
43
|
+
|
44
|
+
columns_content = match.group(1)
|
45
|
+
column_matches = cls.COLUMN_PATTERN.findall(columns_content)
|
46
|
+
|
47
|
+
if not column_matches:
|
48
|
+
return None
|
49
|
+
|
50
|
+
return {"type": "column_list", "column_list": {}}
|
51
|
+
|
52
|
+
@classmethod
|
53
|
+
def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
|
54
|
+
"""Convert Notion column_list block to markdown columns."""
|
55
|
+
if block.get("type") != "column_list":
|
56
|
+
return None
|
57
|
+
|
58
|
+
# In a real implementation, you'd need to fetch the child column blocks
|
59
|
+
# This is a placeholder showing the expected output format
|
60
|
+
markdown = "::: columns\n"
|
61
|
+
|
62
|
+
# Placeholder for column content extraction
|
63
|
+
# In reality, you'd iterate through the child blocks
|
64
|
+
markdown += "::: column\nColumn content here\n:::\n"
|
65
|
+
|
66
|
+
markdown += ":::"
|
67
|
+
return markdown
|
68
|
+
|
69
|
+
@classmethod
|
70
|
+
def find_matches(cls, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
|
71
|
+
"""
|
72
|
+
Find all column block matches in the text and return their positions.
|
73
|
+
|
74
|
+
Args:
|
75
|
+
text: The text to search in
|
76
|
+
|
77
|
+
Returns:
|
78
|
+
List of tuples with (start_pos, end_pos, block_data)
|
79
|
+
"""
|
80
|
+
matches = []
|
81
|
+
for match in cls.PATTERN.finditer(text):
|
82
|
+
block_data = cls.markdown_to_notion(match.group(0))
|
83
|
+
if block_data:
|
84
|
+
matches.append((match.start(), match.end(), block_data))
|
85
|
+
|
86
|
+
return matches
|
87
|
+
|
88
|
+
@classmethod
|
89
|
+
def is_multiline(cls) -> bool:
|
90
|
+
return True
|
91
|
+
|
92
|
+
@classmethod
|
93
|
+
def get_llm_prompt_content(cls) -> ElementPromptContent:
|
94
|
+
"""
|
95
|
+
Returns structured LLM prompt metadata for the columns element.
|
96
|
+
"""
|
97
|
+
return (
|
98
|
+
ElementPromptBuilder()
|
99
|
+
.with_description(
|
100
|
+
"Create multi-column layouts using Pandoc-style fenced divs. Perfect for side-by-side comparisons, "
|
101
|
+
"parallel content, or creating newsletter-style layouts. Each column can contain any markdown content "
|
102
|
+
"including headers, lists, images, and even nested blocks."
|
103
|
+
)
|
104
|
+
.with_usage_guidelines(
|
105
|
+
"Use columns when you need to present information side-by-side for comparison, create visual balance "
|
106
|
+
"in your layout, or organize related content in parallel. Great for pros/cons lists, before/after "
|
107
|
+
"comparisons, or displaying multiple related items. Keep column content balanced in length for best "
|
108
|
+
"visual results."
|
109
|
+
)
|
110
|
+
.with_syntax(
|
111
|
+
"::: columns\n"
|
112
|
+
"::: column\n"
|
113
|
+
"Content for first column\n"
|
114
|
+
":::\n"
|
115
|
+
"::: column\n"
|
116
|
+
"Content for second column\n"
|
117
|
+
":::\n"
|
118
|
+
":::"
|
119
|
+
)
|
120
|
+
.with_examples(
|
121
|
+
[
|
122
|
+
# Simple two-column example
|
123
|
+
"::: columns\n"
|
124
|
+
"::: column\n"
|
125
|
+
"### Pros\n"
|
126
|
+
"- Fast performance\n"
|
127
|
+
"- Easy to use\n"
|
128
|
+
"- Great documentation\n"
|
129
|
+
":::\n"
|
130
|
+
"::: column\n"
|
131
|
+
"### Cons\n"
|
132
|
+
"- Limited customization\n"
|
133
|
+
"- Requires subscription\n"
|
134
|
+
"- No offline mode\n"
|
135
|
+
":::\n"
|
136
|
+
":::",
|
137
|
+
# Three-column example
|
138
|
+
"::: columns\n"
|
139
|
+
"::: column\n"
|
140
|
+
"**Python**\n"
|
141
|
+
"```python\n"
|
142
|
+
"print('Hello')\n"
|
143
|
+
"```\n"
|
144
|
+
":::\n"
|
145
|
+
"::: column\n"
|
146
|
+
"**JavaScript**\n"
|
147
|
+
"```javascript\n"
|
148
|
+
"console.log('Hello');\n"
|
149
|
+
"```\n"
|
150
|
+
":::\n"
|
151
|
+
"::: column\n"
|
152
|
+
"**Ruby**\n"
|
153
|
+
"```ruby\n"
|
154
|
+
"puts 'Hello'\n"
|
155
|
+
"```\n"
|
156
|
+
":::\n"
|
157
|
+
":::",
|
158
|
+
# Mixed content example
|
159
|
+
"::: columns\n"
|
160
|
+
"::: column\n"
|
161
|
+
"\n"
|
162
|
+
"Product photo\n"
|
163
|
+
":::\n"
|
164
|
+
"::: column\n"
|
165
|
+
"## Product Details\n"
|
166
|
+
"- Price: $99\n"
|
167
|
+
"- Weight: 2kg\n"
|
168
|
+
"- Color: Blue\n"
|
169
|
+
"\n"
|
170
|
+
"[Order Now](link)\n"
|
171
|
+
":::\n"
|
172
|
+
":::",
|
173
|
+
]
|
174
|
+
)
|
175
|
+
.with_avoidance_guidelines(
|
176
|
+
"Avoid nesting column blocks within column blocks - this creates confusing layouts. "
|
177
|
+
"Don't use columns for content that should be read sequentially. Keep the number of columns "
|
178
|
+
"reasonable (2-4 max) for readability. Ensure each ::: marker is on its own line with proper "
|
179
|
+
"nesting. Don't mix column syntax with regular markdown formatting on the same line."
|
180
|
+
)
|
181
|
+
.build()
|
182
|
+
)
|
183
|
+
|
184
|
+
@classmethod
|
185
|
+
def get_column_content(cls, text: str) -> List[str]:
|
186
|
+
"""
|
187
|
+
Extract the content of individual columns from the markdown.
|
188
|
+
This is a helper method that can be used by the implementation
|
189
|
+
to process column content separately.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
text: The complete columns markdown block
|
193
|
+
|
194
|
+
Returns:
|
195
|
+
List of column content strings
|
196
|
+
"""
|
197
|
+
match = cls.PATTERN.search(text)
|
198
|
+
if not match:
|
199
|
+
return []
|
200
|
+
|
201
|
+
columns_content = match.group(1)
|
202
|
+
return [
|
203
|
+
content.strip() for content in cls.COLUMN_PATTERN.findall(columns_content)
|
204
|
+
]
|
@@ -57,11 +57,11 @@ class DividerElement(NotionBlockElement):
|
|
57
57
|
"Creates a horizontal divider line to visually separate sections of content."
|
58
58
|
)
|
59
59
|
.with_usage_guidelines(
|
60
|
-
"Use
|
60
|
+
"Use dividers only sparingly and only when the user explicitly asks for them. Dividers create strong visual breaks between content sections, so they should not be used unless specifically requested by the user."
|
61
61
|
)
|
62
62
|
.with_syntax("---")
|
63
63
|
.with_examples(
|
64
64
|
["## Section 1\nContent\n\n---\n\n## Section 2\nMore content"]
|
65
65
|
)
|
66
66
|
.build()
|
67
|
-
)
|
67
|
+
)
|
@@ -126,7 +126,7 @@ class BlockRegistry:
|
|
126
126
|
|
127
127
|
formatter_names = [e.__name__ for e in element_classes]
|
128
128
|
if "TextInlineFormatter" not in formatter_names:
|
129
|
-
element_classes = [TextInlineFormatter]
|
129
|
+
element_classes = element_classes + [TextInlineFormatter]
|
130
130
|
|
131
131
|
return MarkdownSyntaxPromptGenerator.generate_system_prompt(element_classes)
|
132
132
|
|
@@ -27,6 +27,7 @@ from notionary.elements.toggleable_heading_element import ToggleableHeadingEleme
|
|
27
27
|
from notionary.elements.video_element import VideoElement
|
28
28
|
from notionary.elements.toggle_element import ToggleElement
|
29
29
|
from notionary.elements.bookmark_element import BookmarkElement
|
30
|
+
from notionary.elements.column_element import ColumnsElement
|
30
31
|
|
31
32
|
|
32
33
|
class BlockRegistryBuilder:
|
@@ -262,6 +263,12 @@ class BlockRegistryBuilder:
|
|
262
263
|
def with_toggleable_heading_element(self) -> BlockRegistryBuilder:
|
263
264
|
return self.add_element(ToggleableHeadingElement)
|
264
265
|
|
266
|
+
def with_columns(self) -> BlockRegistryBuilder:
|
267
|
+
"""
|
268
|
+
Add support for column elements.
|
269
|
+
"""
|
270
|
+
return self.add_element(ColumnsElement)
|
271
|
+
|
265
272
|
def build(self) -> BlockRegistry:
|
266
273
|
"""
|
267
274
|
Build and return the configured BlockRegistry instance.
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from typing import Any, Dict
|
2
|
+
from textwrap import dedent
|
2
3
|
|
3
4
|
from notionary.elements.divider_element import DividerElement
|
4
5
|
from notionary.elements.registry.block_registry import BlockRegistry
|
@@ -37,15 +38,24 @@ class PageContentWriter(LoggingMixin):
|
|
37
38
|
async def append_markdown(self, markdown_text: str, append_divider=False) -> bool:
|
38
39
|
"""
|
39
40
|
Append markdown text to a Notion page, automatically handling content length limits.
|
40
|
-
|
41
41
|
"""
|
42
|
+
# Check for leading whitespace in the first three lines and log a warning if found
|
43
|
+
first_three_lines = markdown_text.split('\n')[:3]
|
44
|
+
if any(line.startswith(' ') or line.startswith('\t') for line in first_three_lines):
|
45
|
+
self.logger.warning(
|
46
|
+
"Leading whitespace detected in input markdown. Consider using textwrap.dedent or similar logic: "
|
47
|
+
"this code is indented the wrong way, which could lead to formatting issues."
|
48
|
+
)
|
49
|
+
|
50
|
+
markdown_text = "\n".join(line.lstrip() for line in markdown_text.split("\n"))
|
51
|
+
|
42
52
|
if append_divider and not self.block_registry.contains(DividerElement):
|
43
53
|
self.logger.warning(
|
44
54
|
"DividerElement not registered. Appending divider skipped."
|
45
55
|
)
|
46
56
|
append_divider = False
|
47
57
|
|
48
|
-
# Append divider in
|
58
|
+
# Append divider in markdown format as it will be converted to a Notion divider block
|
49
59
|
if append_divider:
|
50
60
|
markdown_text = markdown_text + "\n\n---\n\n"
|
51
61
|
|
@@ -1,4 +1,5 @@
|
|
1
1
|
from typing import Dict, Any, List, Optional, Tuple
|
2
|
+
import re
|
2
3
|
|
3
4
|
from notionary.elements.registry.block_registry import BlockRegistry
|
4
5
|
from notionary.elements.registry.block_registry_builder import (
|
@@ -9,9 +10,11 @@ from notionary.elements.registry.block_registry_builder import (
|
|
9
10
|
class MarkdownToNotionConverter:
|
10
11
|
"""Converts Markdown text to Notion API block format with support for pipe syntax for nested structures."""
|
11
12
|
|
12
|
-
SPACER_MARKER = "
|
13
|
+
SPACER_MARKER = "---spacer---"
|
13
14
|
TOGGLE_ELEMENT_TYPES = ["ToggleElement", "ToggleableHeadingElement"]
|
14
15
|
PIPE_CONTENT_PATTERN = r"^\|\s?(.*)$"
|
16
|
+
HEADING_PATTERN = r"^(#{1,6})\s+(.+)$"
|
17
|
+
DIVIDER_PATTERN = r"^-{3,}$"
|
15
18
|
|
16
19
|
def __init__(self, block_registry: Optional[BlockRegistry] = None):
|
17
20
|
"""Initialize the converter with an optional custom block registry."""
|
@@ -24,9 +27,12 @@ class MarkdownToNotionConverter:
|
|
24
27
|
if not markdown_text:
|
25
28
|
return []
|
26
29
|
|
30
|
+
# Preprocess markdown to add spacers before headings and dividers
|
31
|
+
processed_markdown = self._add_spacers_before_elements(markdown_text)
|
32
|
+
|
27
33
|
# Collect all blocks with their positions in the text
|
28
34
|
all_blocks_with_positions = self._collect_all_blocks_with_positions(
|
29
|
-
|
35
|
+
processed_markdown
|
30
36
|
)
|
31
37
|
|
32
38
|
# Sort all blocks by their position in the text
|
@@ -38,6 +44,39 @@ class MarkdownToNotionConverter:
|
|
38
44
|
# Process spacing between blocks
|
39
45
|
return self._process_block_spacing(blocks)
|
40
46
|
|
47
|
+
def _add_spacers_before_elements(self, markdown_text: str) -> str:
|
48
|
+
"""Add spacer markers before every heading (except the first one) and before every divider."""
|
49
|
+
lines = markdown_text.split('\n')
|
50
|
+
processed_lines = []
|
51
|
+
found_first_heading = False
|
52
|
+
|
53
|
+
i = 0
|
54
|
+
while i < len(lines):
|
55
|
+
line = lines[i]
|
56
|
+
|
57
|
+
# Check if line is a heading
|
58
|
+
if re.match(self.HEADING_PATTERN, line):
|
59
|
+
if found_first_heading:
|
60
|
+
# Only add a single spacer line before headings (no extra line breaks)
|
61
|
+
processed_lines.append(self.SPACER_MARKER)
|
62
|
+
else:
|
63
|
+
found_first_heading = True
|
64
|
+
|
65
|
+
processed_lines.append(line)
|
66
|
+
|
67
|
+
# Check if line is a divider
|
68
|
+
elif re.match(self.DIVIDER_PATTERN, line):
|
69
|
+
# Only add a single spacer line before dividers (no extra line breaks)
|
70
|
+
processed_lines.append(self.SPACER_MARKER)
|
71
|
+
processed_lines.append(line)
|
72
|
+
|
73
|
+
else:
|
74
|
+
processed_lines.append(line)
|
75
|
+
|
76
|
+
i += 1
|
77
|
+
|
78
|
+
return '\n'.join(processed_lines)
|
79
|
+
|
41
80
|
def _collect_all_blocks_with_positions(
|
42
81
|
self, markdown_text: str
|
43
82
|
) -> List[Tuple[int, int, Dict[str, Any]]]:
|
@@ -75,13 +114,10 @@ class MarkdownToNotionConverter:
|
|
75
114
|
if not toggleable_elements:
|
76
115
|
return []
|
77
116
|
|
78
|
-
# Process each toggleable element type
|
79
117
|
for element in toggleable_elements:
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
if matches:
|
84
|
-
toggleable_blocks.extend(matches)
|
118
|
+
matches = element.find_matches(text, self.convert, context_aware=True)
|
119
|
+
if matches:
|
120
|
+
toggleable_blocks.extend(matches)
|
85
121
|
|
86
122
|
return toggleable_blocks
|
87
123
|
|
@@ -112,9 +148,6 @@ class MarkdownToNotionConverter:
|
|
112
148
|
|
113
149
|
multiline_blocks = []
|
114
150
|
for element in multiline_elements:
|
115
|
-
if not hasattr(element, "find_matches"):
|
116
|
-
continue
|
117
|
-
|
118
151
|
matches = element.find_matches(text)
|
119
152
|
|
120
153
|
if not matches:
|
@@ -202,8 +235,6 @@ class MarkdownToNotionConverter:
|
|
202
235
|
|
203
236
|
def _is_pipe_syntax_line(self, line: str) -> bool:
|
204
237
|
"""Check if a line uses pipe syntax (for nested content)."""
|
205
|
-
import re
|
206
|
-
|
207
238
|
return bool(re.match(self.PIPE_CONTENT_PATTERN, line))
|
208
239
|
|
209
240
|
def _process_line(
|
@@ -433,4 +464,4 @@ class MarkdownToNotionConverter:
|
|
433
464
|
return False
|
434
465
|
|
435
466
|
rich_text = block.get("paragraph", {}).get("rich_text", [])
|
436
|
-
return not rich_text or len(rich_text) == 0
|
467
|
+
return not rich_text or len(rich_text) == 0
|
@@ -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,9 @@ class MarkdownSyntaxPromptGenerator:
|
|
11
11
|
and formats them optimally for LLMs.
|
12
12
|
"""
|
13
13
|
|
14
|
-
SYSTEM_PROMPT_TEMPLATE =
|
15
|
-
|
16
|
-
You
|
17
|
-
Notion supports standard Markdown with some special extensions for creating rich content.
|
14
|
+
SYSTEM_PROMPT_TEMPLATE = (
|
15
|
+
"""
|
16
|
+
You create content for Notion pages using Markdown syntax with special Notion extensions.
|
18
17
|
|
19
18
|
# Understanding Notion Blocks
|
20
19
|
|
@@ -27,29 +26,36 @@ class MarkdownSyntaxPromptGenerator:
|
|
27
26
|
|
28
27
|
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
28
|
|
30
|
-
2.
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
29
|
+
2. BACKTICK HANDLING - EXTREMELY IMPORTANT:
|
30
|
+
- NEVER wrap entire content or responses in triple backticks (```).
|
31
|
+
- DO NOT use triple backticks (```) for anything except CODE BLOCKS or DIAGRAMS.
|
32
|
+
- DO NOT use triple backticks to mark or highlight regular text or examples.
|
33
|
+
- USE triple backticks ONLY for actual programming code, pseudocode, or specialized notation.
|
34
|
+
- For inline code, use single backticks (`code`).
|
35
|
+
- When showing Markdown syntax examples, use inline code formatting with single backticks.
|
36
|
+
|
37
|
+
3. CONTENT FORMATTING - CRITICAL:
|
38
|
+
- DO NOT include introductory phrases like "I understand that..." or "Here's the content...".
|
39
|
+
- Provide ONLY the requested content directly without any prefacing text or meta-commentary.
|
40
|
+
- Generate just the content itself, formatted according to these guidelines.
|
41
|
+
- USE INLINE FORMATTING to enhance readability:
|
42
|
+
- Use *italic* for emphasis, terminology, and definitions
|
43
|
+
- Use `code` for technical terms, file paths, variables, and commands
|
44
|
+
- Use **bold** sparingly for truly important information
|
45
|
+
- Use appropriate inline formatting naturally throughout the content, but don't overuse it
|
46
|
+
|
47
|
+
4. USER INSTRUCTIONS - VERY IMPORTANT:
|
48
|
+
- Follow the user's formatting instructions EXACTLY and in the specified order
|
49
|
+
- When the user requests specific elements (e.g., "first a callout, then 4 bullet points"), create them in that precise sequence
|
50
|
+
- Adhere strictly to any structural requirements provided by the user
|
51
|
+
- Do not deviate from or reinterpret the user's formatting requests
|
52
|
+
|
53
|
+
5. ADD EMOJIS TO HEADINGS - REQUIRED UNLESS EXPLICITLY TOLD NOT TO:
|
54
|
+
- ALWAYS add appropriate emojis at the beginning of headings to improve structure and readability
|
55
|
+
- Choose emojis that represent the content or theme of each section
|
56
|
+
- Format as: ## 🚀 Heading Text (with space after emoji)
|
57
|
+
- Only omit emojis if the user explicitly instructs you not to use them
|
58
|
+
"""
|
53
59
|
)
|
54
60
|
|
55
61
|
@staticmethod
|
@@ -108,4 +114,4 @@ class MarkdownSyntaxPromptGenerator:
|
|
108
114
|
Generates a complete system prompt for LLMs.
|
109
115
|
"""
|
110
116
|
element_docs = cls.generate_element_docs(element_classes)
|
111
|
-
return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
|
117
|
+
return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
|
@@ -0,0 +1,245 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: notionary
|
3
|
+
Version: 0.2.7
|
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
|
+
### Code Blocks
|
157
|
+
|
158
|
+
```python
|
159
|
+
def hello_world():
|
160
|
+
print("Hello from Notionary!")
|
161
|
+
```
|
162
|
+
|
163
|
+
### To-do Lists
|
164
|
+
|
165
|
+
```markdown
|
166
|
+
- [ ] Define project scope
|
167
|
+
- [x] Create timeline
|
168
|
+
- [ ] Assign resources
|
169
|
+
```
|
170
|
+
|
171
|
+
### Tables
|
172
|
+
|
173
|
+
```markdown
|
174
|
+
| Feature | Status | Priority |
|
175
|
+
| --------------- | ----------- | -------- |
|
176
|
+
| API Integration | Complete | High |
|
177
|
+
| Documentation | In Progress | Medium |
|
178
|
+
```
|
179
|
+
|
180
|
+
### More Elements
|
181
|
+
|
182
|
+
```markdown
|
183
|
+

|
184
|
+
@[Caption](https://youtube.com/watch?v=...)
|
185
|
+
[bookmark](https://example.com "Title" "Description")
|
186
|
+
```
|
187
|
+
|
188
|
+
## Block Registry & Customization
|
189
|
+
|
190
|
+
```python
|
191
|
+
from notionary import NotionPage, BlockRegistryBuilder
|
192
|
+
|
193
|
+
# Create a custom registry with only the elements you need
|
194
|
+
custom_registry = (
|
195
|
+
BlockRegistryBuilder()
|
196
|
+
.with_headings()
|
197
|
+
.with_callouts()
|
198
|
+
.with_toggles()
|
199
|
+
.with_code()
|
200
|
+
.with_todos()
|
201
|
+
.with_paragraphs()
|
202
|
+
.build()
|
203
|
+
)
|
204
|
+
|
205
|
+
# Apply this registry to a page
|
206
|
+
page = NotionPage.from_url("https://www.notion.so/your-page-url")
|
207
|
+
page.block_registry = custom_registry
|
208
|
+
ark
|
209
|
+
# Replace content using only supported elements
|
210
|
+
await page.replace_content("# Custom heading with selected elements only")
|
211
|
+
```
|
212
|
+
|
213
|
+
## AI-Ready: Generate LLM Prompts
|
214
|
+
|
215
|
+
```python
|
216
|
+
from notionary import BlockRegistryBuilder
|
217
|
+
|
218
|
+
# Create a registry with all standard elements
|
219
|
+
registry = BlockRegistryBuilder.create_full_registry()
|
220
|
+
|
221
|
+
# Generate the LLM system prompt
|
222
|
+
llm_system_prompt = registry.get_notion_markdown_syntax_prompt()
|
223
|
+
print(llm_system_prompt)
|
224
|
+
```
|
225
|
+
|
226
|
+
## Examples
|
227
|
+
|
228
|
+
See the `examples/` folder for:
|
229
|
+
|
230
|
+
- [Database discovery and querying](examples/database_discovery_example.py)
|
231
|
+
- [Rich page creation with Markdown](examples/page_example.py)
|
232
|
+
- [Database management](examples/database_management_example.py)
|
233
|
+
- [Iterating through database entries](examples/database_iteration_example.py)
|
234
|
+
- [Temporary usage & debugging](examples/temp.py)
|
235
|
+
|
236
|
+
## Perfect for AI Agents and Automation
|
237
|
+
|
238
|
+
- **LLM Integration**: Generate Notion-compatible content with any LLM using the system prompt generator
|
239
|
+
- **Dynamic Content Generation**: AI agents can generate content in Markdown and render it directly as Notion pages
|
240
|
+
- **Schema-Aware Operations**: Automatically validate and format properties based on database schemas
|
241
|
+
- **Simplified API**: Clean, intuitive interface for both human developers and AI systems
|
242
|
+
|
243
|
+
## Contributing
|
244
|
+
|
245
|
+
Contributions welcome — feel free to submit a pull request!
|
@@ -1,6 +1,6 @@
|
|
1
1
|
notionary/__init__.py,sha256=hPvZ-iqt5R_dAs9KaRBhC5eXzuQ5uvt-9EaU2O_7bZw,691
|
2
2
|
notionary/notion_client.py,sha256=O-lvy2-jMNSDc_8cWKuGVfckCdc1_PiQOlfxQKjQ6_A,7325
|
3
|
-
notionary/database/database_discovery.py,sha256=
|
3
|
+
notionary/database/database_discovery.py,sha256=Ebn-9HuNb8fwqf9KP4Vyys8t5zCKUOqBACS_-mohkbk,4498
|
4
4
|
notionary/database/notion_database.py,sha256=zbHPejETr101pprd7kewZ555d_TONN_wJi7b9Eyfoyg,7634
|
5
5
|
notionary/database/notion_database_factory.py,sha256=FmijGYz6A4mCWVionOg9sxgFXfb9he52xdgNswJw24k,6584
|
6
6
|
notionary/database/models/page_result.py,sha256=Vmm5_oYpYAkIIJVoTd1ZZGloeC3cmFLMYP255mAmtaw,233
|
@@ -8,8 +8,9 @@ notionary/elements/audio_element.py,sha256=7bEpFl9jA6S1UZlEXsmFzEUVoViEp1o_7zZIC
|
|
8
8
|
notionary/elements/bookmark_element.py,sha256=msCtZvuPkIj1kiShNwE8i1GDYwamFb5mwRyZm4XyVY4,8145
|
9
9
|
notionary/elements/bulleted_list_element.py,sha256=obsb3JqUNET3uS5OZM3yzDqxSzJzUuEob-Fzx0UIg9Y,2664
|
10
10
|
notionary/elements/callout_element.py,sha256=ZsRvRtVy9kxdTwgrB5JGjZ4qcCiwcC0WimWJ_cW0aLY,4492
|
11
|
-
notionary/elements/code_block_element.py,sha256=
|
12
|
-
notionary/elements/
|
11
|
+
notionary/elements/code_block_element.py,sha256=YHOiV2eQIe7gbsOnrsQnztomTZ-eP3HRHsonzrj3Tt8,7529
|
12
|
+
notionary/elements/column_element.py,sha256=lMVRKXndOuN6lsBMlkgZ-11uuoEFLtUFedwrPAswL1E,7555
|
13
|
+
notionary/elements/divider_element.py,sha256=Kt2oJJQD3zKyWSQ3JOG0nUgYqJRodMO4UVy7EXixxes,2330
|
13
14
|
notionary/elements/embed_element.py,sha256=Zcc18Kl8SGoG98P2aYE0TkBviRvSz-sYOdjMEs-tvgk,4579
|
14
15
|
notionary/elements/heading_element.py,sha256=kqgjyfaawEODir2tzDyf7-7wm38DbqoZnsH5k94GsA0,3013
|
15
16
|
notionary/elements/image_element.py,sha256=cwdovaWK8e4uZJU97l_fJ2etAxAgM2rG2EE34t4eag8,4758
|
@@ -24,20 +25,20 @@ notionary/elements/todo_element.py,sha256=ND3oOzSnd0l1AUGTcG2NiHW50ZbI4-atjtNorL
|
|
24
25
|
notionary/elements/toggle_element.py,sha256=h9vYkkAIUHzn-0mu31qC6UPdlk_0EFIsU5A4T_A2ZI8,11082
|
25
26
|
notionary/elements/toggleable_heading_element.py,sha256=XdaPsd8anufwAACL8J-Egd_RcqPqZ1gFlzeol1GOyyc,9960
|
26
27
|
notionary/elements/video_element.py,sha256=y0OmOYXdQBc2rSYAHRmA4l4rzNqPnyhuXbEipcgzQgY,5727
|
27
|
-
notionary/elements/registry/block_registry.py,sha256=
|
28
|
-
notionary/elements/registry/block_registry_builder.py,sha256=
|
28
|
+
notionary/elements/registry/block_registry.py,sha256=T2yKRyzsdC9OSWdsiG-AI2T60SmtaR-7QaM6lOz0qrw,5028
|
29
|
+
notionary/elements/registry/block_registry_builder.py,sha256=KU1Qh3qaB1lMrSBj1iyi8Hkx1h0HfxtajmkXZhMb68k,9545
|
29
30
|
notionary/exceptions/database_exceptions.py,sha256=I-Tx6bYRLpi5pjGPtbT-Mqxvz3BFgYTiuZxknJeLxtI,2638
|
30
31
|
notionary/exceptions/page_creation_exception.py,sha256=4v7IuZD6GsQLrqhDLriGjuG3ML638gAO53zDCrLePuU,281
|
31
32
|
notionary/models/notion_block_response.py,sha256=gzL4C6K9QPcaMS6NbAZaRceSEnMbNwYBVVzxysza5VU,6002
|
32
33
|
notionary/models/notion_database_response.py,sha256=FMAasQP20S12J_KMdMlNpcHHwxFKX2YtbE4Q9xn-ruQ,1213
|
33
34
|
notionary/models/notion_page_response.py,sha256=r4fwMwwDocj92JdbSmyrzIqBKsnEaz4aDUiPabrg9BM,1762
|
34
|
-
notionary/page/markdown_to_notion_converter.py,sha256=
|
35
|
+
notionary/page/markdown_to_notion_converter.py,sha256=QYlxotQoBK5Ruj7UvfYfMdbsylQCMw7OX6Ng5CyMeVo,17097
|
35
36
|
notionary/page/notion_page.py,sha256=NDxAJaNk4tlKUrenhKBdnuvjlVgnxC0Z6fprf2LyNeE,18046
|
36
37
|
notionary/page/notion_page_factory.py,sha256=2A3M5Ub_kV2-q7PPRqDgfwBjhkGCwtL5i3Kr2RfvvVo,7213
|
37
38
|
notionary/page/notion_to_markdown_converter.py,sha256=vUQss0J7LUFLULGvW27PjaTFuWi8OsRQAUBowSYorkM,6408
|
38
39
|
notionary/page/content/notion_page_content_chunker.py,sha256=xRks74Dqec-De6-AVTxMPnXs-MSJBzSm1HfJfaHiKr8,3330
|
39
40
|
notionary/page/content/page_content_retriever.py,sha256=f8IU1CIfSTTT07m72-vgpUr_VOCsisqqFHQ1JeOhb3g,2222
|
40
|
-
notionary/page/content/page_content_writer.py,sha256=
|
41
|
+
notionary/page/content/page_content_writer.py,sha256=ZLqDBiYdkdCjgZgucoBGiUH8qM46zltIbJqu1aBqXzw,4425
|
41
42
|
notionary/page/metadata/metadata_editor.py,sha256=HI7m8Zn_Lz6x36rBnW1EnbicVS-4Q8NmCJYKN-OlY-c,5130
|
42
43
|
notionary/page/metadata/notion_icon_manager.py,sha256=6a9GS5sT0trfuAb0hlF2Cw_Wc1oM59a1QA4kO9asvMA,2576
|
43
44
|
notionary/page/metadata/notion_page_cover_manager.py,sha256=gHQSA8EtO4gbkMt_C3nKc0DF44SY_4ycd57cJSihdqk,2215
|
@@ -49,12 +50,12 @@ notionary/page/relations/notion_page_relation_manager.py,sha256=tfkvLHClaYel_uEa
|
|
49
50
|
notionary/page/relations/notion_page_title_resolver.py,sha256=dIjiEeHjjNT-DrIhz1nynkfHkMpUuJJFOEjb25Wy7f4,3575
|
50
51
|
notionary/page/relations/page_database_relation.py,sha256=8lEp8fQjPwjWhA8nZu3k8mW6EEc54ki1Uwf4iUV1DOU,2245
|
51
52
|
notionary/prompting/element_prompt_content.py,sha256=tHref-SKA81Ua_IQD2Km7y7BvFtHl74haSIjHNYE3FE,4403
|
52
|
-
notionary/prompting/markdown_syntax_prompt_generator.py,sha256=
|
53
|
+
notionary/prompting/markdown_syntax_prompt_generator.py,sha256=WHTpftR7LdY-yA54TlicVwt6R-mhZ2OpUFmNKaCSG0I,5096
|
53
54
|
notionary/util/logging_mixin.py,sha256=b6wHj0IoVSWXbHh0yynfJlwvIR33G2qmaGNzrqyb7Gs,1825
|
54
55
|
notionary/util/page_id_utils.py,sha256=EYNMxgf-7ghzL5K8lKZBZfW7g5CsdY0Xuj4IYmU8RPk,1381
|
55
56
|
notionary/util/warn_direct_constructor_usage.py,sha256=vyJR73F95XVSRWIbyij-82IGOpAne9SBPM25eDpZfSU,1715
|
56
|
-
notionary-0.2.
|
57
|
-
notionary-0.2.
|
58
|
-
notionary-0.2.
|
59
|
-
notionary-0.2.
|
60
|
-
notionary-0.2.
|
57
|
+
notionary-0.2.7.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
|
58
|
+
notionary-0.2.7.dist-info/METADATA,sha256=KZbjwvx9HJQ2BCg5RnRgZrhOGa43r8QMOWF2yKwY1uA,7116
|
59
|
+
notionary-0.2.7.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
60
|
+
notionary-0.2.7.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
|
61
|
+
notionary-0.2.7.dist-info/RECORD,,
|
@@ -1,256 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: notionary
|
3
|
-
Version: 0.2.6
|
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
|
-
## Features
|
35
|
-
|
36
|
-
- **Rich Markdown Support**: Create Notion pages using intuitive Markdown syntax with custom extensions
|
37
|
-
- **Dynamic Database Operations**: Create, update, and query database entries with schema auto-detection
|
38
|
-
- **Extensible Block Registry**: Add, customize, or remove Notion block elements with a flexible registry pattern
|
39
|
-
- **LLM-Ready Prompts**: Generate system prompts explaining Markdown syntax for LLMs to create Notion content
|
40
|
-
- **Async-First Design**: Built for modern Python with full async/await support
|
41
|
-
- **Schema-Based Validation**: Automatic property validation based on database schemas
|
42
|
-
- **Intelligent Content Conversion**: Bidirectional conversion between Markdown and Notion blocks
|
43
|
-
|
44
|
-
## Installation
|
45
|
-
|
46
|
-
```bash
|
47
|
-
pip install notionary
|
48
|
-
```
|
49
|
-
|
50
|
-
## Custom Markdown Syntax
|
51
|
-
|
52
|
-
Notionary extends standard Markdown with special syntax to support Notion-specific features:
|
53
|
-
|
54
|
-
### Text Formatting
|
55
|
-
|
56
|
-
- Standard Markdown: `**bold**`, `*italic*`, `~~strikethrough~~`, `` `code` ``
|
57
|
-
- Highlights: `==highlighted text==`, `==red:warning==`, `==blue:note==`
|
58
|
-
|
59
|
-
### Block Elements
|
60
|
-
|
61
|
-
#### Callouts
|
62
|
-
|
63
|
-
```markdown
|
64
|
-
!> [💡] This is a default callout with the light bulb emoji
|
65
|
-
!> [🔔] This is a callout with a bell emoji
|
66
|
-
!> {blue_background} [💧] This is a blue callout with a water drop emoji
|
67
|
-
!> {yellow_background} [⚠️] Warning: This is an important note
|
68
|
-
```
|
69
|
-
|
70
|
-
#### Toggles
|
71
|
-
|
72
|
-
```markdown
|
73
|
-
+++ How to use NotionPageManager
|
74
|
-
|
75
|
-
1. Initialize with NotionPageManager
|
76
|
-
2. Update metadata with set_title(), set_page_icon(), etc.
|
77
|
-
3. Add content with replace_content() or append_markdown()
|
78
|
-
```
|
79
|
-
|
80
|
-
#### Bookmarks
|
81
|
-
|
82
|
-
```markdown
|
83
|
-
[bookmark](https://notion.so "Notion Homepage" "Your connected workspace")
|
84
|
-
```
|
85
|
-
|
86
|
-
#### Multi-Column Layouts
|
87
|
-
|
88
|
-
```markdown
|
89
|
-
::: columns
|
90
|
-
::: column
|
91
|
-
Content for first column
|
92
|
-
:::
|
93
|
-
::: column
|
94
|
-
Content for second column
|
95
|
-
:::
|
96
|
-
:::
|
97
|
-
```
|
98
|
-
|
99
|
-
And more:
|
100
|
-
|
101
|
-
- Tables with standard Markdown syntax
|
102
|
-
- Code blocks with syntax highlighting
|
103
|
-
- To-do lists with `- [ ]` and `- [x]`
|
104
|
-
- Block quotes with `>`
|
105
|
-
|
106
|
-
## Database Management
|
107
|
-
|
108
|
-
Notionary makes it easy to work with Notion databases, automatically handling schema detection and property conversion:
|
109
|
-
|
110
|
-
```python
|
111
|
-
import asyncio
|
112
|
-
from notionary import NotionDatabaseFactory
|
113
|
-
|
114
|
-
async def main():
|
115
|
-
# Find database by name with fuzzy matching
|
116
|
-
db_manager = await NotionDatabaseFactory.from_database_name("Projects")
|
117
|
-
|
118
|
-
# Create a new page with properties
|
119
|
-
properties = {
|
120
|
-
"Title": "Created via Notionary",
|
121
|
-
"Status": "In Progress",
|
122
|
-
"Priority": "High"
|
123
|
-
}
|
124
|
-
|
125
|
-
page = await db_manager.create_blank_page()
|
126
|
-
|
127
|
-
# Set page content with rich Markdown
|
128
|
-
await page.set_title("My New Project")
|
129
|
-
await page.set_page_icon(emoji="🚀")
|
130
|
-
|
131
|
-
markdown = """
|
132
|
-
# Project Overview
|
133
|
-
|
134
|
-
!> [💡] This page was created programmatically using Notionary.
|
135
|
-
|
136
|
-
## Tasks
|
137
|
-
- [ ] Define project scope
|
138
|
-
- [ ] Create timeline
|
139
|
-
- [ ] Assign resources
|
140
|
-
|
141
|
-
+++ Implementation Details
|
142
|
-
This project will use our standard architecture with custom extensions.
|
143
|
-
"""
|
144
|
-
|
145
|
-
await page.replace_content(markdown)
|
146
|
-
|
147
|
-
if __name__ == "__main__":
|
148
|
-
asyncio.run(main())
|
149
|
-
```
|
150
|
-
|
151
|
-
## Page Content Management
|
152
|
-
|
153
|
-
Create rich Notion pages using enhanced Markdown:
|
154
|
-
|
155
|
-
```python
|
156
|
-
from notionary import NotionPage
|
157
|
-
|
158
|
-
async def create_rich_page():
|
159
|
-
url = "https://www.notion.so/Your-Page-1cd389d57bd381e58be9d35ce24adf3d"
|
160
|
-
page_manager = NotionPage(url=url)
|
161
|
-
|
162
|
-
await page_manager.set_title("Notionary Demo")
|
163
|
-
await page_manager.set_page_icon(emoji="✨")
|
164
|
-
await page_manager.set_page_cover("https://images.unsplash.com/photo-1555066931-4365d14bab8c")
|
165
|
-
|
166
|
-
markdown = '''
|
167
|
-
# Notionary Rich Content Demo
|
168
|
-
|
169
|
-
!> [💡] This page was created with Notionary's custom Markdown syntax.
|
170
|
-
|
171
|
-
## Features
|
172
|
-
- Easy-to-use Python API
|
173
|
-
- **Rich** Markdown support
|
174
|
-
- Async functionality
|
175
|
-
|
176
|
-
+++ Implementation Details
|
177
|
-
Notionary uses a custom converter to transform Markdown into Notion blocks.
|
178
|
-
This makes it easy to create rich content programmatically.
|
179
|
-
'''
|
180
|
-
|
181
|
-
await page_manager.replace_content(markdown)
|
182
|
-
```
|
183
|
-
|
184
|
-
## Block Registry & Builder
|
185
|
-
|
186
|
-
Notionary uses a flexible registry pattern with a builder to customize which Notion elements are supported, allowing programmatic creation of complex UI layouts that were previously only possible through Notion's UI:
|
187
|
-
|
188
|
-
```python
|
189
|
-
from notionary import NotionPage
|
190
|
-
from notionary.elements.block_element_registry_builder import BlockElementRegistryBuilder
|
191
|
-
|
192
|
-
# Create a registry with standard Notion elements
|
193
|
-
registry = BlockElementRegistryBuilder.create_full_registry()
|
194
|
-
|
195
|
-
# Or build a custom registry with only the elements you need
|
196
|
-
custom_registry = (
|
197
|
-
BlockElementRegistryBuilder()
|
198
|
-
.with_headings()
|
199
|
-
.with_callouts()
|
200
|
-
.with_toggles()
|
201
|
-
.with_lists()
|
202
|
-
.with_tables()
|
203
|
-
.with_paragraphs()
|
204
|
-
.build()
|
205
|
-
)
|
206
|
-
|
207
|
-
# Apply this registry to a page to enable custom Markdown support
|
208
|
-
page = NotionPage(url="https://www.notion.so/your-page-url")
|
209
|
-
page.block_registry = custom_registry
|
210
|
-
|
211
|
-
# Now your page supports exactly the elements you've defined
|
212
|
-
await page.replace_content("# Custom heading with only selected elements")
|
213
|
-
```
|
214
|
-
|
215
|
-
This registry approach gives you granular control over which Notion UI elements can be created through Markdown, making it possible to programmatically construct any page layout that would normally require manual UI interaction.
|
216
|
-
|
217
|
-
## AI-Ready LLM Prompt Generation
|
218
|
-
|
219
|
-
Notionary can automatically generate comprehensive system prompts for LLMs to understand Notion's custom Markdown syntax:
|
220
|
-
|
221
|
-
```python
|
222
|
-
from notionary.elements.block_element_registry_builder import BlockElementRegistryBuilder
|
223
|
-
|
224
|
-
registry = BlockElementRegistryBuilder.create_full_registry()
|
225
|
-
llm_system_prompt = registry.generate_llm_prompt()
|
226
|
-
|
227
|
-
# Use this prompt with your LLM to generate Notion-compatible Markdown
|
228
|
-
print(llm_system_prompt)
|
229
|
-
```
|
230
|
-
|
231
|
-
This makes Notionary the perfect foundation for AI-driven Notion content generation, enabling LLMs to create properly formatted Notion pages.
|
232
|
-
|
233
|
-
## Examples
|
234
|
-
|
235
|
-
See the [examples folder](examples/) for more comprehensive demonstrations:
|
236
|
-
|
237
|
-
- [Database discovery and querying](examples/database_discovery_example.py)
|
238
|
-
- [Rich page creation with Markdown](examples/page_example.py)
|
239
|
-
- [Database factory usage](examples/database_factory_example.py)
|
240
|
-
- [Page lookup and access](examples/page_factory_by_url_example.py)
|
241
|
-
- [Iterating through database entries](examples/iter_database_example.py)
|
242
|
-
|
243
|
-
## Perfect for AI Agents and Automation
|
244
|
-
|
245
|
-
- **LLM Integration**: Generate Notion-compatible content with LLMs using the system prompt generator
|
246
|
-
- **Dynamic Content Generation**: AI agents can generate content in Markdown and render it as Notion pages
|
247
|
-
- **Schema-Aware Operations**: Automatically validate and format properties
|
248
|
-
- **Simplified API**: Easier integration with AI workflows
|
249
|
-
|
250
|
-
## License
|
251
|
-
|
252
|
-
MIT
|
253
|
-
|
254
|
-
## Contributing
|
255
|
-
|
256
|
-
Contributions welcome — feel free to submit a pull request!
|
File without changes
|
File without changes
|
File without changes
|