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
@@ -0,0 +1,92 @@
1
+ from typing import Optional
2
+ import re
3
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
4
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
5
+
6
+
7
+ class CaptionMixin:
8
+ """Mixin to add caption parsing functionality to block elements."""
9
+
10
+ # Generic caption pattern - finds caption anywhere in text
11
+ CAPTION_PATTERN = re.compile(r"\(caption:([^)]*)\)")
12
+
13
+ @classmethod
14
+ def extract_caption(cls, text: str) -> Optional[str]:
15
+ """
16
+ Extract caption text from anywhere in the input text.
17
+ Returns only the caption content, preserving parentheses in content.
18
+ """
19
+ # Look for (caption: followed by content followed by )
20
+ # Handle cases where caption content contains parentheses
21
+ caption_start = text.find("(caption:")
22
+ if caption_start == -1:
23
+ return None
24
+
25
+ # Find the matching closing parenthesis
26
+ # Start after "(caption:"
27
+ content_start = caption_start + 9 # len("(caption:")
28
+ paren_count = 1
29
+ pos = content_start
30
+
31
+ while pos < len(text) and paren_count > 0:
32
+ if text[pos] == "(":
33
+ paren_count += 1
34
+ elif text[pos] == ")":
35
+ paren_count -= 1
36
+ pos += 1
37
+
38
+ if paren_count == 0:
39
+ # Found matching closing parenthesis
40
+ return text[content_start : pos - 1]
41
+
42
+ return None
43
+
44
+ @classmethod
45
+ def remove_caption(cls, text: str) -> str:
46
+ """
47
+ Remove caption from text and return clean text.
48
+ Uses the same balanced parentheses logic as extract_caption.
49
+ """
50
+ caption_start = text.find("(caption:")
51
+ if caption_start == -1:
52
+ return text.strip()
53
+
54
+ # Find the matching closing parenthesis
55
+ content_start = caption_start + 9 # len("(caption:")
56
+ paren_count = 1
57
+ pos = content_start
58
+
59
+ while pos < len(text) and paren_count > 0:
60
+ if text[pos] == "(":
61
+ paren_count += 1
62
+ elif text[pos] == ")":
63
+ paren_count -= 1
64
+ pos += 1
65
+
66
+ if paren_count == 0:
67
+ # Remove the entire caption including the outer parentheses
68
+ return (text[:caption_start] + text[pos:]).strip()
69
+
70
+ # Fallback to regex-based removal if balanced parsing fails
71
+ return cls.CAPTION_PATTERN.sub("", text).strip()
72
+
73
+ @classmethod
74
+ def build_caption_rich_text(cls, caption_text: str) -> list[RichTextObject]:
75
+ """Return caption as canonical rich text list (with annotations)."""
76
+ if not caption_text:
77
+ return []
78
+ # IMPORTANT: use the same formatter used elsewhere in the app
79
+ return [RichTextObject.for_caption(caption_text)]
80
+
81
+ @classmethod
82
+ async def format_caption_for_markdown(
83
+ cls, caption_list: list[RichTextObject]
84
+ ) -> str:
85
+ """Convert rich text caption back to markdown format."""
86
+ if not caption_list:
87
+ return ""
88
+ # Preserve markdown formatting (bold, italic, etc.)
89
+ caption_text = await TextInlineFormatter.extract_text_with_formatting(
90
+ caption_list
91
+ )
92
+ return f"(caption:{caption_text})" if caption_text else ""
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Literal, Optional, Union
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from notionary.blocks.types import BlockType
8
+
9
+ if TYPE_CHECKING:
10
+ from notionary.blocks.bookmark import BookmarkBlock, CreateBookmarkBlock
11
+ from notionary.blocks.breadcrumbs import BreadcrumbBlock, CreateBreadcrumbBlock
12
+ from notionary.blocks.bulleted_list import (
13
+ BulletedListItemBlock,
14
+ CreateBulletedListItemBlock,
15
+ )
16
+ from notionary.blocks.callout import CalloutBlock, CreateCalloutBlock
17
+ from notionary.blocks.child_page import ChildPageBlock, CreateChildPageBlock
18
+ from notionary.blocks.code import CodeBlock, CreateCodeBlock
19
+ from notionary.blocks.column import (
20
+ ColumnBlock,
21
+ ColumnListBlock,
22
+ CreateColumnBlock,
23
+ CreateColumnListBlock,
24
+ )
25
+ from notionary.blocks.divider import CreateDividerBlock, DividerBlock
26
+ from notionary.blocks.embed import CreateEmbedBlock, EmbedBlock
27
+ from notionary.blocks.equation import CreateEquationBlock, EquationBlock
28
+ from notionary.blocks.file import CreateFileBlock, FileBlock
29
+ from notionary.blocks.heading import (
30
+ CreateHeading1Block,
31
+ CreateHeading2Block,
32
+ CreateHeading3Block,
33
+ HeadingBlock,
34
+ )
35
+ from notionary.blocks.image_block import CreateImageBlock
36
+ from notionary.blocks.numbered_list import (
37
+ CreateNumberedListItemBlock,
38
+ NumberedListItemBlock,
39
+ )
40
+ from notionary.blocks.paragraph import CreateParagraphBlock, ParagraphBlock
41
+ from notionary.blocks.pdf import CreatePdfBlock
42
+ from notionary.blocks.quote import CreateQuoteBlock, QuoteBlock
43
+ from notionary.blocks.table import CreateTableBlock, TableBlock, TableRowBlock
44
+ from notionary.blocks.table_of_contents import (
45
+ CreateTableOfContentsBlock,
46
+ TableOfContentsBlock,
47
+ )
48
+ from notionary.blocks.todo import CreateToDoBlock, ToDoBlock
49
+ from notionary.blocks.toggle import CreateToggleBlock, ToggleBlock
50
+ from notionary.blocks.video import CreateVideoBlock
51
+ from notionary.blocks.child_database import ChildDatabaseBlock
52
+
53
+
54
+ class BlockChildrenResponse(BaseModel):
55
+ object: Literal["list"]
56
+ results: list["Block"]
57
+ next_cursor: Optional[str] = None
58
+ has_more: bool
59
+ type: Literal["block"]
60
+ block: dict = {}
61
+ request_id: str
62
+
63
+
64
+ class PageParent(BaseModel):
65
+ type: Literal["page_id"]
66
+ page_id: str
67
+
68
+
69
+ class DatabaseParent(BaseModel):
70
+ type: Literal["database_id"]
71
+ database_id: str
72
+
73
+
74
+ class BlockParent(BaseModel):
75
+ type: Literal["block_id"]
76
+ block_id: str
77
+
78
+
79
+ class WorkspaceParent(BaseModel):
80
+ type: Literal["workspace"]
81
+ workspace: bool = True
82
+
83
+
84
+ ParentObject = Union[PageParent, DatabaseParent, BlockParent, WorkspaceParent]
85
+
86
+
87
+ class PartialUser(BaseModel):
88
+ object: Literal["user"]
89
+ id: str
90
+
91
+
92
+ class Block(BaseModel):
93
+ object: Literal["block"]
94
+ id: str
95
+ parent: Optional[ParentObject] = None
96
+ type: BlockType
97
+ created_time: str
98
+ last_edited_time: str
99
+ created_by: PartialUser
100
+ last_edited_by: PartialUser
101
+ archived: bool = False
102
+ in_trash: bool = False
103
+ has_children: bool = False
104
+
105
+ children: Optional[list[Block]] = None
106
+
107
+ # Block type-specific content (only one will be populated based on type)
108
+ audio: Optional[FileBlock] = None
109
+ bookmark: Optional[BookmarkBlock] = None
110
+ breadcrumb: Optional[BreadcrumbBlock] = None
111
+ bulleted_list_item: Optional[BulletedListItemBlock] = None
112
+ callout: Optional[CalloutBlock] = None
113
+ child_page: Optional[ChildPageBlock] = None
114
+ code: Optional[CodeBlock] = None
115
+ column_list: Optional[ColumnListBlock] = None
116
+ column: Optional[ColumnBlock] = None
117
+ divider: Optional[DividerBlock] = None
118
+ embed: Optional[EmbedBlock] = None
119
+ equation: Optional[EquationBlock] = None
120
+ file: Optional[FileBlock] = None
121
+ heading_1: Optional[HeadingBlock] = None
122
+ heading_2: Optional[HeadingBlock] = None
123
+ heading_3: Optional[HeadingBlock] = None
124
+ image: Optional[FileBlock] = None
125
+ numbered_list_item: Optional[NumberedListItemBlock] = None
126
+ paragraph: Optional[ParagraphBlock] = None
127
+ quote: Optional[QuoteBlock] = None
128
+ table: Optional[TableBlock] = None
129
+ table_row: Optional[TableRowBlock] = None
130
+ to_do: Optional[ToDoBlock] = None
131
+ toggle: Optional[ToggleBlock] = None
132
+ video: Optional[FileBlock] = None
133
+ pdf: Optional[FileBlock] = None
134
+ table_of_contents: Optional[TableOfContentsBlock] = None
135
+ child_database: Optional[ChildDatabaseBlock] = None
136
+
137
+ def get_block_content(self) -> Optional[Any]:
138
+ """Get the content object for this block based on its type."""
139
+ return getattr(self, self.type, None)
140
+
141
+
142
+ if TYPE_CHECKING:
143
+ BlockCreateRequest = Union[
144
+ CreateBookmarkBlock,
145
+ CreateBreadcrumbBlock,
146
+ CreateBulletedListItemBlock,
147
+ CreateCalloutBlock,
148
+ CreateChildPageBlock,
149
+ CreateCodeBlock,
150
+ CreateColumnListBlock,
151
+ CreateColumnBlock,
152
+ CreateDividerBlock,
153
+ CreateEmbedBlock,
154
+ CreateEquationBlock,
155
+ CreateFileBlock,
156
+ CreateHeading1Block,
157
+ CreateHeading2Block,
158
+ CreateHeading3Block,
159
+ CreateImageBlock,
160
+ CreateNumberedListItemBlock,
161
+ CreateParagraphBlock,
162
+ CreateQuoteBlock,
163
+ CreateToDoBlock,
164
+ CreateToggleBlock,
165
+ CreateVideoBlock,
166
+ CreateTableOfContentsBlock,
167
+ CreatePdfBlock,
168
+ CreateTableBlock,
169
+ ]
170
+ BlockCreateResult = Union[BlockCreateRequest]
171
+ else:
172
+ # at runtime there are no typings anyway
173
+ BlockCreateRequest = Any
174
+ BlockCreateResult = Any
@@ -1,7 +1,17 @@
1
- from .numbered_list_element import NumberedListElement
2
- from .numbered_list_markdown_node import NumberedListMarkdownNode
1
+ from notionary.blocks.numbered_list.numbered_list_element import NumberedListElement
2
+ from notionary.blocks.numbered_list.numbered_list_markdown_node import (
3
+ NumberedListMarkdownBlockParams,
4
+ NumberedListMarkdownNode,
5
+ )
6
+ from notionary.blocks.numbered_list.numbered_list_models import (
7
+ CreateNumberedListItemBlock,
8
+ NumberedListItemBlock,
9
+ )
3
10
 
