notionary 0.1.2__py3-none-any.whl → 0.1.3__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.
Files changed (49) hide show
  1. notionary/core/__init__.py +0 -0
  2. notionary/core/converters/__init__.py +50 -0
  3. notionary/core/converters/elements/__init__.py +0 -0
  4. notionary/core/converters/elements/bookmark_element.py +224 -0
  5. notionary/core/converters/elements/callout_element.py +179 -0
  6. notionary/core/converters/elements/code_block_element.py +153 -0
  7. notionary/core/converters/elements/column_element.py +294 -0
  8. notionary/core/converters/elements/divider_element.py +73 -0
  9. notionary/core/converters/elements/heading_element.py +84 -0
  10. notionary/core/converters/elements/image_element.py +130 -0
  11. notionary/core/converters/elements/list_element.py +130 -0
  12. notionary/core/converters/elements/notion_block_element.py +51 -0
  13. notionary/core/converters/elements/paragraph_element.py +73 -0
  14. notionary/core/converters/elements/qoute_element.py +242 -0
  15. notionary/core/converters/elements/table_element.py +306 -0
  16. notionary/core/converters/elements/text_inline_formatter.py +294 -0
  17. notionary/core/converters/elements/todo_lists.py +114 -0
  18. notionary/core/converters/elements/toggle_element.py +205 -0
  19. notionary/core/converters/elements/video_element.py +159 -0
  20. notionary/core/converters/markdown_to_notion_converter.py +482 -0
  21. notionary/core/converters/notion_to_markdown_converter.py +45 -0
  22. notionary/core/converters/registry/__init__.py +0 -0
  23. notionary/core/converters/registry/block_element_registry.py +234 -0
  24. notionary/core/converters/registry/block_element_registry_builder.py +280 -0
  25. notionary/core/database/database_info_service.py +43 -0
  26. notionary/core/database/database_query_service.py +73 -0
  27. notionary/core/database/database_schema_service.py +57 -0
  28. notionary/core/database/models/page_result.py +10 -0
  29. notionary/core/database/notion_database_manager.py +332 -0
  30. notionary/core/database/notion_database_manager_factory.py +233 -0
  31. notionary/core/database/notion_database_schema.py +415 -0
  32. notionary/core/database/notion_database_writer.py +390 -0
  33. notionary/core/database/page_service.py +161 -0
  34. notionary/core/notion_client.py +134 -0
  35. notionary/core/page/meta_data/metadata_editor.py +37 -0
  36. notionary/core/page/notion_page_manager.py +110 -0
  37. notionary/core/page/page_content_manager.py +85 -0
  38. notionary/core/page/property_formatter.py +97 -0
  39. notionary/exceptions/database_exceptions.py +76 -0
  40. notionary/exceptions/page_creation_exception.py +9 -0
  41. notionary/util/logging_mixin.py +47 -0
  42. notionary/util/singleton_decorator.py +20 -0
  43. notionary/util/uuid_utils.py +24 -0
  44. {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/METADATA +1 -1
  45. notionary-0.1.3.dist-info/RECORD +49 -0
  46. notionary-0.1.2.dist-info/RECORD +0 -6
  47. {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/WHEEL +0 -0
  48. {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/licenses/LICENSE +0 -0
  49. {notionary-0.1.2.dist-info → notionary-0.1.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,130 @@
1
+ import re
2
+ from typing import Dict, Any, Optional
3
+ from typing_extensions import override
4
+ from notionary.core.converters.elements.notion_block_element import NotionBlockElement
5
+ from notionary.core.converters.elements.text_inline_formatter import TextInlineFormatter
6
+
7
+
8
+ class BulletedListElement(NotionBlockElement):
9
+ """Class for converting between Markdown bullet lists and Notion bulleted list items."""
10
+
11
+ @override
12
+ @staticmethod
13
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
14
+ """Convert markdown bulleted list item to Notion block."""
15
+ pattern = re.compile(
16
+ r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$"
17
+ ) # Avoid matching todo items
18
+ list_match = pattern.match(text)
19
+ if not list_match:
20
+ return None
21
+
22
+ content = list_match.group(2)
23
+
24
+ # Use parse_inline_formatting to handle rich text
25
+ rich_text = TextInlineFormatter.parse_inline_formatting(content)
26
+
27
+ return {
28
+ "type": "bulleted_list_item",
29
+ "bulleted_list_item": {"rich_text": rich_text, "color": "default"},
30
+ }
31
+
32
+ @override
33
+ @staticmethod
34
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
35
+ """Convert Notion bulleted list item block to markdown."""
36
+ if block.get("type") != "bulleted_list_item":
37
+ return None
38
+
39
+ rich_text = block.get("bulleted_list_item", {}).get("rich_text", [])
40
+ content = TextInlineFormatter.extract_text_with_formatting(rich_text)
41
+
42
+ return f"- {content}"
43
+
44
+ @override
45
+ @staticmethod
46
+ def match_markdown(text: str) -> bool:
47
+ """Check if this element can handle the given markdown text."""
48
+ pattern = re.compile(r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$")
49
+ return bool(pattern.match(text))
50
+
51
+ @override
52
+ @staticmethod
53
+ def match_notion(block: Dict[str, Any]) -> bool:
54
+ """Check if this element can handle the given Notion block."""
55
+ return block.get("type") == "bulleted_list_item"
56
+
57
+ @override
58
+ @classmethod
59
+ def get_llm_prompt_content(cls) -> dict:
60
+ """Returns information for LLM prompts about this element."""
61
+ return {
62
+ "description": "Creates bulleted list items for unordered lists.",
63
+ "when_to_use": "Use for lists where order doesn't matter, such as features, options, or items without hierarchy.",
64
+ "syntax": ["- Item text", "* Item text", "+ Item text"],
65
+ "examples": ["- First item\n- Second item\n- Third item"],
66
+ }
67
+
68
+
69
+ class NumberedListElement:
70
+ """Class for converting between Markdown numbered lists and Notion numbered list items."""
71
+
72
+ @override
73
+ @staticmethod
74
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
75
+ """Convert markdown numbered list item to Notion block."""
76
+ pattern = re.compile(r"^\s*(\d+)\.\s+(.+)$")
77
+ numbered_match = pattern.match(text)
78
+ if not numbered_match:
79
+ return None
80
+
81
+ content = numbered_match.group(2)
82
+
83
+ # Use parse_inline_formatting to handle rich text
84
+ rich_text = TextInlineFormatter.parse_inline_formatting(content)
85
+
86
+ return {
87
+ "type": "numbered_list_item",
88
+ "numbered_list_item": {"rich_text": rich_text, "color": "default"},
89
+ }
90
+
91
+ @override
92
+ @staticmethod
93
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
94
+ """Convert Notion numbered list item block to markdown."""
95
+ if block.get("type") != "numbered_list_item":
96
+ return None
97
+
98
+ rich_text = block.get("numbered_list_item", {}).get("rich_text", [])
99
+ content = TextInlineFormatter.extract_text_with_formatting(rich_text)
100
+
101
+ return f"1. {content}"
102
+
103
+ @override
104
+ @staticmethod
105
+ def match_markdown(text: str) -> bool:
106
+ """Check if this element can handle the given markdown text."""
107
+ pattern = re.compile(r"^\s*\d+\.\s+(.+)$")
108
+ return bool(pattern.match(text))
109
+
110
+ @override
111
+ @staticmethod
112
+ def match_notion(block: Dict[str, Any]) -> bool:
113
+ """Check if this element can handle the given Notion block."""
114
+ return block.get("type") == "numbered_list_item"
115
+
116
+ @override
117
+ @staticmethod
118
+ def is_multiline() -> bool:
119
+ return False
120
+
121
+ @override
122
+ @classmethod
123
+ def get_llm_prompt_content(cls) -> dict:
124
+ """Returns information for LLM prompts about this element."""
125
+ return {
126
+ "description": "Creates numbered list items for ordered sequences.",
127
+ "when_to_use": "Use for lists where order matters, such as steps, rankings, or sequential items.",
128
+ "syntax": ["1. Item text"],
129
+ "examples": ["1. First step\n2. Second step\n3. Third step"],
130
+ }
@@ -0,0 +1,51 @@
1
+ import inspect
2
+ from typing import Dict, Any, Optional
3
+ from abc import ABC
4
+
5
+
6
+ class NotionBlockElement(ABC):
7
+ """Base class for elements that can be converted between Markdown and Notion."""
8
+
9
+ @staticmethod
10
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
11
+ """Convert markdown to Notion block."""
12
+
13
+ @staticmethod
14
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
15
+ """Convert Notion block to markdown."""
16
+
17
+ @staticmethod
18
+ def match_markdown(text: str) -> bool:
19
+ """Check if this element can handle the given markdown text."""
20
+ return bool(NotionBlockElement.markdown_to_notion(text))
21
+
22
+ @staticmethod
23
+ def match_notion(block: Dict[str, Any]) -> bool:
24
+ """Check if this element can handle the given Notion block."""
25
+ return bool(NotionBlockElement.notion_to_markdown(block))
26
+
27
+ @staticmethod
28
+ def is_multiline() -> bool:
29
+ return False
30
+
31
+ @classmethod
32
+ def get_llm_documentation(cls) -> str:
33
+ """
34
+ Returns documentation specifically formatted for LLM system prompts.
35
+ Can be overridden by subclasses to provide custom LLM-friendly documentation.
36
+
37
+ By default, returns the class docstring.
38
+ """
39
+
40
+
41
+ @classmethod
42
+ def get_llm_prompt_content(cls) -> dict:
43
+ """
44
+ Returns a dictionary with information for LLM prompts about this element.
45
+ This default implementation extracts information from the class docstring.
46
+ Subclasses should override this method to provide more structured information.
47
+
48
+ Returns:
49
+ Dictionary with documentation information
50
+ """
51
+ return {"description": inspect.cleandoc(cls.__doc__ or ""), "examples": []}
@@ -0,0 +1,73 @@
1
+ from typing import Dict, Any, Optional
2
+ from typing_extensions import override
3
+
4
+ from notionary.core.converters.elements.notion_block_element import NotionBlockElement
5
+ from notionary.core.converters.elements.text_inline_formatter import TextInlineFormatter
6
+
7
+
8
+ class ParagraphElement(NotionBlockElement):
9
+ """Handles conversion between Markdown paragraphs and Notion paragraph blocks."""
10
+
11
+ @override
12
+ @staticmethod
13
+ def match_markdown(text: str) -> bool:
14
+ """
15
+ Check if text is a markdown paragraph.
16
+ Paragraphs are essentially any text that isn't matched by other block elements.
17
+ Since paragraphs are the fallback element, this always returns True.
18
+ """
19
+ return True
20
+
21
+ @override
22
+ @staticmethod
23
+ def match_notion(block: Dict[str, Any]) -> bool:
24
+ """Check if block is a Notion paragraph."""
25
+ return block.get("type") == "paragraph"
26
+
27
+ @override
28
+ @staticmethod
29
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
30
+ """Convert markdown paragraph to Notion paragraph block."""
31
+ if not text.strip():
32
+ return None
33
+
34
+ return {
35
+ "type": "paragraph",
36
+ "paragraph": {
37
+ "rich_text": TextInlineFormatter.parse_inline_formatting(text)
38
+ },
39
+ }
40
+
41
+ @override
42
+ @staticmethod
43
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
44
+ """Convert Notion paragraph block to markdown paragraph."""
45
+ if block.get("type") != "paragraph":
46
+ return None
47
+
48
+ paragraph_data = block.get("paragraph", {})
49
+ rich_text = paragraph_data.get("rich_text", [])
50
+
51
+ text = TextInlineFormatter.extract_text_with_formatting(rich_text)
52
+ return text if text else None
53
+
54
+ @override
55
+ @staticmethod
56
+ def is_multiline() -> bool:
57
+ return False
58
+
59
+ @classmethod
60
+ def get_llm_prompt_content(cls) -> dict:
61
+ """Returns information for LLM prompts about this element."""
62
+ return {
63
+ "description": "Creates standard paragraph blocks for regular text content.",
64
+ "when_to_use": "Use paragraphs for normal text content. Paragraphs are the default block type and will be used when no other specific formatting is applied.",
65
+ "syntax": ["Just write text normally without any special prefix"],
66
+ "notes": [
67
+ "Paragraphs support inline formatting like **bold**, *italic*, ~~strikethrough~~, `code`, and [links](url)"
68
+ ],
69
+ "examples": [
70
+ "This is a simple paragraph with plain text.",
71
+ "This paragraph has **bold** and *italic* formatting.",
72
+ ],
73
+ }
@@ -0,0 +1,242 @@
1
+ import re
2
+ from typing import Dict, Any, Optional, List, Tuple
3
+ from typing_extensions import override
4
+
5
+ from notionary.core.converters.elements.notion_block_element import NotionBlockElement
6
+
7
+
8
+ class QuoteElement(NotionBlockElement):
9
+ """Class for converting between Markdown blockquotes and Notion quote blocks with background color support."""
10
+
11
+ # Mapping von Markdown-Farbnamen zu Notion-Farbnamen
12
+ COLOR_MAPPING = {
13
+ "gray": "gray_background",
14
+ "brown": "brown_background",
15
+ "orange": "orange_background",
16
+ "yellow": "yellow_background",
17
+ "green": "green_background",
18
+ "blue": "blue_background",
19
+ "purple": "purple_background",
20
+ "pink": "pink_background",
21
+ "red": "red_background",
22
+ }
23
+
24
+ # Umgekehrtes Mapping für die Rückkonvertierung
25
+ REVERSE_COLOR_MAPPING = {v: k for k, v in COLOR_MAPPING.items()}
26
+
27
+ @staticmethod
28
+ def find_matches(text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
29
+ """
30
+ Find all blockquote matches in the text and return their positions and blocks.
31
+
32
+ Args:
33
+ text: The input markdown text
34
+
35
+ Returns:
36
+ List of tuples (start_pos, end_pos, block)
37
+ """
38
+ quote_pattern = re.compile(r"^\s*>\s?(.*)", re.MULTILINE)
39
+ matches = []
40
+
41
+ # Find all potential quote line matches
42
+ quote_matches = list(quote_pattern.finditer(text))
43
+ if not quote_matches:
44
+ return []
45
+
46
+ # Group consecutive quote lines
47
+ i = 0
48
+ while i < len(quote_matches):
49
+ start_match = quote_matches[i]
50
+ start_pos = start_match.start()
51
+
52
+ # Find consecutive quote lines
53
+ j = i + 1
54
+ while j < len(quote_matches):
55
+ # Check if this is the next line (considering newlines)
56
+ if (
57
+ text[quote_matches[j - 1].end() : quote_matches[j].start()].count(
58
+ "\n"
59
+ )
60
+ == 1
61
+ or
62
+ # Or if it's an empty line followed by a quote line
63
+ (
64
+ text[
65
+ quote_matches[j - 1].end() : quote_matches[j].start()
66
+ ].strip()
67
+ == ""
68
+ and text[
69
+ quote_matches[j - 1].end() : quote_matches[j].start()
70
+ ].count("\n")
71
+ <= 2
72
+ )
73
+ ):
74
+ j += 1
75
+ else:
76
+ break
77
+
78
+ end_pos = quote_matches[j - 1].end()
79
+ quote_text = text[start_pos:end_pos]
80
+
81
+ # Create the block
82
+ block = QuoteElement.markdown_to_notion(quote_text)
83
+ if block:
84
+ matches.append((start_pos, end_pos, block))
85
+
86
+ i = j
87
+
88
+ return matches
89
+
90
+ @override
91
+ @staticmethod
92
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
93
+ """Convert markdown blockquote to Notion block with background color support."""
94
+ if not text:
95
+ return None
96
+
97
+ quote_pattern = re.compile(r"^\s*>\s?(.*)", re.MULTILINE)
98
+
99
+ # Check if it's a blockquote
100
+ if not quote_pattern.search(text):
101
+ return None
102
+
103
+ # Extract quote content
104
+ lines = text.split("\n")
105
+ quote_lines = []
106
+ color = "default" # Standardfarbe
107
+
108
+ # Überprüfen, ob der erste Nicht-Leerzeichen-Inhalt eine Farbangabe ist
109
+ first_line = None
110
+ for line in lines:
111
+ quote_match = quote_pattern.match(line)
112
+ if quote_match and quote_match.group(1).strip():
113
+ first_line = quote_match.group(1).strip()
114
+ break
115
+
116
+ # Farbangabe in eckigen Klammern prüfen
117
+ if first_line:
118
+ color_match = re.match(r"^\[background:(\w+)\]\s*(.*)", first_line)
119
+ if color_match:
120
+ potential_color = color_match.group(1).lower()
121
+ if potential_color in QuoteElement.COLOR_MAPPING:
122
+ color = QuoteElement.COLOR_MAPPING[potential_color]
123
+ # Erste Zeile ohne Farbangabe neu hinzufügen
124
+ first_line = color_match.group(2)
125
+
126
+ # Inhalte extrahieren
127
+ processing_first_color_line = True
128
+ for line in lines:
129
+ quote_match = quote_pattern.match(line)
130
+ if quote_match:
131
+ content = quote_match.group(1)
132
+ # Farbangabe in der ersten Zeile entfernen
133
+ if (
134
+ processing_first_color_line
135
+ and content.strip()
136
+ and re.match(r"^\[background:(\w+)\]", content.strip())
137
+ ):
138
+ content = re.sub(r"^\[background:(\w+)\]\s*", "", content)
139
+ processing_first_color_line = False
140
+ quote_lines.append(content)
141
+ elif not line.strip() and quote_lines:
142
+ # Allow empty lines within the quote
143
+ quote_lines.append("")
144
+
145
+ if not quote_lines:
146
+ return None
147
+
148
+ quote_content = "\n".join(quote_lines).strip()
149
+
150
+ # Create rich_text elements directly
151
+ rich_text = [{"type": "text", "text": {"content": quote_content}}]
152
+
153
+ return {"type": "quote", "quote": {"rich_text": rich_text, "color": color}}
154
+
155
+ @override
156
+ @staticmethod
157
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
158
+ """Convert Notion quote block to markdown with background color support."""
159
+ if block.get("type") != "quote":
160
+ return None
161
+
162
+ rich_text = block.get("quote", {}).get("rich_text", [])
163
+ color = block.get("quote", {}).get("color", "default")
164
+
165
+ # Extract the text content
166
+ content = QuoteElement._extract_text_content(rich_text)
167
+
168
+ # Format as markdown blockquote
169
+ lines = content.split("\n")
170
+ formatted_lines = []
171
+
172
+ # Füge die Farbinformation zur ersten Zeile hinzu, falls nicht default
173
+ if color != "default" and color in QuoteElement.REVERSE_COLOR_MAPPING:
174
+ markdown_color = QuoteElement.REVERSE_COLOR_MAPPING.get(color)
175
+ first_line = lines[0] if lines else ""
176
+ formatted_lines.append(f"> [background:{markdown_color}] {first_line}")
177
+ lines = lines[1:] if len(lines) > 1 else []
178
+
179
+ # Füge die restlichen Zeilen hinzu
180
+ for line in lines:
181
+ formatted_lines.append(f"> {line}")
182
+
183
+ return "\n".join(formatted_lines)
184
+
185
+ @override
186
+ @staticmethod
187
+ def match_markdown(text: str) -> bool:
188
+ """Check if this element can handle the given markdown text."""
189
+ quote_pattern = re.compile(r"^\s*>\s?(.*)", re.MULTILINE)
190
+ return bool(quote_pattern.search(text))
191
+
192
+ @override
193
+ @staticmethod
194
+ def match_notion(block: Dict[str, Any]) -> bool:
195
+ """Check if this element can handle the given Notion block."""
196
+ return block.get("type") == "quote"
197
+
198
+ @override
199
+ @staticmethod
200
+ def is_multiline() -> bool:
201
+ """Blockquotes can span multiple lines."""
202
+ return True
203
+
204
+ @staticmethod
205
+ def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
206
+ """Extract plain text content from Notion rich_text elements."""
207
+ result = ""
208
+ for text_obj in rich_text:
209
+ if text_obj.get("type") == "text":
210
+ result += text_obj.get("text", {}).get("content", "")
211
+ elif "plain_text" in text_obj:
212
+ result += text_obj.get("plain_text", "")
213
+ return result
214
+
215
+ @override
216
+ @classmethod
217
+ def get_llm_prompt_content(cls) -> dict:
218
+ """Returns information for LLM prompts about this element."""
219
+ return {
220
+ "description": "Creates blockquotes that visually distinguish quoted text with optional background colors.",
221
+ "when_to_use": "Use blockquotes for quoting external sources, highlighting important statements, or creating visual emphasis for key information.",
222
+ "syntax": [
223
+ "> Text - Simple blockquote",
224
+ "> [background:color] Text - Blockquote with colored background",
225
+ ],
226
+ "color_options": [
227
+ "gray",
228
+ "brown",
229
+ "orange",
230
+ "yellow",
231
+ "green",
232
+ "blue",
233
+ "purple",
234
+ "pink",
235
+ "red",
236
+ ],
237
+ "examples": [
238
+ "> This is a simple blockquote without any color",
239
+ "> [background:blue] This is a blockquote with blue background",
240
+ "> Multi-line quotes\n> continue like this\n> across several lines",
241
+ ],
242
+ }