notionary 0.2.19__py3-none-any.whl → 0.2.22__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 (220) hide show
  1. notionary/__init__.py +8 -4
  2. notionary/base_notion_client.py +3 -1
  3. notionary/blocks/__init__.py +2 -91
  4. notionary/blocks/_bootstrap.py +271 -0
  5. notionary/blocks/audio/__init__.py +8 -2
  6. notionary/blocks/audio/audio_element.py +69 -106
  7. notionary/blocks/audio/audio_markdown_node.py +13 -5
  8. notionary/blocks/audio/audio_models.py +6 -55
  9. notionary/blocks/base_block_element.py +42 -0
  10. notionary/blocks/bookmark/__init__.py +9 -2
  11. notionary/blocks/bookmark/bookmark_element.py +49 -139
  12. notionary/blocks/bookmark/bookmark_markdown_node.py +19 -18
  13. notionary/blocks/bookmark/bookmark_models.py +15 -0
  14. notionary/blocks/breadcrumbs/__init__.py +17 -0
  15. notionary/blocks/breadcrumbs/breadcrumb_element.py +39 -0
  16. notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +32 -0
  17. notionary/blocks/breadcrumbs/breadcrumb_models.py +12 -0
  18. notionary/blocks/bulleted_list/__init__.py +12 -2
  19. notionary/blocks/bulleted_list/bulleted_list_element.py +55 -53
  20. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +2 -1
  21. notionary/blocks/bulleted_list/bulleted_list_models.py +18 -0
  22. notionary/blocks/callout/__init__.py +9 -2
  23. notionary/blocks/callout/callout_element.py +53 -86
  24. notionary/blocks/callout/callout_markdown_node.py +3 -1
  25. notionary/blocks/callout/callout_models.py +33 -0
  26. notionary/blocks/child_database/__init__.py +14 -0
  27. notionary/blocks/child_database/child_database_element.py +61 -0
  28. notionary/blocks/child_database/child_database_models.py +12 -0
  29. notionary/blocks/child_page/__init__.py +9 -0
  30. notionary/blocks/child_page/child_page_element.py +94 -0
  31. notionary/blocks/child_page/child_page_models.py +12 -0
  32. notionary/blocks/{shared/block_client.py → client.py} +54 -54
  33. notionary/blocks/code/__init__.py +6 -2
  34. notionary/blocks/code/code_element.py +96 -181
  35. notionary/blocks/code/code_markdown_node.py +64 -13
  36. notionary/blocks/code/code_models.py +94 -0
  37. notionary/blocks/column/__init__.py +25 -1
  38. notionary/blocks/column/column_element.py +44 -312
  39. notionary/blocks/column/column_list_element.py +52 -0
  40. notionary/blocks/column/column_list_markdown_node.py +50 -0
  41. notionary/blocks/column/column_markdown_node.py +59 -0
  42. notionary/blocks/column/column_models.py +26 -0
  43. notionary/blocks/divider/__init__.py +9 -2
  44. notionary/blocks/divider/divider_element.py +18 -49
  45. notionary/blocks/divider/divider_markdown_node.py +2 -1
  46. notionary/blocks/divider/divider_models.py +12 -0
  47. notionary/blocks/embed/__init__.py +9 -2
  48. notionary/blocks/embed/embed_element.py +65 -111
  49. notionary/blocks/embed/embed_markdown_node.py +3 -1
  50. notionary/blocks/embed/embed_models.py +14 -0
  51. notionary/blocks/equation/__init__.py +14 -0
  52. notionary/blocks/equation/equation_element.py +133 -0
  53. notionary/blocks/equation/equation_element_markdown_node.py +35 -0
  54. notionary/blocks/equation/equation_models.py +11 -0
  55. notionary/blocks/file/__init__.py +25 -0
  56. notionary/blocks/file/file_element.py +112 -0
  57. notionary/blocks/file/file_element_markdown_node.py +37 -0
  58. notionary/blocks/file/file_element_models.py +39 -0
  59. notionary/blocks/guards.py +22 -0
  60. notionary/blocks/heading/__init__.py +16 -2
  61. notionary/blocks/heading/heading_element.py +83 -69
  62. notionary/blocks/heading/heading_markdown_node.py +2 -1
  63. notionary/blocks/heading/heading_models.py +29 -0
  64. notionary/blocks/image_block/__init__.py +13 -0
  65. notionary/blocks/image_block/image_element.py +89 -0
  66. notionary/blocks/{image → image_block}/image_markdown_node.py +13 -6
  67. notionary/blocks/image_block/image_models.py +10 -0
  68. notionary/blocks/mixins/captions/__init__.py +4 -0
  69. notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
  70. notionary/blocks/mixins/captions/caption_mixin.py +92 -0
  71. notionary/blocks/models.py +174 -0
  72. notionary/blocks/numbered_list/__init__.py +12 -2
  73. notionary/blocks/numbered_list/numbered_list_element.py +48 -56
  74. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
  75. notionary/blocks/numbered_list/numbered_list_models.py +17 -0
  76. notionary/blocks/paragraph/__init__.py +12 -2
  77. notionary/blocks/paragraph/paragraph_element.py +40 -66
  78. notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
  79. notionary/blocks/paragraph/paragraph_models.py +16 -0
  80. notionary/blocks/pdf/__init__.py +13 -0
  81. notionary/blocks/pdf/pdf_element.py +97 -0
  82. notionary/blocks/pdf/pdf_markdown_node.py +37 -0
  83. notionary/blocks/pdf/pdf_models.py +11 -0
  84. notionary/blocks/quote/__init__.py +11 -2
  85. notionary/blocks/quote/quote_element.py +45 -62
  86. notionary/blocks/quote/quote_markdown_node.py +6 -3
  87. notionary/blocks/quote/quote_models.py +18 -0
  88. notionary/blocks/registry/__init__.py +4 -0
  89. notionary/blocks/registry/block_registry.py +60 -121
  90. notionary/blocks/registry/block_registry_builder.py +115 -59
  91. notionary/blocks/rich_text/__init__.py +33 -0
  92. notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
  93. notionary/blocks/rich_text/rich_text_models.py +221 -0
  94. notionary/blocks/rich_text/text_inline_formatter.py +456 -0
  95. notionary/blocks/syntax_prompt_builder.py +137 -0
  96. notionary/blocks/table/__init__.py +16 -2
  97. notionary/blocks/table/table_element.py +136 -228
  98. notionary/blocks/table/table_markdown_node.py +2 -1
  99. notionary/blocks/table/table_models.py +28 -0
  100. notionary/blocks/table_of_contents/__init__.py +19 -0
  101. notionary/blocks/table_of_contents/table_of_contents_element.py +68 -0
  102. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
  103. notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
  104. notionary/blocks/todo/__init__.py +9 -2
  105. notionary/blocks/todo/todo_element.py +52 -92
  106. notionary/blocks/todo/todo_markdown_node.py +2 -1
  107. notionary/blocks/todo/todo_models.py +19 -0
  108. notionary/blocks/toggle/__init__.py +13 -3
  109. notionary/blocks/toggle/toggle_element.py +69 -260
  110. notionary/blocks/toggle/toggle_markdown_node.py +25 -15
  111. notionary/blocks/toggle/toggle_models.py +17 -0
  112. notionary/blocks/toggleable_heading/__init__.py +6 -2
  113. notionary/blocks/toggleable_heading/toggleable_heading_element.py +86 -241
  114. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
  115. notionary/blocks/types.py +130 -0
  116. notionary/blocks/video/__init__.py +8 -2
  117. notionary/blocks/video/video_element.py +70 -141
  118. notionary/blocks/video/video_element_models.py +10 -0
  119. notionary/blocks/video/video_markdown_node.py +13 -6
  120. notionary/database/client.py +26 -8
  121. notionary/database/database.py +13 -14
  122. notionary/database/database_filter_builder.py +2 -2
  123. notionary/database/database_provider.py +5 -4
  124. notionary/database/models.py +337 -0
  125. notionary/database/notion_database.py +6 -7
  126. notionary/file_upload/client.py +5 -7
  127. notionary/file_upload/models.py +3 -2
  128. notionary/file_upload/notion_file_upload.py +2 -3
  129. notionary/markdown/markdown_builder.py +729 -0
  130. notionary/markdown/markdown_document_model.py +228 -0
  131. notionary/{blocks → markdown}/markdown_node.py +1 -0
  132. notionary/models/notion_database_response.py +0 -338
  133. notionary/page/client.py +34 -15
  134. notionary/page/models.py +327 -0
  135. notionary/page/notion_page.py +136 -58
  136. notionary/page/{content/page_content_writer.py → page_content_deleting_service.py} +25 -59
  137. notionary/page/page_content_writer.py +177 -0
  138. notionary/page/page_context.py +65 -0
  139. notionary/page/reader/handler/__init__.py +19 -0
  140. notionary/page/reader/handler/base_block_renderer.py +44 -0
  141. notionary/page/reader/handler/block_processing_context.py +35 -0
  142. notionary/page/reader/handler/block_rendering_context.py +48 -0
  143. notionary/page/reader/handler/column_list_renderer.py +51 -0
  144. notionary/page/reader/handler/column_renderer.py +60 -0
  145. notionary/page/reader/handler/line_renderer.py +73 -0
  146. notionary/page/reader/handler/numbered_list_renderer.py +85 -0
  147. notionary/page/reader/handler/toggle_renderer.py +69 -0
  148. notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
  149. notionary/page/reader/page_content_retriever.py +81 -0
  150. notionary/page/search_filter_builder.py +2 -1
  151. notionary/page/writer/handler/__init__.py +24 -0
  152. notionary/page/writer/handler/code_handler.py +72 -0
  153. notionary/page/writer/handler/column_handler.py +141 -0
  154. notionary/page/writer/handler/column_list_handler.py +139 -0
  155. notionary/page/writer/handler/equation_handler.py +74 -0
  156. notionary/page/writer/handler/line_handler.py +35 -0
  157. notionary/page/writer/handler/line_processing_context.py +54 -0
  158. notionary/page/writer/handler/regular_line_handler.py +86 -0
  159. notionary/page/writer/handler/table_handler.py +66 -0
  160. notionary/page/writer/handler/toggle_handler.py +155 -0
  161. notionary/page/writer/handler/toggleable_heading_handler.py +173 -0
  162. notionary/page/writer/markdown_to_notion_converter.py +95 -0
  163. notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
  164. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
  165. notionary/page/writer/notion_text_length_processor.py +150 -0
  166. notionary/telemetry/__init__.py +2 -2
  167. notionary/telemetry/service.py +3 -3
  168. notionary/user/__init__.py +2 -2
  169. notionary/user/base_notion_user.py +2 -1
  170. notionary/user/client.py +2 -3
  171. notionary/user/models.py +1 -0
  172. notionary/user/notion_bot_user.py +4 -5
  173. notionary/user/notion_user.py +3 -4
  174. notionary/user/notion_user_manager.py +23 -95
  175. notionary/util/__init__.py +3 -2
  176. notionary/util/fuzzy.py +2 -1
  177. notionary/util/logging_mixin.py +2 -2
  178. notionary/util/singleton_metaclass.py +1 -1
  179. notionary/workspace.py +6 -5
  180. notionary-0.2.22.dist-info/METADATA +237 -0
  181. notionary-0.2.22.dist-info/RECORD +200 -0
  182. notionary/blocks/document/__init__.py +0 -7
  183. notionary/blocks/document/document_element.py +0 -102
  184. notionary/blocks/document/document_markdown_node.py +0 -31
  185. notionary/blocks/image/__init__.py +0 -7
  186. notionary/blocks/image/image_element.py +0 -151
  187. notionary/blocks/markdown_builder.py +0 -356
  188. notionary/blocks/mention/__init__.py +0 -7
  189. notionary/blocks/mention/mention_element.py +0 -229
  190. notionary/blocks/mention/mention_markdown_node.py +0 -38
  191. notionary/blocks/prompts/element_prompt_builder.py +0 -83
  192. notionary/blocks/prompts/element_prompt_content.py +0 -41
  193. notionary/blocks/shared/models.py +0 -713
  194. notionary/blocks/shared/notion_block_element.py +0 -37
  195. notionary/blocks/shared/text_inline_formatter.py +0 -262
  196. notionary/blocks/shared/text_inline_formatter_new.py +0 -139
  197. notionary/database/models/page_result.py +0 -10
  198. notionary/models/notion_block_response.py +0 -264
  199. notionary/models/notion_page_response.py +0 -78
  200. notionary/models/search_response.py +0 -0
  201. notionary/page/__init__.py +0 -0
  202. notionary/page/content/markdown_whitespace_processor.py +0 -80
  203. notionary/page/content/notion_text_length_utils.py +0 -87
  204. notionary/page/content/page_content_retriever.py +0 -60
  205. notionary/page/formatting/line_processor.py +0 -153
  206. notionary/page/formatting/markdown_to_notion_converter.py +0 -153
  207. notionary/page/markdown_syntax_prompt_generator.py +0 -114
  208. notionary/page/notion_to_markdown_converter.py +0 -179
  209. notionary/page/properites/property_value_extractor.py +0 -0
  210. notionary/user/notion_user_provider.py +0 -1
  211. notionary-0.2.19.dist-info/METADATA +0 -225
  212. notionary-0.2.19.dist-info/RECORD +0 -150
  213. /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
  214. /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
  215. /notionary/{blocks/mention/mention_models.py → page/reader/handler/equation_renderer.py} +0 -0
  216. /notionary/{blocks/shared/__init__.py → page/writer/markdown_to_notion_post_processor.py} +0 -0
  217. /notionary/{blocks/toggleable_heading/toggleable_heading_models.py → page/writer/markdown_to_notion_text_length_post_processor.py} +0 -0
  218. /notionary/{elements/__init__.py → util/concurrency_limiter.py} +0 -0
  219. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
  220. {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/WHEEL +0 -0
@@ -1,37 +0,0 @@
1
- from typing import Optional, Any, TypeAlias, Union
2
- from abc import ABC
3
-
4
- from notionary.blocks.prompts.element_prompt_content import ElementPromptContent
5
-
6
- NotionBlock: TypeAlias = dict[str, Any]
7
- NotionBlockResult: TypeAlias = Optional[Union[list[dict[str, Any]], dict[str, Any]]]
8
-
9
-
10
- class NotionBlockElement(ABC):
11
- """Base class for elements that can be converted between Markdown and Notion."""
12
-
13
- @classmethod
14
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
15
- """Convert markdown to Notion blocks (can return multiple blocks or single block)."""
16
-
17
- @classmethod
18
- def notion_to_markdown(cls, block: dict[str, any]) -> Optional[str]:
19
- """Convert Notion block to markdown."""
20
-
21
- @classmethod
22
- def match_markdown(cls, text: str) -> bool:
23
- """Check if this element can handle the given markdown text."""
24
- return bool(cls.markdown_to_notion(text)) # Now calls the class's version
25
-
26
- @classmethod
27
- def match_notion(cls, block: dict[str, any]) -> bool:
28
- """Check if this element can handle the given Notion block."""
29
- return bool(cls.notion_to_markdown(block)) # Now calls the class's version
30
-
31
- @classmethod
32
- def is_multiline(cls) -> bool:
33
- return False
34
-
35
- @classmethod
36
- def get_llm_prompt_content(cls) -> ElementPromptContent:
37
- """Returns a dictionary with information for LLM prompts about this element."""
@@ -1,262 +0,0 @@
1
- from typing import Any
2
- import re
3
-
4
- from notionary.blocks import ElementPromptBuilder, ElementPromptContent
5
-
6
-
7
- class TextInlineFormatter:
8
- """
9
- Handles conversion between Markdown inline formatting and Notion rich text elements.
10
-
11
- Supports various formatting options:
12
- - Bold: **text**
13
- - Italic: *text* or _text_
14
- - Underline: __text__
15
- - Strikethrough: ~~text~~
16
- - Code: `text`
17
- - Links: [text](url)
18
- """
19
-
20
- # Format patterns for matching Markdown formatting
21
- FORMAT_PATTERNS = [
22
- (r"\*\*(.+?)\*\*", {"bold": True}),
23
- (r"\*(.+?)\*", {"italic": True}),
24
- (r"_(.+?)_", {"italic": True}),
25
- (r"__(.+?)__", {"underline": True}),
26
- (r"~~(.+?)~~", {"strikethrough": True}),
27
- (r"`(.+?)`", {"code": True}),
28
- (r"\[(.+?)\]\((.+?)\)", {"link": True}),
29
- (r"@\[([0-9a-f-]+)\]", {"mention": True}),
30
- ]
31
-
32
- @classmethod
33
- def parse_inline_formatting(cls, text: str) -> list[dict[str, Any]]:
34
- """
35
- Parse inline text formatting into Notion rich_text format.
36
-
37
- Args:
38
- text: Markdown text with inline formatting
39
-
40
- Returns:
41
- list of Notion rich_text objects
42
- """
43
- if not text:
44
- return []
45
-
46
- return cls._split_text_into_segments(text, cls.FORMAT_PATTERNS)
47
-
48
- @classmethod
49
- def _split_text_into_segments(
50
- cls, text: str, format_patterns: list[tuple]
51
- ) -> list[dict[str, Any]]:
52
- """
53
- Split text into segments by formatting markers and convert to Notion rich_text format.
54
-
55
- Args:
56
- text: Text to split
57
- format_patterns: list of (regex pattern, formatting dict) tuples
58
-
59
- Returns:
60
- list of Notion rich_text objects
61
- """
62
- segments = []
63
- remaining_text = text
64
-
65
- while remaining_text:
66
- earliest_match = None
67
- earliest_format = None
68
- earliest_pos = len(remaining_text)
69
-
70
- # Find the earliest formatting marker
71
- for pattern, formatting in format_patterns:
72
- match = re.search(pattern, remaining_text)
73
- if match and match.start() < earliest_pos:
74
- earliest_match = match
75
- earliest_format = formatting
76
- earliest_pos = match.start()
77
-
78
- if earliest_match is None:
79
- if remaining_text:
80
- segments.append(cls._create_text_element(remaining_text, {}))
81
- break
82
-
83
- if earliest_pos > 0:
84
- segments.append(
85
- cls._create_text_element(remaining_text[:earliest_pos], {})
86
- )
87
-
88
- if "link" in earliest_format:
89
- content = earliest_match.group(1)
90
- url = earliest_match.group(2)
91
- segments.append(cls._create_link_element(content, url))
92
-
93
- elif "mention" in earliest_format:
94
- id = earliest_match.group(1)
95
- segments.append(cls._create_mention_element(id))
96
-
97
- else:
98
- content = earliest_match.group(1)
99
- segments.append(cls._create_text_element(content, earliest_format))
100
-
101
- # Move past the processed segment
102
- remaining_text = remaining_text[
103
- earliest_pos + len(earliest_match.group(0)) :
104
- ]
105
-
106
- return segments
107
-
108
- @classmethod
109
- def _create_text_element(
110
- cls, text: str, formatting: dict[str, Any]
111
- ) -> dict[str, Any]:
112
- """
113
- Create a Notion text element with formatting.
114
-
115
- Args:
116
- text: The text content
117
- formatting: Dictionary of formatting options
118
-
119
- Returns:
120
- Notion rich_text element
121
- """
122
- annotations = cls._default_annotations()
123
-
124
- # Apply formatting
125
- for key, value in formatting.items():
126
- if key == "color":
127
- annotations["color"] = value
128
- elif key in annotations:
129
- annotations[key] = value
130
-
131
- return {
132
- "type": "text",
133
- "text": {"content": text},
134
- "annotations": annotations,
135
- "plain_text": text,
136
- }
137
-
138
- @classmethod
139
- def _create_link_element(cls, text: str, url: str) -> dict[str, Any]:
140
- """
141
- Create a Notion link element.
142
-
143
- Args:
144
- text: The link text
145
- url: The URL
146
-
147
- Returns:
148
- Notion rich_text element with link
149
- """
150
- return {
151
- "type": "text",
152
- "text": {"content": text, "link": {"url": url}},
153
- "annotations": cls._default_annotations(),
154
- "plain_text": text,
155
- }
156
-
157
- @classmethod
158
- def _create_mention_element(cls, id: str) -> dict[str, Any]:
159
- """
160
- Create a Notion mention element.
161
-
162
- Args:
163
- id: The page ID
164
-
165
- Returns:
166
- Notion rich_text element with mention
167
- """
168
- return {
169
- "type": "mention",
170
- "mention": {"type": "page", "page": {"id": id}},
171
- "annotations": cls._default_annotations(),
172
- }
173
-
174
- @classmethod
175
- def extract_text_with_formatting(cls, rich_text: list[dict[str, Any]]) -> str:
176
- """
177
- Convert Notion rich_text elements back to Markdown formatted text.
178
-
179
- Args:
180
- rich_text: list of Notion rich_text elements
181
-
182
- Returns:
183
- Markdown formatted text
184
- """
185
- formatted_parts = []
186
-
187
- for text_obj in rich_text:
188
- # Fallback: If plain_text is missing, use text['content']
189
- content = text_obj.get("plain_text")
190
- if content is None:
191
- content = text_obj.get("text", {}).get("content", "")
192
-
193
- annotations = text_obj.get("annotations", {})
194
-
195
- if annotations.get("code", False):
196
- content = f"`{content}`"
197
- if annotations.get("strikethrough", False):
198
- content = f"~~{content}~~"
199
- if annotations.get("underline", False):
200
- content = f"__{content}__"
201
- if annotations.get("italic", False):
202
- content = f"*{content}*"
203
- if annotations.get("bold", False):
204
- content = f"**{content}**"
205
-
206
- text_data = text_obj.get("text", {})
207
- link_data = text_data.get("link")
208
- if link_data:
209
- url = link_data.get("url", "")
210
- content = f"[{content}]({url})"
211
-
212
- formatted_parts.append(content)
213
-
214
- return "".join(formatted_parts)
215
-
216
- @classmethod
217
- def _default_annotations(cls) -> dict[str, bool]:
218
- """
219
- Create default annotations object.
220
-
221
- Returns:
222
- Default Notion text annotations
223
- """
224
- return {
225
- "bold": False,
226
- "italic": False,
227
- "strikethrough": False,
228
- "underline": False,
229
- "code": False,
230
- "color": "default",
231
- }
232
-
233
- @classmethod
234
- def get_llm_prompt_content(cls) -> ElementPromptContent:
235
- """
236
- Returns structured LLM prompt metadata for inline formatting.
237
- """
238
- return (
239
- ElementPromptBuilder()
240
- .with_description(
241
- "Inline formatting can be used within most block types to style your text. You can combine multiple formatting options."
242
- )
243
- .with_usage_guidelines(
244
- "Use inline formatting to highlight important words, provide emphasis, show code or paths, or add hyperlinks. "
245
- "This helps create visual hierarchy and improves readability."
246
- )
247
- .with_syntax(
248
- "**bold**, *italic*, `code`, ~~strikethrough~~, __underline__, [text](url)"
249
- )
250
- .with_examples(
251
- [
252
- "This text has a **bold** word.",
253
- "This text has an *italic* word.",
254
- "This text has `code` formatting.",
255
- "This text has ~~strikethrough~~ formatting.",
256
- "This text has __underlined__ formatting.",
257
- "This has a [hyperlink](https://example.com).",
258
- "You can **combine *different* formatting** styles.",
259
- ]
260
- )
261
- .build()
262
- )
@@ -1,139 +0,0 @@
1
- from typing import Optional
2
- import re
3
-
4
- # TODO: Use this inline formatting here
5
- from notionary.blocks.shared.models import (
6
- MentionRichText,
7
- RichTextObject,
8
- TextAnnotations,
9
- TextContent,
10
- )
11
-
12
- FORMAT_PATTERNS = [
13
- (r"\*\*(.+?)\*\*", {"bold": True}),
14
- (r"\*(.+?)\*", {"italic": True}),
15
- (r"_(.+?)_", {"italic": True}),
16
- (r"__(.+?)__", {"underline": True}),
17
- (r"~~(.+?)~~", {"strikethrough": True}),
18
- (r"`(.+?)`", {"code": True}),
19
- (r"\[(.+?)\]\((.+?)\)", {"link": True}),
20
- (r"@\[([0-9a-f-]+)\]", {"mention": True}),
21
- ]
22
-
23
-
24
- def parse_inline_formatting(text: str) -> list[dict[str, any]]:
25
- """Parse inline text formatting into Notion rich_text format."""
26
- if not text:
27
- return []
28
-
29
- return _split_text_into_segments(text)
30
-
31
-
32
- def _split_text_into_segments(text: str) -> list[dict[str, any]]:
33
- """Split text into segments by formatting markers."""
34
- segments = []
35
- remaining_text = text
36
-
37
- while remaining_text:
38
- match_info = _find_earliest_match(remaining_text)
39
-
40
- # No more formatting found - add remaining text and exit
41
- if not match_info:
42
- segments.append(_create_plain_text(remaining_text))
43
- break
44
-
45
- match, formatting, pos = match_info
46
-
47
- # Add text before match if exists
48
- if pos > 0:
49
- segments.append(_create_plain_text(remaining_text[:pos]))
50
-
51
- # Add formatted segment
52
- segments.append(_create_formatted_segment(match, formatting))
53
-
54
- # Update remaining text
55
- remaining_text = remaining_text[pos + len(match.group(0)) :]
56
-
57
- return segments
58
-
59
-
60
- def _find_earliest_match(text: str) -> Optional[tuple]:
61
- """Find the earliest formatting match in text."""
62
- earliest_match = None
63
- earliest_format = None
64
- earliest_pos = len(text)
65
-
66
- for pattern, formatting in FORMAT_PATTERNS:
67
- match = re.search(pattern, text)
68
- if match and match.start() < earliest_pos:
69
- earliest_match = match
70
- earliest_format = formatting
71
- earliest_pos = match.start()
72
-
73
- return (earliest_match, earliest_format, earliest_pos) if earliest_match else None
74
-
75
-
76
- def _create_formatted_segment(match: re.Match, formatting: dict) -> dict[str, any]:
77
- """Create a formatted segment based on match and formatting."""
78
- if "link" in formatting:
79
- return _create_link_text(match.group(1), match.group(2))
80
- elif "mention" in formatting:
81
- return _create_mention_text(match.group(1))
82
- else:
83
- return _create_formatted_text(match.group(1), **formatting)
84
-
85
-
86
- def _create_plain_text(content: str) -> dict[str, any]:
87
- """Create plain text rich text object."""
88
- return RichTextObject.from_plain_text(content).model_dump()
89
-
90
-
91
- def _create_formatted_text(content: str, **formatting) -> dict[str, any]:
92
- """Create formatted text rich text object."""
93
- return RichTextObject.from_plain_text(content, **formatting).model_dump()
94
-
95
-
96
- def _create_link_text(content: str, url: str) -> dict[str, any]:
97
- """Create link text rich text object."""
98
- text_content = TextContent(content=content, link=url)
99
- annotations = TextAnnotations()
100
-
101
- rich_text = RichTextObject(
102
- text=text_content, annotations=annotations, plain_text=content, href=url
103
- )
104
- return rich_text.model_dump()
105
-
106
-
107
- def _create_mention_text(page_id: str) -> dict[str, any]:
108
- """Create mention rich text object."""
109
- return MentionRichText.from_page_id(page_id).model_dump()
110
-
111
-
112
- def extract_text_with_formatting(rich_text: list[dict[str, any]]) -> str:
113
- """Convert Notion rich_text elements back to Markdown."""
114
- return "".join(_rich_text_to_markdown(item) for item in rich_text)
115
-
116
-
117
- def _rich_text_to_markdown(text_obj: dict[str, any]) -> str:
118
- """Convert single rich text object to markdown."""
119
- content = text_obj.get("plain_text", text_obj.get("text", {}).get("content", ""))
120
- annotations = text_obj.get("annotations", {})
121
-
122
- # Apply formatting in reverse order
123
- if annotations.get("code", False):
124
- content = f"`{content}`"
125
- if annotations.get("strikethrough", False):
126
- content = f"~~{content}~~"
127
- if annotations.get("underline", False):
128
- content = f"__{content}__"
129
- if annotations.get("italic", False):
130
- content = f"*{content}*"
131
- if annotations.get("bold", False):
132
- content = f"**{content}**"
133
-
134
- # Handle links
135
- link_data = text_obj.get("text", {}).get("link")
136
- if link_data and link_data.get("url"):
137
- content = f"[{content}]({link_data['url']})"
138
-
139
- return content
@@ -1,10 +0,0 @@
1
- from typing import Optional, TypedDict
2
-
3
-
4
- class PageResult(TypedDict, total=False):
5
- """Type definition for page operation results."""
6
-
7
- success: bool
8
- page_id: str
9
- url: Optional[str]
10
- message: Optional[str]
@@ -1,264 +0,0 @@
1
- from typing import List, Optional, Union, Literal
2
- from pydantic import BaseModel
3
-
4
-
5
- # Rich Text Komponenten
6
- class TextContent(BaseModel):
7
- content: str
8
- link: Optional[dict] = None
9
-
10
-
11
- class Annotations(BaseModel):
12
- bold: bool
13
- italic: bool
14
- strikethrough: bool
15
- underline: bool
16
- code: bool
17
- color: str
18
-
19
-
20
- class RichText(BaseModel):
21
- type: Literal["text"]
22
- text: TextContent
23
- annotations: Annotations
24
- plain_text: str
25
- href: Optional[str]
26
-
27
-
28
- # Benutzerobjekt
29
- class User(BaseModel):
30
- object: str
31
- id: str
32
-
33
-
34
- # Elternobjekte
35
- class PageParent(BaseModel):
36
- type: Literal["page_id"]
37
- page_id: str
38
-
39
-
40
- class DatabaseParent(BaseModel):
41
- type: Literal["database_id"]
42
- database_id: str
43
-
44
-
45
- class WorkspaceParent(BaseModel):
46
- type: Literal["workspace"]
47
- workspace: bool = True
48
-
49
-
50
- Parent = Union[PageParent, DatabaseParent, WorkspaceParent]
51
-
52
-
53
- # Block-spezifische Inhalte
54
- class ParagraphBlock(BaseModel):
55
- rich_text: List[RichText]
56
- color: Optional[str] = "default"
57
-
58
-
59
- class Heading1Block(BaseModel):
60
- rich_text: List[RichText]
61
- color: Optional[str] = "default"
62
- is_toggleable: Optional[bool] = False
63
-
64
-
65
- class Heading2Block(BaseModel):
66
- rich_text: List[RichText]
67
- color: Optional[str] = "default"
68
- is_toggleable: Optional[bool] = False
69
-
70
-
71
- class Heading3Block(BaseModel):
72
- rich_text: List[RichText]
73
- color: Optional[str] = "default"
74
- is_toggleable: Optional[bool] = False
75
-
76
-
77
- class BulletedListItemBlock(BaseModel):
78
- rich_text: List[RichText]
79
- color: Optional[str] = "default"
80
-
81
-
82
- class NumberedListItemBlock(BaseModel):
83
- rich_text: List[RichText]
84
- color: Optional[str] = "default"
85
-
86
-
87
- class ToDoBlock(BaseModel):
88
- rich_text: List[RichText]
89
- checked: Optional[bool] = False
90
- color: Optional[str] = "default"
91
-
92
-
93
- class ToggleBlock(BaseModel):
94
- rich_text: List[RichText]
95
- color: Optional[str] = "default"
96
-
97
-
98
- class QuoteBlock(BaseModel):
99
- rich_text: List[RichText]
100
- color: Optional[str] = "default"
101
-
102
-
103
- class CalloutBlock(BaseModel):
104
- rich_text: List[RichText]
105
- icon: Optional[dict] = None
106
- color: Optional[str] = "default"
107
-
108
-
109
- class CodeBlock(BaseModel):
110
- rich_text: List[RichText]
111
- language: Optional[str] = "plain text"
112
-
113
-
114
- class EquationBlock(BaseModel):
115
- expression: str
116
-
117
-
118
- class DividerBlock(BaseModel):
119
- pass
120
-
121
-
122
- class TableOfContentsBlock(BaseModel):
123
- color: Optional[str] = "default"
124
-
125
-
126
- class BreadcrumbBlock(BaseModel):
127
- pass
128
-
129
-
130
- class ColumnListBlock(BaseModel):
131
- pass
132
-
133
-
134
- class ColumnBlock(BaseModel):
135
- pass
136
-
137
-
138
- class LinkToPageBlock(BaseModel):
139
- type: str
140
- page_id: Optional[str] = None
141
- database_id: Optional[str] = None
142
-
143
-
144
- class SyncedBlock(BaseModel):
145
- synced_from: Optional[dict] = None
146
-
147
-
148
- class TemplateBlock(BaseModel):
149
- rich_text: List[RichText]
150
-
151
-
152
- class TableBlock(BaseModel):
153
- table_width: int
154
- has_column_header: bool
155
- has_row_header: bool
156
-
157
-
158
- class TableRowBlock(BaseModel):
159
- cells: List[List[RichText]]
160
-
161
-
162
- class BookmarkBlock(BaseModel):
163
- caption: List[RichText]
164
- url: str
165
-
166
-
167
- class EmbedBlock(BaseModel):
168
- url: str
169
-
170
-
171
- class ImageBlock(BaseModel):
172
- type: str
173
- external: Optional[dict] = None
174
- file: Optional[dict] = None
175
- caption: List[RichText]
176
-
177
-
178
- class VideoBlock(BaseModel):
179
- type: str
180
- external: Optional[dict] = None
181
- file: Optional[dict] = None
182
- caption: List[RichText]
183
-
184
-
185
- class PDFBlock(BaseModel):
186
- type: str
187
- external: Optional[dict] = None
188
- file: Optional[dict] = None
189
- caption: List[RichText]
190
-
191
-
192
- class FileBlock(BaseModel):
193
- type: str
194
- external: Optional[dict] = None
195
- file: Optional[dict] = None
196
- caption: List[RichText]
197
-
198
-
199
- class AudioBlock(BaseModel):
200
- type: str
201
- external: Optional[dict] = None
202
- file: Optional[dict] = None
203
- caption: List[RichText]
204
-
205
-
206
- class LinkPreviewBlock(BaseModel):
207
- url: str
208
-
209
-
210
- class ChildPageBlock(BaseModel):
211
- title: str
212
-
213
-
214
- class ChildDatabaseBlock(BaseModel):
215
- title: str
216
-
217
-
218
- # TODO: Use the block typing here:
219
- # Test the code base.
220
- class Block(BaseModel):
221
- object: Literal["block"]
222
- id: str
223
- parent: Parent
224
- created_time: str
225
- last_edited_time: str
226
- created_by: User
227
- last_edited_by: User
228
- has_children: bool
229
- archived: bool
230
- in_trash: bool
231
- type: str
232
- paragraph: Optional[ParagraphBlock] = None
233
- heading_1: Optional[Heading1Block] = None
234
- heading_2: Optional[Heading2Block] = None
235
- heading_3: Optional[Heading3Block] = None
236
- bulleted_list_item: Optional[BulletedListItemBlock] = None
237
- numbered_list_item: Optional[NumberedListItemBlock] = None
238
- to_do: Optional[ToDoBlock] = None
239
- toggle: Optional[ToggleBlock] = None
240
- quote: Optional[QuoteBlock] = None
241
- callout: Optional[CalloutBlock] = None
242
- code: Optional[CodeBlock] = None
243
- equation: Optional[EquationBlock] = None
244
- divider: Optional[DividerBlock] = None
245
- table_of_contents: Optional[TableOfContentsBlock] = None
246
- breadcrumb: Optional[BreadcrumbBlock] = None
247
- column_list: Optional[ColumnListBlock] = None
248
- column: Optional[ColumnBlock] = None
249
- link_to_page: Optional[LinkToPageBlock] = None
250
- synced_block: Optional[SyncedBlock] = None
251
- template: Optional[TemplateBlock] = None
252
- table: Optional[TableBlock] = None
253
- table_row: Optional[TableRowBlock] = None
254
- bookmark: Optional[BookmarkBlock] = None
255
- embed: Optional[EmbedBlock] = None
256
- image: Optional[ImageBlock] = None
257
- video: Optional[VideoBlock] = None
258
- pdf: Optional[PDFBlock] = None
259
- file: Optional[FileBlock] = None
260
- audio: Optional[AudioBlock] = None
261
- link_preview: Optional[LinkPreviewBlock] = None
262
- child_page: Optional[ChildPageBlock] = None
263
- child_database: Optional[ChildDatabaseBlock] = None
264
- unsupported: Optional[dict] = None