4
11
  __all__ = [
5
12
  "NumberedListElement",
13
+ "NumberedListItemBlock",
14
+ "CreateNumberedListItemBlock",
6
15
  "NumberedListMarkdownNode",
16
+ "NumberedListMarkdownBlockParams",
7
17
  ]
@@ -1,73 +1,65 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Any, Optional
3
- from notionary.blocks import NotionBlockElement
4
- from notionary.blocks import (
5
- ElementPromptContent,
6
- ElementPromptBuilder,
7
- NotionBlockResult,
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
8
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
9
+ from notionary.blocks.numbered_list.numbered_list_models import (
10
+ CreateNumberedListItemBlock,
11
+ NumberedListItemBlock,
8
12
  )
9
- from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
13
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
14
+ from notionary.blocks.types import BlockColor
10
15
 
11
16
 
12
- class NumberedListElement(NotionBlockElement):
13
- """Class for converting between Markdown numbered lists and Notion numbered list items."""
17
+ class NumberedListElement(BaseBlockElement):
18
+ """Converts between Markdown numbered lists and Notion numbered list items."""
14
19
 
15
- @classmethod
16
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
17
- """Convert markdown numbered list item to Notion block."""
18
- pattern = re.compile(r"^\s*(\d+)\.\s+(.+)$")
19
- numbered_match = pattern.match(text)
20
- if not numbered_match:
21
- return None
20
+ PATTERN = re.compile(r"^\s*(\d+)\.\s+(.+)$")
22
21
 
23
- content = numbered_match.group(2)
24
-
25
- # Use parse_inline_formatting to handle rich text
26
- rich_text = TextInlineFormatter.parse_inline_formatting(content)
27
-
28
- return {
29
- "type": "numbered_list_item",
30
- "numbered_list_item": {"rich_text": rich_text, "color": "default"},
31
- }
22
+ @classmethod
23
+ def match_notion(cls, block: Block) -> bool:
24
+ return block.type == BlockType.NUMBERED_LIST_ITEM and block.numbered_list_item
32
25
 
33
26
  @classmethod
34
- def notion_to_markdown(cls, block: dict[str, Any]) -> Optional[str]:
35
- """Convert Notion numbered list item block to markdown."""
36
- if block.get("type") != "numbered_list_item":
27
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
28
+ """Convert markdown numbered list item to Notion NumberedListItemBlock."""
29
+ match = cls.PATTERN.match(text.strip())
30
+ if not match:
37
31
  return None
38
32
 
39
- rich_text = block.get("numbered_list_item", {}).get("rich_text", [])
40
- content = TextInlineFormatter.extract_text_with_formatting(rich_text)
33
+ content = match.group(2)
34
+ rich_text = await TextInlineFormatter.parse_inline_formatting(content)
41
35
 
42
- return f"1. {content}"
43
-
44
- @classmethod
45
- def match_markdown(cls, text: str) -> bool:
46
- """Check if this element can handle the given markdown text."""
47
- pattern = re.compile(r"^\s*\d+\.\s+(.+)$")
48
- return bool(pattern.match(text))
36
+ numbered_list_content = NumberedListItemBlock(
37
+ rich_text=rich_text, color=BlockColor.DEFAULT
38
+ )
39
+ return CreateNumberedListItemBlock(numbered_list_item=numbered_list_content)
49
40
 
41
+ # FIX: Roundtrip conversions will never work this way here
50
42
  @classmethod
51
- def match_notion(cls, block: dict[str, Any]) -> bool:
52
- """Check if this element can handle the given Notion block."""
53
- return block.get("type") == "numbered_list_item"
43
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
44
+ if block.type != BlockType.NUMBERED_LIST_ITEM or not block.numbered_list_item:
45
+ return None
54
46
 
55
- @classmethod
56
- def is_multiline(cls) -> bool:
57
- return False
47
+ rich = block.numbered_list_item.rich_text
48
+ content = await TextInlineFormatter.extract_text_with_formatting(rich)
49
+ return f"1. {content}"
58
50
 
59
51
  @classmethod
60
- def get_llm_prompt_content(cls) -> ElementPromptContent:
61
- """
62
- Returns structured LLM prompt metadata for the numbered list element.
63
- """
64
- return (
65
- ElementPromptBuilder()
66
- .with_description("Creates numbered list items for ordered sequences.")
67
- .with_usage_guidelines(
68
- "Use for lists where order matters, such as steps, rankings, or sequential items."
69
- )
70
- .with_syntax("1. Item text")
71
- .with_standard_markdown()
72
- .build()
52
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
53
+ """Get system prompt information for numbered list blocks."""
54
+ return BlockElementMarkdownInformation(
55
+ block_type=cls.__name__,
56
+ description="Numbered list items create ordered lists with sequential numbering",
57
+ syntax_examples=[
58
+ "1. First item",
59
+ "2. Second item",
60
+ "3. Third item",
61
+ "1. Item with **bold text**",
62
+ "1. Item with *italic text*",
63
+ ],
64
+ usage_guidelines="Use numbers followed by periods to create ordered lists. Supports inline formatting like bold, italic, and links. Numbering is automatically handled by Notion.",
73
65
  )
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
+
2
3
  from pydantic import BaseModel
3
- from notionary.blocks.markdown_node import MarkdownNode
4
+
5
+ from notionary.markdown.markdown_node import MarkdownNode
4
6
 
5
7
 
6
8
  class NumberedListMarkdownBlockParams(BaseModel):
@@ -0,0 +1,17 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing_extensions import Literal
3
+
4
+ from notionary.blocks.models import Block
5
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
6
+ from notionary.blocks.types import BlockColor
7
+
8
+
9
+ class NumberedListItemBlock(BaseModel):
10
+ rich_text: list[RichTextObject]
11
+ color: BlockColor = BlockColor.DEFAULT
12
+ children: list[Block] = Field(default_factory=list)
13
+
14
+
15
+ class CreateNumberedListItemBlock(BaseModel):
16
+ type: Literal["numbered_list_item"] = "numbered_list_item"
17
+ numbered_list_item: NumberedListItemBlock
@@ -1,7 +1,17 @@
1
- from .paragraph_element import ParagraphElement
2
- from .paragraph_markdown_node import ParagraphMarkdownNode
1
+ from notionary.blocks.paragraph.paragraph_element import ParagraphElement
2
+ from notionary.blocks.paragraph.paragraph_markdown_node import (
3
+ ParagraphMarkdownBlockParams,
4
+ ParagraphMarkdownNode,
5
+ )
6
+ from notionary.blocks.paragraph.paragraph_models import (
7
+ CreateParagraphBlock,
8
+ ParagraphBlock,
9
+ )
3
10
 
4
11
  __all__ = [
5
12
  "ParagraphElement",
13
+ "ParagraphBlock",
14
+ "CreateParagraphBlock",
6
15
  "ParagraphMarkdownNode",
16
+ "ParagraphMarkdownBlockParams",
7
17
  ]
@@ -1,84 +1,58 @@
1
- from typing import Dict, Any, Optional
1
+ from __future__ import annotations
2
2
 
3
- from notionary.blocks import NotionBlockElement
4
- from notionary.blocks import (
5
- ElementPromptContent,
6
- ElementPromptBuilder,
7
- NotionBlockResult,
8
- )
9
- from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
3
+ from typing import Optional
10
4
 
5
+ from notionary.blocks.base_block_element import BaseBlockElement
6
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
7
+ from notionary.blocks.models import Block, BlockCreateResult
8
+ from notionary.blocks.paragraph.paragraph_models import (
9
+ CreateParagraphBlock,
10
+ ParagraphBlock,
11
+ )
12
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
13
+ from notionary.blocks.types import BlockColor, BlockType
11
14
 
12
- class ParagraphElement(NotionBlockElement):
13
- """Handles conversion between Markdown paragraphs and Notion paragraph blocks."""
14
15
 
15
- @classmethod
16
- def match_markdown(cls, text: str) -> bool:
17
- """
18
- Check if text is a markdown paragraph.
19
- Paragraphs are essentially any text that isn't matched by other block elements.
20
- Since paragraphs are the fallback element, this always returns True.
21
- """
22
- return True
16
+ class ParagraphElement(BaseBlockElement):
17
+ """
18
+ Handles conversion between Markdown paragraphs and Notion paragraph blocks.
19
+ """
23
20
 
24
21
  @classmethod
25
- def match_notion(cls, block: Dict[str, Any]) -> bool:
26
- """Check if block is a Notion paragraph."""
27
- return block.get("type") == "paragraph"
22
+ def match_notion(cls, block: Block) -> bool:
23
+ return block.type == "paragraph" and block.paragraph
28
24
 
29
25
  @classmethod
30
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
31
- """Convert markdown paragraph to Notion paragraph block."""
26
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
27
+ """Convert markdown text to a Notion ParagraphBlock."""
32
28
  if not text.strip():
33
29
  return None
34
30
 
35
- return {
36
- "type": "paragraph",
37
- "paragraph": {
38
- "rich_text": TextInlineFormatter.parse_inline_formatting(text)
39
- },
40
- }
31
+ rich = await TextInlineFormatter.parse_inline_formatting(text)
32
+
33
+ paragraph_content = ParagraphBlock(rich_text=rich, color=BlockColor.DEFAULT)
34
+ return CreateParagraphBlock(paragraph=paragraph_content)
41
35
 
42
36
  @classmethod
43
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
44
- """Convert Notion paragraph block to markdown paragraph."""
45
- if block.get("type") != "paragraph":
37
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
38
+ if block.type != BlockType.PARAGRAPH or not block.paragraph:
46
39
  return None
47
40
 
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
- @classmethod
55
- def is_multiline(cls) -> bool:
56
- return False
41
+ rich_list = block.paragraph.rich_text
42
+ markdown = await TextInlineFormatter.extract_text_with_formatting(rich_list)
43
+ return markdown or None
57
44
 
58
45
  @classmethod
59
- def get_llm_prompt_content(cls) -> ElementPromptContent:
60
- """
61
- Returns structured LLM prompt metadata for the paragraph element,
62
- including information about supported inline formatting.
63
- """
64
- return (
65
- ElementPromptBuilder()
66
- .with_description(
67
- "Creates standard paragraph blocks for regular text content with support for inline formatting: "
68
- "**bold**, *italic*, `code`, ~~strikethrough~~, __underline__, and [links](url)."
69
- )
70
- .with_usage_guidelines(
71
- "Use for normal text content. Paragraphs are the default block type when no specific formatting is applied. "
72
- "Apply inline formatting to highlight key points or provide links to resources."
73
- )
74
- .with_syntax("Just write text normally without any special prefix")
75
- .with_examples(
76
- [
77
- "This is a simple paragraph with plain text.",
78
- "This paragraph has **bold** and *italic* formatting.",
79
- "You can include [links](https://example.com) or `inline code`.",
80
- "Advanced formatting: ~~strikethrough~~ and __underlined text__.",
81
- ]
82
- )
83
- .build()
46
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
47
+ """Get system prompt information for paragraph blocks."""
48
+ return BlockElementMarkdownInformation(
49
+ block_type=cls.__name__,
50
+ description="Paragraph blocks contain regular text content with optional inline formatting",
51
+ syntax_examples=[
52
+ "This is a simple paragraph.",
53
+ "Paragraph with **bold text** and *italic text*.",
54
+ "Paragraph with [link](https://example.com) and `code`.",
55
+ "Multiple sentences in one paragraph. Each sentence flows naturally.",
56
+ ],
57
+ usage_guidelines="Use for regular text content. Supports inline formatting: **bold**, *italic*, `code`, [links](url). Default block type for plain text.",
84
58
  )
@@ -1,7 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from pydantic import BaseModel
4
- from notionary.blocks.markdown_node import MarkdownNode
4
+
5
+ from notionary.markdown.markdown_node import MarkdownNode
5
6
 
6
7
 
7
8
  class ParagraphMarkdownBlockParams(BaseModel):
@@ -0,0 +1,16 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
6
+ from notionary.blocks.types import BlockColor
7
+
8
+
9
+ class ParagraphBlock(BaseModel):
10
+ rich_text: list[RichTextObject]
11
+ color: BlockColor = BlockColor.DEFAULT.value
12
+
13
+
14
+ class CreateParagraphBlock(BaseModel):
15
+ type: Literal["paragraph"] = "paragraph"
16
+ paragraph: ParagraphBlock
@@ -0,0 +1,13 @@
1
+ from notionary.blocks.pdf.pdf_element import PdfElement
2
+ from notionary.blocks.pdf.pdf_markdown_node import (
3
+ PdfMarkdownNode,
4
+ PdfMarkdownNodeParams,
5
+ )
6
+ from notionary.blocks.pdf.pdf_models import CreatePdfBlock
7
+
8
+ __all__ = [
9
+ "PdfElement",
10
+ "CreatePdfBlock",
11
+ "PdfMarkdownNode",
12
+ "PdfMarkdownNodeParams",
13
+ ]