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,72 +1,74 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Dict, Any, Optional
3
- from notionary.blocks import NotionBlockElement
4
- from notionary.blocks import (
5
- ElementPromptContent,
6
- ElementPromptBuilder,
7
- NotionBlockResult,
8
- )
4
+ from typing import Optional
9
5
 
10
- from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.bulleted_list.bulleted_list_models import (
8
+ BulletedListItemBlock,
9
+ CreateBulletedListItemBlock,
10
+ )
11
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
12
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
13
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
11
14
 
12
15
 
13
- class BulletedListElement(NotionBlockElement):
16
+ class BulletedListElement(BaseBlockElement):
14
17
  """Class for converting between Markdown bullet lists and Notion bulleted list items."""
15
18
 
19
+ # Regex for markdown bullets (excluding todo items [ ] or [x])
20
+ PATTERN = re.compile(r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$")
21
+
16
22
  @classmethod
17
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
18
- """Convert markdown bulleted list item to Notion block."""
19
- pattern = re.compile(
20
- r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$"
21
- ) # Avoid matching todo items
22
- list_match = pattern.match(text)
23
- if not list_match:
23
+ def match_notion(cls, block: Block) -> bool:
24
+ """Check if this element can handle the given Notion block."""
25
+ return block.type == BlockType.BULLETED_LIST_ITEM and block.bulleted_list_item
26
+
27
+ @classmethod
28
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
29
+ """
30
+ Convert a markdown bulleted list item into a Notion BulletedListItemBlock.
31
+ """
32
+ if not (match := cls.PATTERN.match(text.strip())):
24
33
  return None
25
34
 
26
- content = list_match.group(2)
35
+ # Extract the content part (second capture group)
36
+ content = match.group(2)
27
37
 
28
- # Use parse_inline_formatting to handle rich text
29
- rich_text = TextInlineFormatter.parse_inline_formatting(content)
38
+ # Parse inline markdown formatting into RichTextObject list
39
+ rich_text = await TextInlineFormatter.parse_inline_formatting(content)
30
40
 
31
- return {
32
- "type": "bulleted_list_item",
33
- "bulleted_list_item": {"rich_text": rich_text, "color": "default"},
34
- }
41
+ # Return a properly typed Notion block
42
+ bulleted_list_content = BulletedListItemBlock(
43
+ rich_text=rich_text, color="default"
44
+ )
45
+ return CreateBulletedListItemBlock(bulleted_list_item=bulleted_list_content)
35
46
 
36
47
  @classmethod
37
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
38
- """Convert Notion bulleted list item block to markdown."""
39
- if block.get("type") != "bulleted_list_item":
48
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
49
+ """Convert Notion bulleted_list_item block to Markdown."""
50
+ if block.type != BlockType.BULLETED_LIST_ITEM or not block.bulleted_list_item:
40
51
  return None
41
52
 
42
- rich_text = block.get("bulleted_list_item", {}).get("rich_text", [])
43
- content = TextInlineFormatter.extract_text_with_formatting(rich_text)
44
-
45
- return f"- {content}"
46
-
47
- @classmethod
48
- def match_markdown(cls, text: str) -> bool:
49
- """Check if this element can handle the given markdown text."""
50
- pattern = re.compile(r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$")
51
- return bool(pattern.match(text))
53
+ rich_list = block.bulleted_list_item.rich_text
54
+ if not rich_list:
55
+ return "-"
52
56
 
53
- @classmethod
54
- def match_notion(cls, block: Dict[str, Any]) -> bool:
55
- """Check if this element can handle the given Notion block."""
56
- return block.get("type") == "bulleted_list_item"
57
+ text = await TextInlineFormatter.extract_text_with_formatting(rich_list)
58
+ return f"- {text}"
57
59
 
58
60
  @classmethod
59
- def get_llm_prompt_content(cls) -> ElementPromptContent:
60
- """
61
- Returns structured LLM prompt metadata for the bulleted list element.
62
- """
63
- return (
64
- ElementPromptBuilder()
65
- .with_description("Creates bulleted list items for unordered lists.")
66
- .with_usage_guidelines(
67
- "Use for lists where order doesn't matter, such as features, options, or items without hierarchy."
68
- )
69
- .with_syntax("- Item text")
70
- .with_standard_markdown()
71
- .build()
61
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
62
+ """Get system prompt information for bulleted list blocks."""
63
+ return BlockElementMarkdownInformation(
64
+ block_type=cls.__name__,
65
+ description="Bulleted list items create unordered lists with bullet points",
66
+ syntax_examples=[
67
+ "- First item",
68
+ "* Second item",
69
+ "+ Third item",
70
+ "- Item with **bold text**",
71
+ "- Item with *italic text*",
72
+ ],
73
+ usage_guidelines="Use -, *, or + to create bullet points. Supports inline formatting like bold, italic, and links. Do not use for todo items (use [ ] or [x] for those).",
72
74
  )
@@ -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 BulletedListMarkdownBlockParams(BaseModel):
@@ -0,0 +1,18 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from notionary.blocks.models import Block
6
+ from notionary.blocks.rich_text import RichTextObject
7
+ from notionary.blocks.types import BlockColor
8
+
9
+
10
+ class BulletedListItemBlock(BaseModel):
11
+ rich_text: list[RichTextObject]
12
+ color: BlockColor = BlockColor.DEFAULT
13
+ children: list[Block] = Field(default_factory=list)
14
+
15
+
16
+ class CreateBulletedListItemBlock(BaseModel):
17
+ type: Literal["bulleted_list_item"] = "bulleted_list_item"
18
+ bulleted_list_item: BulletedListItemBlock
@@ -1,7 +1,14 @@
1
- from .callout_element import CalloutElement
2
- from .callout_markdown_node import CalloutMarkdownNode
1
+ from notionary.blocks.callout.callout_element import CalloutElement
2
+ from notionary.blocks.callout.callout_markdown_node import (
3
+ CalloutMarkdownBlockParams,
4
+ CalloutMarkdownNode,
5
+ )
6
+ from notionary.blocks.callout.callout_models import CalloutBlock, CreateCalloutBlock
3
7
 
4
8
  __all__ = [
5
9
  "CalloutElement",
10
+ "CalloutBlock",
11
+ "CreateCalloutBlock",
6
12
  "CalloutMarkdownNode",
13
+ "CalloutMarkdownBlockParams",
7
14
  ]
@@ -1,16 +1,21 @@
1
+ from __future__ import annotations
2
+
1
3
  import re
2
- from typing import Dict, Any, Optional, List
3
-
4
- from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
5
- from notionary.blocks import (
6
- NotionBlockElement,
7
- ElementPromptContent,
8
- ElementPromptBuilder,
9
- NotionBlockResult,
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.callout.callout_models import (
8
+ CalloutBlock,
9
+ CreateCalloutBlock,
10
+ EmojiIcon,
11
+ IconObject,
10
12
  )
13
+ from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
14
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
15
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
11
16
 
12
17
 
13
- class CalloutElement(NotionBlockElement):
18
+ class CalloutElement(BaseBlockElement):
14
19
  """
15
20
  Handles conversion between Markdown callouts and Notion callout blocks.
16
21
 
@@ -23,110 +28,72 @@ class CalloutElement(NotionBlockElement):
23
28
  - emoji is an optional emoji character (enclosed in quotes)
24
29
  """
25
30
 
26
- # Regex pattern for callout syntax with optional emoji
27
31
  PATTERN = re.compile(
28
- r"^\[callout\]\(" # [callout]( prefix
29
- + r'([^"]+?)' # Text content (required)
30
- + r'(?:\s+"([^"]+)")?' # Optional emoji in quotes
31
- + r"\)$" # closing parenthesis
32
+ r"^\[callout\]\(" # prefix
33
+ r"([^\"]+?)" # content
34
+ r"(?:\s+\"([^\"]+)\")?" # optional emoji
35
+ r"\)$"
32
36
  )
33
37
 
34
- # Default values
35
38
  DEFAULT_EMOJI = "💡"
36
39
  DEFAULT_COLOR = "gray_background"
37
40
 
38
41
  @classmethod
39
- def match_markdown(cls, text: str) -> bool:
40
- """Check if text is a markdown callout."""
41
- return text.strip().startswith("[callout]") and bool(
42
- CalloutElement.PATTERN.match(text.strip())
43
- )
44
-
45
- @classmethod
46
- def match_notion(cls, block: Dict[str, Any]) -> bool:
47
- """Check if block is a Notion callout."""
48
- return block.get("type") == "callout"
42
+ def match_notion(cls, block: Block) -> bool:
43
+ return block.type == BlockType.CALLOUT and block.callout
49
44
 
50
45
  @classmethod
51
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
52
- """Convert markdown callout to Notion callout block."""
53
- callout_match = CalloutElement.PATTERN.match(text.strip())
54
- if not callout_match:
46
+ async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
47
+ """Convert a markdown callout into a Notion CalloutBlock."""
48
+ match = cls.PATTERN.match(text.strip())
49
+ if not match:
55
50
  return None
56
51
 
57
- content = callout_match.group(1)
58
- emoji = callout_match.group(2)
59
-
52
+ content, emoji = match.group(1), match.group(2)
60
53
  if not content:
61
54
  return None
62
55
 
63
- # Use default emoji if none provided
64
56
  if not emoji:
65
- emoji = CalloutElement.DEFAULT_EMOJI
57
+ emoji = cls.DEFAULT_EMOJI
66
58
 
67
- callout_data = {
68
- "rich_text": TextInlineFormatter.parse_inline_formatting(content.strip()),
69
- "icon": {"type": "emoji", "emoji": emoji},
70
- "color": CalloutElement.DEFAULT_COLOR,
71
- }
59
+ rich_text = await TextInlineFormatter.parse_inline_formatting(content.strip())
72
60
 
73
- return {"type": "callout", "callout": callout_data}
61
+ callout_content = CalloutBlock(
62
+ rich_text=rich_text,
63
+ icon=EmojiIcon(emoji=emoji),
64
+ color=cls.DEFAULT_COLOR,
65
+ )
66
+ return CreateCalloutBlock(callout=callout_content)
74
67
 
75
68
  @classmethod
76
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
77
- """Convert Notion callout block to markdown callout."""
78
- if block.get("type") != "callout":
69
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
70
+ if block.type != BlockType.CALLOUT or not block.callout:
79
71
  return None
80
72
 
81
- callout_data = block.get("callout", {})
82
- rich_text = callout_data.get("rich_text", [])
83
- icon = callout_data.get("icon", {})
73
+ data = block.callout
84
74
 
85
- content = TextInlineFormatter.extract_text_with_formatting(rich_text)
75
+ content = await TextInlineFormatter.extract_text_with_formatting(data.rich_text)
86
76
  if not content:
87
77
  return None
88
78
 
89
- emoji = CalloutElement._extract_emoji(icon)
90
-
91
- if emoji and emoji != CalloutElement.DEFAULT_EMOJI:
92
- return f'[callout]({content} "{emoji}")'
79
+ icon: Optional[IconObject] = block.callout.icon
80
+ emoji_char = icon.emoji if isinstance(icon, EmojiIcon) else cls.DEFAULT_EMOJI
93
81
 
82
+ if emoji_char and emoji_char != cls.DEFAULT_EMOJI:
83
+ return f'[callout]({content} "{emoji_char}")'
94
84
  return f"[callout]({content})"
95
85
 
96
86
  @classmethod
97
- def is_multiline(cls) -> bool:
98
- """Callouts are single-line elements."""
99
- return False
100
-
101
- @classmethod
102
- def _extract_emoji(cls, icon: Dict[str, Any]) -> str:
103
- """Extract emoji from Notion icon object."""
104
- if icon and icon.get("type") == "emoji":
105
- return icon.get("emoji", "")
106
- return ""
107
-
108
- @classmethod
109
- def get_llm_prompt_content(cls) -> ElementPromptContent:
110
- """
111
- Returns structured LLM prompt metadata for the callout element.
112
- """
113
- return (
114
- ElementPromptBuilder()
115
- .with_description(
116
- "Creates a callout block to highlight important information with an icon."
117
- )
118
- .with_usage_guidelines(
119
- "Use callouts when you want to draw attention to important information, "
120
- "tips, warnings, or notes that stand out from the main content."
121
- )
122
- .with_syntax('[callout](Text content "Optional emoji")')
123
- .with_examples(
124
- [
125
- "[callout](This is a default callout with the light bulb emoji)",
126
- '[callout](This is a callout with a bell emoji "🔔")',
127
- '[callout](Warning: This is an important note "⚠️")',
128
- '[callout](Tip: Add emoji that matches your content\'s purpose "💡")',
129
- ]
130
- )
131
- .build()
87
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
88
+ """Get system prompt information for callout blocks."""
89
+ return BlockElementMarkdownInformation(
90
+ block_type=cls.__name__,
91
+ description="Callout blocks create highlighted text boxes with optional custom emojis for emphasis",
92
+ syntax_examples=[
93
+ "[callout](This is important information)",
94
+ '[callout](Warning message "⚠️")',
95
+ '[callout](Success message "")',
96
+ "[callout](Note with default emoji)",
97
+ ],
98
+ usage_guidelines="Use for highlighting important information, warnings, tips, or notes. Default emoji is 💡. Custom emoji should be provided in quotes after the text content.",
132
99
  )
@@ -1,8 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from typing import Optional
4
+
4
5
  from pydantic import BaseModel
5
- from notionary.blocks.markdown_node import MarkdownNode
6
+
7
+ from notionary.markdown.markdown_node import MarkdownNode
6
8
 
7
9
 
8
10
  class CalloutMarkdownBlockParams(BaseModel):
@@ -0,0 +1,33 @@
1
+ from typing import Literal, Optional, Union
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+ from notionary.blocks.file.file_element_models import FileBlock
6
+ from notionary.blocks.models import Block
7
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
8
+ from notionary.blocks.types import BlockColor
9
+
10
+
11
+ class EmojiIcon(BaseModel):
12
+ type: Literal["emoji"] = "emoji"
13
+ emoji: str
14
+
15
+
16
+ class FileIcon(BaseModel):
17
+ type: Literal["file"] = "file"
18
+ file: FileBlock
19
+
20
+
21
+ IconObject = Union[EmojiIcon, FileIcon]
22
+
23
+
24
+ class CalloutBlock(BaseModel):
25
+ rich_text: list[RichTextObject]
26
+ icon: Optional[IconObject] = None
27
+ color: BlockColor = BlockColor.DEFAULT
28
+ children: list[Block] = Field(default_factory=list)
29
+
30
+
31
+ class CreateCalloutBlock(BaseModel):
32
+ type: Literal["callout"] = "callout"
33
+ callout: CalloutBlock
@@ -0,0 +1,14 @@
1
+ """
2
+ Child Database Block Module
3
+
4
+ This module provides functionality for handling Notion child database blocks.
5
+ """
6
+
7
+ from .child_database_element import ChildDatabaseElement
8
+ from .child_database_models import ChildDatabaseBlock, CreateChildDatabaseBlock
9
+
10
+ __all__ = [
11
+ "ChildDatabaseElement",
12
+ "ChildDatabaseBlock",
13
+ "CreateChildDatabaseBlock",
14
+ ]
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ import re
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, BlockType
9
+ from notionary.util import LoggingMixin
10
+
11
+ class ChildDatabaseElement(BaseBlockElement, LoggingMixin):
12
+ """
13
+ Handles conversion between Markdown database references and Notion child database blocks.
14
+
15
+ Creates new databases when converting from markdown.
16
+ """
17
+
18
+ PATTERN_BRACKET = re.compile(r"^\[database:\s*(.+)\]$", re.IGNORECASE)
19
+ PATTERN_EMOJI = re.compile(r"^📊\s*(.+)$")
20
+
21
+ @classmethod
22
+ def match_notion(cls, block: Block) -> bool:
23
+ return block.type == BlockType.CHILD_DATABASE and block.child_database
24
+
25
+ @classmethod
26
+ async def markdown_to_notion(
27
+ cls, text: str
28
+ ) -> Optional[str]:
29
+ """
30
+ Convert markdown database syntax to actual Notion database.
31
+ Returns the database_id if successful, None otherwise.
32
+ """
33
+ cls.logger.warning("Creating database from markdown is not supported via the block api. Call the create_child_page method in NotionPage instead.s")
34
+ return None
35
+
36
+ @classmethod
37
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
38
+ if block.type != BlockType.CHILD_DATABASE or not block.child_database:
39
+ return None
40
+
41
+ title = block.child_database.title
42
+ if not title or not title.strip():
43
+ return None
44
+
45
+ # Use bracket syntax for output
46
+ return f"[database: {title.strip()}]"
47
+
48
+ @classmethod
49
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
50
+ """Get system prompt information for child database blocks."""
51
+ return BlockElementMarkdownInformation(
52
+ block_type=cls.__name__,
53
+ description="Creates new embedded databases within a Notion page",
54
+ syntax_examples=[
55
+ "[database: Project Tasks]",
56
+ "[database: Customer Information]",
57
+ "📊 Sales Pipeline",
58
+ "📊 Team Directory",
59
+ ],
60
+ usage_guidelines="Use to create new databases that will be embedded in the page. The database will be created with a basic 'Name' property and can be customized later.",
61
+ )
@@ -0,0 +1,12 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ChildDatabaseBlock(BaseModel):
7
+ title: str
8
+
9
+
10
+ class CreateChildDatabaseBlock(BaseModel):
11
+ type: Literal["child_database"] = "child_database"
12
+ child_database: ChildDatabaseBlock
@@ -0,0 +1,9 @@
1
+ from notionary.blocks.child_page.child_page_models import (
2
+ ChildPageBlock,
3
+ CreateChildPageBlock,
4
+ )
5
+
6
+ __all__ = [
7
+ "ChildPageBlock",
8
+ "CreateChildPageBlock",
9
+ ]
@@ -0,0 +1,94 @@
1
+ from __future__ import annotations
2
+
3
+ import re
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.page.page_context import get_page_context
10
+
11
+
12
+ class ChildPageElement(BaseBlockElement):
13
+ """
14
+ Handles conversion between Markdown page references and Notion child page blocks.
15
+
16
+ Creates new pages when converting from markdown.
17
+ """
18
+
19
+ PATTERN_BRACKET = re.compile(r"^\[page:\s*(.+)\]$", re.IGNORECASE)
20
+ PATTERN_EMOJI = re.compile(r"^[📝📄]\s*(.+)$")
21
+
22
+ @classmethod
23
+ def match_notion(cls, block: Block) -> bool:
24
+ return block.type == BlockType.CHILD_PAGE and getattr(block, "child_page", None)
25
+
26
+ @classmethod
27
+ async def markdown_to_notion(cls, text: str) -> Optional[BlockCreateResult]:
28
+ """
29
+ Convert markdown page syntax to an actual Notion page.
30
+ Returns None since child_page blocks are created implicitly via Pages API (not Blocks API).
31
+ """
32
+ context = get_page_context()
33
+
34
+ text = text.strip()
35
+
36
+ match = cls.PATTERN_BRACKET.match(text)
37
+ if not match:
38
+ match = cls.PATTERN_EMOJI.match(text)
39
+
40
+ if not match:
41
+ return None
42
+
43
+ title = match.group(1).strip()
44
+ if not title:
45
+ return None
46
+
47
+ # Reject multiline titles
48
+ if "\n" in title or "\r" in title:
49
+ return None
50
+
51
+ try:
52
+ # Create the actual page using context
53
+ await context.page_client.create_page(
54
+ title=title,
55
+ parent_page_id=context.page_id,
56
+ )
57
+ # Return None as per BaseBlockElement convention:
58
+ # child_page blocks cannot be written through the Blocks API directly.
59
+ # Creating a page under the parent page will automatically insert a child_page block.
60
+ return None
61
+
62
+ except Exception as e:
63
+ print(f"Failed to create page '{title}': {e}")
64
+ return None
65
+
66
+ @classmethod
67
+ async def notion_to_markdown(cls, block: Block) -> Optional[str]:
68
+ if block.type != BlockType.CHILD_PAGE or not getattr(block, "child_page", None):
69
+ return None
70
+
71
+ title = block.child_page.title
72
+ if not title or not title.strip():
73
+ return None
74
+
75
+ # Use bracket syntax for output
76
+ return f"[page: {title.strip()}]"
77
+
78
+ @classmethod
79
+ def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
80
+ """Get system prompt information for child page blocks."""
81
+ return BlockElementMarkdownInformation(
82
+ block_type=cls.__name__,
83
+ description="Creates new sub-pages within a Notion page.",
84
+ syntax_examples=[
85
+ "[page: Meeting Notes]",
86
+ "[page: Ideas]",
87
+ "📝 Project Overview",
88
+ "📄 Research Log",
89
+ ],
90
+ usage_guidelines=(
91
+ "Use to create new pages that will appear as child_page blocks in the current page. "
92
+ "Pages are created via the Pages API with the current page as parent."
93
+ ),
94
+ )
@@ -0,0 +1,12 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ChildPageBlock(BaseModel):
7
+ title: str
8
+
9
+
10
+ class CreateChildPageBlock(BaseModel):
11
+ type: Literal["child_page"] = "child_page"
12
+ child_page: ChildPageBlock