notionary 0.1.1__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 (51) hide show
  1. notionary/__init__.py +9 -0
  2. notionary/core/__init__.py +0 -0
  3. notionary/core/converters/__init__.py +50 -0
  4. notionary/core/converters/elements/__init__.py +0 -0
  5. notionary/core/converters/elements/bookmark_element.py +224 -0
  6. notionary/core/converters/elements/callout_element.py +179 -0
  7. notionary/core/converters/elements/code_block_element.py +153 -0
  8. notionary/core/converters/elements/column_element.py +294 -0
  9. notionary/core/converters/elements/divider_element.py +73 -0
  10. notionary/core/converters/elements/heading_element.py +84 -0
  11. notionary/core/converters/elements/image_element.py +130 -0
  12. notionary/core/converters/elements/list_element.py +130 -0
  13. notionary/core/converters/elements/notion_block_element.py +51 -0
  14. notionary/core/converters/elements/paragraph_element.py +73 -0
  15. notionary/core/converters/elements/qoute_element.py +242 -0
  16. notionary/core/converters/elements/table_element.py +306 -0
  17. notionary/core/converters/elements/text_inline_formatter.py +294 -0
  18. notionary/core/converters/elements/todo_lists.py +114 -0
  19. notionary/core/converters/elements/toggle_element.py +205 -0
  20. notionary/core/converters/elements/video_element.py +159 -0
  21. notionary/core/converters/markdown_to_notion_converter.py +482 -0
  22. notionary/core/converters/notion_to_markdown_converter.py +45 -0
  23. notionary/core/converters/registry/__init__.py +0 -0
  24. notionary/core/converters/registry/block_element_registry.py +234 -0
  25. notionary/core/converters/registry/block_element_registry_builder.py +280 -0
  26. notionary/core/database/database_info_service.py +43 -0
  27. notionary/core/database/database_query_service.py +73 -0
  28. notionary/core/database/database_schema_service.py +57 -0
  29. notionary/core/database/models/page_result.py +10 -0
  30. notionary/core/database/notion_database_manager.py +332 -0
  31. notionary/core/database/notion_database_manager_factory.py +233 -0
  32. notionary/core/database/notion_database_schema.py +415 -0
  33. notionary/core/database/notion_database_writer.py +390 -0
  34. notionary/core/database/page_service.py +161 -0
  35. notionary/core/notion_client.py +134 -0
  36. notionary/core/page/meta_data/metadata_editor.py +37 -0
  37. notionary/core/page/notion_page_manager.py +110 -0
  38. notionary/core/page/page_content_manager.py +85 -0
  39. notionary/core/page/property_formatter.py +97 -0
  40. notionary/exceptions/database_exceptions.py +76 -0
  41. notionary/exceptions/page_creation_exception.py +9 -0
  42. notionary/util/logging_mixin.py +47 -0
  43. notionary/util/singleton_decorator.py +20 -0
  44. notionary/util/uuid_utils.py +24 -0
  45. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/METADATA +1 -1
  46. notionary-0.1.3.dist-info/RECORD +49 -0
  47. notionary-0.1.3.dist-info/top_level.txt +1 -0
  48. notionary-0.1.1.dist-info/RECORD +0 -5
  49. notionary-0.1.1.dist-info/top_level.txt +0 -1
  50. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/WHEEL +0 -0
  51. {notionary-0.1.1.dist-info → notionary-0.1.3.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,114 @@
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 TodoElement(NotionBlockElement):
9
+ """
10
+ Handles conversion between Markdown todo items and Notion to_do blocks.
11
+
12
+ Markdown syntax examples:
13
+ - [ ] Unchecked todo item
14
+ - [x] Checked todo item
15
+ * [ ] Also works with asterisk
16
+ + [ ] Also works with plus sign
17
+ """
18
+
19
+ # Patterns for detecting Markdown todo items
20
+ TODO_PATTERN = re.compile(r"^\s*[-*+]\s+\[\s?\]\s+(.+)$")
21
+ DONE_PATTERN = re.compile(r"^\s*[-*+]\s+\[x\]\s+(.+)$")
22
+
23
+ @override
24
+ @staticmethod
25
+ def match_markdown(text: str) -> bool:
26
+ """Check if text is a markdown todo item."""
27
+ return bool(
28
+ TodoElement.TODO_PATTERN.match(text) or TodoElement.DONE_PATTERN.match(text)
29
+ )
30
+
31
+ @override
32
+ @staticmethod
33
+ def match_notion(block: Dict[str, Any]) -> bool:
34
+ """Check if block is a Notion to_do block."""
35
+ return block.get("type") == "to_do"
36
+
37
+ @override
38
+ @staticmethod
39
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
40
+ """Convert markdown todo item to Notion to_do block."""
41
+ done_match = TodoElement.DONE_PATTERN.match(text)
42
+ if done_match:
43
+ content = done_match.group(1)
44
+ return TodoElement._create_todo_block(content, True)
45
+
46
+ todo_match = TodoElement.TODO_PATTERN.match(text)
47
+ if todo_match:
48
+ content = todo_match.group(1)
49
+ return TodoElement._create_todo_block(content, False)
50
+
51
+ return None
52
+
53
+ @override
54
+ @staticmethod
55
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
56
+ """Convert Notion to_do block to markdown todo item."""
57
+ if block.get("type") != "to_do":
58
+ return None
59
+
60
+ todo_data = block.get("to_do", {})
61
+ checked = todo_data.get("checked", False)
62
+
63
+ # Extract text content
64
+ rich_text = todo_data.get("rich_text", [])
65
+ content = TextInlineFormatter.extract_text_with_formatting(rich_text)
66
+
67
+ # Format as markdown todo item
68
+ checkbox = "[x]" if checked else "[ ]"
69
+ return f"- {checkbox} {content}"
70
+
71
+ @staticmethod
72
+ def _create_todo_block(content: str, checked: bool) -> Dict[str, Any]:
73
+ """
74
+ Create a Notion to_do block.
75
+
76
+ Args:
77
+ content: The text content of the todo item
78
+ checked: Whether the todo item is checked
79
+
80
+ Returns:
81
+ Notion to_do block dictionary
82
+ """
83
+ return {
84
+ "type": "to_do",
85
+ "to_do": {
86
+ "rich_text": TextInlineFormatter.parse_inline_formatting(content),
87
+ "checked": checked,
88
+ "color": "default",
89
+ },
90
+ }
91
+
92
+ @override
93
+ @staticmethod
94
+ def is_multiline() -> bool:
95
+ return False
96
+
97
+ @classmethod
98
+ def get_llm_prompt_content(cls) -> dict:
99
+ """Returns information for LLM prompts about this element."""
100
+ return {
101
+ "description": "Creates interactive to-do items with checkboxes that can be marked as complete.",
102
+ "when_to_use": "Use to-do items for task lists, checklists, or tracking progress on items that need to be completed. Todo items are interactive in Notion and can be checked/unchecked directly.",
103
+ "syntax": ["- [ ] Unchecked to-do item", "- [x] Checked to-do item"],
104
+ "notes": [
105
+ "Can use any list indicator (-, *, +) before the checkbox",
106
+ "Space in brackets [ ] indicates unchecked status",
107
+ "x in brackets [x] indicates checked status",
108
+ "To-do items support inline formatting like **bold** and *italic*",
109
+ ],
110
+ "examples": [
111
+ "- [ ] Draft project proposal\n- [ ] Schedule kickoff meeting\n- [x] Create initial timeline",
112
+ "* [ ] Review code changes\n* [x] Write documentation\n* [ ] Deploy to production",
113
+ ],
114
+ }
@@ -0,0 +1,205 @@
1
+ import re
2
+ from typing import Dict, Any, Optional, List, Tuple, Callable
3
+
4
+ from notionary.core.converters.elements.notion_block_element import NotionBlockElement
5
+
6
+
7
+ class ToggleElement(NotionBlockElement):
8
+ """
9
+ Handles conversion between Markdown toggle blocks and Notion toggle blocks.
10
+
11
+ Markdown toggle syntax:
12
+ +++ Toggle title
13
+ Indented content that belongs to the toggle
14
+ More indented content
15
+
16
+ Non-indented content marks the end of the toggle block.
17
+ """
18
+
19
+ TOGGLE_PATTERN = re.compile(r"^[+]{3}\s+(.+)$")
20
+
21
+ INDENT_PATTERN = re.compile(r"^(\s{2,}|\t+)(.+)$")
22
+
23
+ @staticmethod
24
+ def match_markdown(text: str) -> bool:
25
+ """Check if text is a markdown toggle."""
26
+ return bool(ToggleElement.TOGGLE_PATTERN.match(text.strip()))
27
+
28
+ @staticmethod
29
+ def match_notion(block: Dict[str, Any]) -> bool:
30
+ """Check if block is a Notion toggle."""
31
+ return block.get("type") == "toggle"
32
+
33
+ @staticmethod
34
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
35
+ """Convert markdown toggle to Notion toggle block.
36
+
37
+ Note: This method only converts the toggle title line.
38
+ The nested content needs to be processed separately.
39
+ """
40
+ toggle_match = ToggleElement.TOGGLE_PATTERN.match(text.strip())
41
+ if not toggle_match:
42
+ return None
43
+
44
+ # Extract content
45
+ title = toggle_match.group(1)
46
+
47
+ return {
48
+ "type": "toggle",
49
+ "toggle": {
50
+ "rich_text": [{"type": "text", "text": {"content": title}}],
51
+ "color": "default",
52
+ "children": [], # Will be populated with nested content
53
+ },
54
+ }
55
+
56
+ @staticmethod
57
+ def extract_nested_content(
58
+ lines: List[str], start_index: int
59
+ ) -> Tuple[List[str], int]:
60
+ """
61
+ Extract the nested content of a toggle element.
62
+
63
+ Args:
64
+ lines: All lines of text
65
+ start_index: Starting index to look for nested content
66
+
67
+ Returns:
68
+ Tuple of (nested_content_lines, next_line_index)
69
+ """
70
+ nested_content = []
71
+ current_index = start_index
72
+
73
+ while current_index < len(lines):
74
+ line = lines[current_index]
75
+
76
+ # Empty line is still part of toggle content
77
+ if not line.strip():
78
+ nested_content.append("")
79
+ current_index += 1
80
+ continue
81
+
82
+ # Check if line is indented (part of toggle content)
83
+ if line.startswith(" ") or line.startswith("\t"):
84
+ # Extract content with indentation removed
85
+ content_line = ToggleElement._remove_indentation(line)
86
+ nested_content.append(content_line)
87
+ current_index += 1
88
+ continue
89
+
90
+ # Non-indented, non-empty line marks the end of toggle content
91
+ break
92
+
93
+ return nested_content, current_index
94
+
95
+ @staticmethod
96
+ def _remove_indentation(line: str) -> str:
97
+ """Remove indentation from a line, handling both spaces and tabs."""
98
+ if line.startswith("\t"):
99
+ return line[1:]
100
+ else:
101
+ # Find number of leading spaces
102
+ leading_spaces = len(line) - len(line.lstrip(" "))
103
+ # Remove at least 2 spaces, but not more than what's there
104
+ return line[min(2, leading_spaces) :]
105
+
106
+ @staticmethod
107
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
108
+ """Convert Notion toggle block to markdown toggle."""
109
+ if block.get("type") != "toggle":
110
+ return None
111
+
112
+ toggle_data = block.get("toggle", {})
113
+
114
+ # Extract title from rich_text
115
+ title = ToggleElement._extract_text_content(toggle_data.get("rich_text", []))
116
+
117
+ # Create the toggle line
118
+ toggle_line = f"+++ {title}"
119
+
120
+ # Process children if any
121
+ children = toggle_data.get("children", [])
122
+ if children:
123
+ child_lines = []
124
+ for child_block in children:
125
+ # This would need to be handled by a full converter that can dispatch
126
+ # to the appropriate element type for each child block
127
+ child_markdown = " [Nested content]" # Placeholder
128
+ child_lines.append(f" {child_markdown}")
129
+
130
+ return toggle_line + "\n" + "\n".join(child_lines)
131
+
132
+ return toggle_line
133
+
134
+ @staticmethod
135
+ def is_multiline() -> bool:
136
+ """Toggle blocks can span multiple lines due to their nested content."""
137
+ return True
138
+
139
+ @staticmethod
140
+ def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
141
+ """Extract plain text content from Notion rich_text elements."""
142
+ result = ""
143
+ for text_obj in rich_text:
144
+ if text_obj.get("type") == "text":
145
+ result += text_obj.get("text", {}).get("content", "")
146
+ elif "plain_text" in text_obj:
147
+ result += text_obj.get("plain_text", "")
148
+ return result
149
+
150
+ @classmethod
151
+ def find_matches(
152
+ cls, text: str, process_nested_content: Callable = None
153
+ ) -> List[Tuple[int, int, Dict[str, Any]]]:
154
+ """
155
+ Find all toggle elements in the text and process them.
156
+
157
+ Args:
158
+ text: The text to search in
159
+ process_nested_content: Optional callback function to process nested content
160
+ It should accept a string and return a list of Notion blocks
161
+
162
+ Returns:
163
+ List of (start_pos, end_pos, block) tuples
164
+ """
165
+ if not text:
166
+ return []
167
+
168
+ toggle_blocks = []
169
+ lines = text.split("\n")
170
+
171
+ i = 0
172
+ while i < len(lines):
173
+ line = lines[i]
174
+
175
+ # Check if line is a toggle
176
+ if not cls.match_markdown(line):
177
+ i += 1
178
+ continue
179
+
180
+ start_pos = 0
181
+ for j in range(i):
182
+ start_pos += len(lines[j]) + 1
183
+
184
+ toggle_block = cls.markdown_to_notion(line)
185
+ if not toggle_block:
186
+ i += 1
187
+ continue
188
+
189
+ # Extract nested content
190
+ nested_content, next_index = cls.extract_nested_content(lines, i + 1)
191
+
192
+ # Calculate ending position
193
+ end_pos = start_pos + len(line) + sum(len(l) + 1 for l in nested_content)
194
+
195
+ if nested_content and process_nested_content:
196
+ nested_text = "\n".join(nested_content)
197
+ nested_blocks = process_nested_content(nested_text)
198
+ if nested_blocks:
199
+ toggle_block["toggle"]["children"] = nested_blocks
200
+
201
+ toggle_blocks.append((start_pos, end_pos, toggle_block))
202
+
203
+ i = next_index
204
+
205
+ return toggle_blocks
@@ -0,0 +1,159 @@
1
+ import re
2
+ from typing import Dict, Any, Optional, List
3
+ from notionary.core.converters.elements.notion_block_element import NotionBlockElement
4
+
5
+
6
+ class VideoElement(NotionBlockElement):
7
+ """
8
+ Handles conversion between Markdown video embeds and Notion video blocks.
9
+
10
+ Markdown video syntax (custom format since standard Markdown doesn't support videos):
11
+ - @[Caption](https://example.com/video.mp4) - Basic video with caption
12
+ - @[](https://example.com/video.mp4) - Video without caption
13
+ - @[Caption](https://www.youtube.com/watch?v=dQw4w9WgXcQ) - YouTube video
14
+ - @[Caption](https://youtu.be/dQw4w9WgXcQ) - YouTube shortened URL
15
+
16
+ Supports various video URLs including YouTube, Vimeo, and direct video file links.
17
+ """
18
+
19
+ # Regex pattern for video syntax
20
+ PATTERN = re.compile(
21
+ r"^\@\[(.*?)\]" # @[Caption] part
22
+ + r'\((https?://[^\s"]+)' # (URL part
23
+ + r"\)$" # closing parenthesis
24
+ )
25
+
26
+ # YouTube specific patterns
27
+ YOUTUBE_PATTERNS = [
28
+ re.compile(
29
+ r"(?:https?://)?(?:www\.)?youtube\.com/watch\?v=([a-zA-Z0-9_-]{11})"
30
+ ),
31
+ re.compile(r"(?:https?://)?(?:www\.)?youtu\.be/([a-zA-Z0-9_-]{11})"),
32
+ ]
33
+
34
+ @staticmethod
35
+ def match_markdown(text: str) -> bool:
36
+ """Check if text is a markdown video embed."""
37
+ text = text.strip()
38
+ return text.startswith("@[") and bool(VideoElement.PATTERN.match(text))
39
+
40
+ @staticmethod
41
+ def match_notion(block: Dict[str, Any]) -> bool:
42
+ """Check if block is a Notion video."""
43
+ return block.get("type") == "video"
44
+
45
+ @staticmethod
46
+ def is_youtube_url(url: str) -> bool:
47
+ """Check if URL is a YouTube video and return video ID if it is."""
48
+ for pattern in VideoElement.YOUTUBE_PATTERNS:
49
+ match = pattern.match(url)
50
+ if match:
51
+ return True
52
+ return False
53
+
54
+ @staticmethod
55
+ def get_youtube_id(url: str) -> Optional[str]:
56
+ """Extract YouTube video ID from URL."""
57
+ for pattern in VideoElement.YOUTUBE_PATTERNS:
58
+ match = pattern.match(url)
59
+ if match:
60
+ return match.group(1)
61
+ return None
62
+
63
+ @staticmethod
64
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
65
+ """Convert markdown video embed to Notion video block."""
66
+ video_match = VideoElement.PATTERN.match(text.strip())
67
+ if not video_match:
68
+ return None
69
+
70
+ caption = video_match.group(1)
71
+ url = video_match.group(2)
72
+
73
+ if not url:
74
+ return None
75
+
76
+ # For YouTube videos, ensure we use the full embed URL
77
+ youtube_id = VideoElement.get_youtube_id(url)
78
+ if youtube_id:
79
+ url = f"https://www.youtube.com/watch?v={youtube_id}"
80
+
81
+ # Prepare the video block
82
+ video_block = {
83
+ "type": "video",
84
+ "video": {"type": "external", "external": {"url": url}},
85
+ }
86
+
87
+ # Add caption if provided
88
+ if caption:
89
+ video_block["video"]["caption"] = [
90
+ {"type": "text", "text": {"content": caption}}
91
+ ]
92
+
93
+ return video_block
94
+
95
+ @staticmethod
96
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
97
+ """Convert Notion video block to markdown video embed."""
98
+ if block.get("type") != "video":
99
+ return None
100
+
101
+ video_data = block.get("video", {})
102
+
103
+ # Handle both external and file (uploaded) videos
104
+ if video_data.get("type") == "external":
105
+ url = video_data.get("external", {}).get("url", "")
106
+ elif video_data.get("type") == "file":
107
+ url = video_data.get("file", {}).get("url", "")
108
+ else:
109
+ return None
110
+
111
+ if not url:
112
+ return None
113
+
114
+ # Extract caption if available
115
+ caption = ""
116
+ caption_rich_text = video_data.get("caption", [])
117
+ if caption_rich_text:
118
+ caption = VideoElement._extract_text_content(caption_rich_text)
119
+
120
+ return f"@[{caption}]({url})"
121
+
122
+ @staticmethod
123
+ def is_multiline() -> bool:
124
+ """Videos are single-line elements."""
125
+ return False
126
+
127
+ @staticmethod
128
+ def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
129
+ """Extract plain text content from Notion rich_text elements."""
130
+ result = ""
131
+ for text_obj in rich_text:
132
+ if text_obj.get("type") == "text":
133
+ result += text_obj.get("text", {}).get("content", "")
134
+ elif "plain_text" in text_obj:
135
+ result += text_obj.get("plain_text", "")
136
+ return result
137
+
138
+ @classmethod
139
+ def get_llm_prompt_content(cls) -> dict:
140
+ """Returns information for LLM prompts about this element."""
141
+ return {
142
+ "description": "Embeds video content from external sources like YouTube or direct video URLs.",
143
+ "when_to_use": "Use video embeds when you want to include multimedia content directly in your document. Videos are useful for tutorials, demonstrations, presentations, or any content that benefits from visual explanation.",
144
+ "syntax": [
145
+ "@[](https://example.com/video.mp4) - Video without caption",
146
+ "@[Caption text](https://example.com/video.mp4) - Video with caption",
147
+ ],
148
+ "supported_sources": [
149
+ "YouTube videos (https://youtube.com/watch?v=ID or https://youtu.be/ID)",
150
+ "Vimeo videos",
151
+ "Direct links to video files (.mp4, .mov, etc.)",
152
+ "Other video hosting platforms supported by Notion",
153
+ ],
154
+ "examples": [
155
+ "@[How to use this feature](https://www.youtube.com/watch?v=dQw4w9WgXcQ)",
156
+ "@[Product demo](https://example.com/videos/demo.mp4)",
157
+ "@[](https://youtu.be/dQw4w9WgXcQ)",
158
+ ],
159
+ }