notionary 0.2.5__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.
@@ -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 discover(self, page_size: int = 100) -> List[Tuple[str, str]]:
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.discover(page_size)
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(r"```(\w*)\n([\s\S]+?)```", re.MULTILINE)
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
- return {
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
- return f"```{language}\n{content}\n```"
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
+ "![Image](url)\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 to create clear visual breaks between different sections without requiring headings."
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] + element_classes
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,3 @@
1
- import json
2
1
  from typing import Any, Dict, List, Optional
3
2
 
4
3
  from notionary.elements.registry.block_registry import BlockRegistry
@@ -25,7 +24,6 @@ class PageContentRetriever(LoggingMixin):
25
24
 
26
25
  async def get_page_content(self) -> str:
27
26
  blocks = await self._get_page_blocks_with_children()
28
- print("blocks", json.dumps(blocks, indent=2))
29
27
  return self._notion_to_markdown_converter.convert(blocks)
30
28
 
31
29
  async def _get_page_blocks_with_children(
@@ -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 markdonw format as it will be converted to a Notion divider block
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 = "<!-- spacer -->"
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
- markdown_text
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
- if hasattr(element, "find_matches"):
81
- # Find matches with context awareness
82
- matches = element.find_matches(text, self.convert, context_aware=True)
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 = dedent(
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
+ """
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. INLINE FORMATTING - VERY IMPORTANT:
31
- You can use inline formatting within almost any block type.
32
- Combine **bold**, _italic_, `code`, and other formatting as needed.
33
- Format text to create visual hierarchy and emphasize important points.
34
- DO NOT overuse formatting - be strategic with formatting for best readability.
35
-
36
- 3. BACKTICK HANDLING - EXTREMELY IMPORTANT:
37
- ❌ NEVER wrap entire content or responses in triple backticks (```).
38
- DO NOT use triple backticks (```) for anything except CODE BLOCKS or DIAGRAMS.
39
- DO NOT use triple backticks to mark or highlight regular text or examples.
40
- USE triple backticks ONLY for actual programming code, pseudocode, or specialized notation.
41
- For inline code, use single backticks (`code`).
42
- When showing Markdown syntax examples, use inline code formatting with single backticks.
43
-
44
- 4. BLOCK SEPARATION - IMPORTANT:
45
- Use empty lines between different blocks to ensure proper rendering in Notion.
46
- For major logical sections, use the spacer element (see documentation below).
47
- ⚠️ While headings can sometimes work without an empty line before the following paragraph, including empty lines between all block types ensures consistent rendering.
48
-
49
- 5. CONTENT FORMATTING - CRITICAL:
50
- DO NOT include introductory phrases like "I understand that..." or "Here's the content...".
51
- Provide ONLY the requested content directly without any prefacing text or meta-commentary.
52
- Generate just the content itself, formatted according to these guidelines."""
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
+ [![Python Version](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/downloads/)
30
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](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
+ ![Caption](https://example.com/image.jpg)
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=qDGFhXG9s-_6CXdRg8tMiwX4dvX7jLjgAUFPSNlYtlI,4506
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=Zy5l8bsX2R1QBgmCZDM_tkxzSXylHABshKY8r0kNBBo,5853
12
- notionary/elements/divider_element.py,sha256=0e10YK-CC8uGuL7921dEIjeJK9ha-WhRIYRf2fFuxVQ,2211
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=giWGcdgc3Z60wvfUr-FS6UMc-k-Q6DlXO8T0gl4fVC8,5027
28
- notionary/elements/registry/block_registry_builder.py,sha256=Wnob3PbgzVAoXBW0Eon1KzX4aD4d36KeaDe_uYIKFnU,9311
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=EuqUGNv2HZu67INOnGheeJkt7WHTWGuLnhEG72_Wv5Y,15833
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
- notionary/page/content/page_content_retriever.py,sha256=i6y4Q3_5EIzsd6HRgO3G-v1STZs1060OJe5DFkZi-QI,2290
40
- notionary/page/content/page_content_writer.py,sha256=czBzNCGcwdpqNLSQPyna1s8Y7pjyPzDgJC3UUK5PLGA,3793
40
+ notionary/page/content/page_content_retriever.py,sha256=f8IU1CIfSTTT07m72-vgpUr_VOCsisqqFHQ1JeOhb3g,2222
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=0hD2DZDDjQ3ZKHPvZnDKpUs6Puq5O_E2IGW1te1R6v4,4751
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.5.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
57
- notionary-0.2.5.dist-info/METADATA,sha256=QT7PBlbv3Qu6BqzamzrxZnf28zwZp43_IgBa4F3uCwo,8374
58
- notionary-0.2.5.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
59
- notionary-0.2.5.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
60
- notionary-0.2.5.dist-info/RECORD,,
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.5
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
- [![Python Version](https://img.shields.io/badge/python-3.8%2B-blue.svg)](https://www.python.org/downloads/)
30
- [![License](https://img.shields.io/badge/license-MIT-green.svg)](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!