notionary 0.2.18__py3-none-any.whl → 0.2.21__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 (204) 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 +263 -0
  5. notionary/blocks/audio/__init__.py +8 -2
  6. notionary/blocks/audio/audio_element.py +42 -104
  7. notionary/blocks/audio/audio_markdown_node.py +3 -1
  8. notionary/blocks/audio/audio_models.py +6 -55
  9. notionary/blocks/base_block_element.py +30 -0
  10. notionary/blocks/bookmark/__init__.py +9 -2
  11. notionary/blocks/bookmark/bookmark_element.py +46 -139
  12. notionary/blocks/bookmark/bookmark_markdown_node.py +3 -1
  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 +40 -55
  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 +40 -89
  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 +7 -0
  27. notionary/blocks/child_database/child_database_models.py +19 -0
  28. notionary/blocks/child_page/__init__.py +9 -0
  29. notionary/blocks/child_page/child_page_models.py +12 -0
  30. notionary/blocks/{shared/block_client.py → client.py} +55 -54
  31. notionary/blocks/code/__init__.py +6 -2
  32. notionary/blocks/code/code_element.py +53 -187
  33. notionary/blocks/code/code_markdown_node.py +13 -13
  34. notionary/blocks/code/code_models.py +94 -0
  35. notionary/blocks/column/__init__.py +25 -1
  36. notionary/blocks/column/column_element.py +40 -314
  37. notionary/blocks/column/column_list_element.py +37 -0
  38. notionary/blocks/column/column_list_markdown_node.py +50 -0
  39. notionary/blocks/column/column_markdown_node.py +59 -0
  40. notionary/blocks/column/column_models.py +26 -0
  41. notionary/blocks/divider/__init__.py +9 -2
  42. notionary/blocks/divider/divider_element.py +26 -49
  43. notionary/blocks/divider/divider_markdown_node.py +2 -1
  44. notionary/blocks/divider/divider_models.py +12 -0
  45. notionary/blocks/embed/__init__.py +9 -2
  46. notionary/blocks/embed/embed_element.py +47 -114
  47. notionary/blocks/embed/embed_markdown_node.py +3 -1
  48. notionary/blocks/embed/embed_models.py +14 -0
  49. notionary/blocks/equation/__init__.py +14 -0
  50. notionary/blocks/equation/equation_element.py +80 -0
  51. notionary/blocks/equation/equation_element_markdown_node.py +36 -0
  52. notionary/blocks/equation/equation_models.py +11 -0
  53. notionary/blocks/file/__init__.py +25 -0
  54. notionary/blocks/file/file_element.py +93 -0
  55. notionary/blocks/file/file_element_markdown_node.py +35 -0
  56. notionary/blocks/file/file_element_models.py +39 -0
  57. notionary/blocks/heading/__init__.py +16 -2
  58. notionary/blocks/heading/heading_element.py +67 -72
  59. notionary/blocks/heading/heading_markdown_node.py +2 -1
  60. notionary/blocks/heading/heading_models.py +29 -0
  61. notionary/blocks/image_block/__init__.py +13 -0
  62. notionary/blocks/image_block/image_element.py +84 -0
  63. notionary/blocks/{image → image_block}/image_markdown_node.py +3 -1
  64. notionary/blocks/image_block/image_models.py +10 -0
  65. notionary/blocks/models.py +172 -0
  66. notionary/blocks/numbered_list/__init__.py +12 -2
  67. notionary/blocks/numbered_list/numbered_list_element.py +33 -58
  68. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
  69. notionary/blocks/numbered_list/numbered_list_models.py +17 -0
  70. notionary/blocks/paragraph/__init__.py +12 -2
  71. notionary/blocks/paragraph/paragraph_element.py +27 -69
  72. notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
  73. notionary/blocks/paragraph/paragraph_models.py +16 -0
  74. notionary/blocks/pdf/__init__.py +13 -0
  75. notionary/blocks/pdf/pdf_element.py +91 -0
  76. notionary/blocks/pdf/pdf_markdown_node.py +35 -0
  77. notionary/blocks/pdf/pdf_models.py +11 -0
  78. notionary/blocks/quote/__init__.py +11 -2
  79. notionary/blocks/quote/quote_element.py +31 -65
  80. notionary/blocks/quote/quote_markdown_node.py +4 -1
  81. notionary/blocks/quote/quote_models.py +18 -0
  82. notionary/blocks/registry/__init__.py +4 -0
  83. notionary/blocks/registry/block_registry.py +75 -91
  84. notionary/blocks/registry/block_registry_builder.py +107 -59
  85. notionary/blocks/rich_text/__init__.py +33 -0
  86. notionary/blocks/rich_text/rich_text_models.py +188 -0
  87. notionary/blocks/rich_text/text_inline_formatter.py +125 -0
  88. notionary/blocks/table/__init__.py +16 -2
  89. notionary/blocks/table/table_element.py +48 -241
  90. notionary/blocks/table/table_markdown_node.py +2 -1
  91. notionary/blocks/table/table_models.py +28 -0
  92. notionary/blocks/table_of_contents/__init__.py +19 -0
  93. notionary/blocks/table_of_contents/table_of_contents_element.py +51 -0
  94. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
  95. notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
  96. notionary/blocks/todo/__init__.py +9 -2
  97. notionary/blocks/todo/todo_element.py +38 -95
  98. notionary/blocks/todo/todo_markdown_node.py +2 -1
  99. notionary/blocks/todo/todo_models.py +19 -0
  100. notionary/blocks/toggle/__init__.py +13 -3
  101. notionary/blocks/toggle/toggle_element.py +57 -264
  102. notionary/blocks/toggle/toggle_markdown_node.py +24 -14
  103. notionary/blocks/toggle/toggle_models.py +17 -0
  104. notionary/blocks/toggleable_heading/__init__.py +6 -2
  105. notionary/blocks/toggleable_heading/toggleable_heading_element.py +74 -244
  106. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
  107. notionary/blocks/types.py +61 -0
  108. notionary/blocks/video/__init__.py +8 -2
  109. notionary/blocks/video/video_element.py +67 -143
  110. notionary/blocks/video/video_element_models.py +10 -0
  111. notionary/blocks/video/video_markdown_node.py +3 -1
  112. notionary/database/client.py +3 -8
  113. notionary/database/database.py +13 -14
  114. notionary/database/database_filter_builder.py +2 -2
  115. notionary/database/database_provider.py +5 -4
  116. notionary/database/models.py +337 -0
  117. notionary/database/notion_database.py +6 -7
  118. notionary/file_upload/client.py +5 -7
  119. notionary/file_upload/models.py +2 -1
  120. notionary/file_upload/notion_file_upload.py +2 -3
  121. notionary/markdown/markdown_builder.py +722 -0
  122. notionary/markdown/markdown_document_model.py +228 -0
  123. notionary/{blocks → markdown}/markdown_node.py +1 -0
  124. notionary/models/notion_database_response.py +0 -338
  125. notionary/page/client.py +9 -10
  126. notionary/page/models.py +327 -0
  127. notionary/page/notion_page.py +99 -52
  128. notionary/page/notion_text_length_utils.py +119 -0
  129. notionary/page/{content/page_content_writer.py → page_content_writer.py} +88 -38
  130. notionary/page/reader/handler/__init__.py +17 -0
  131. notionary/page/reader/handler/base_block_renderer.py +44 -0
  132. notionary/page/reader/handler/block_processing_context.py +35 -0
  133. notionary/page/reader/handler/block_rendering_context.py +43 -0
  134. notionary/page/reader/handler/column_list_renderer.py +51 -0
  135. notionary/page/reader/handler/column_renderer.py +60 -0
  136. notionary/page/reader/handler/line_renderer.py +60 -0
  137. notionary/page/reader/handler/toggle_renderer.py +69 -0
  138. notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
  139. notionary/page/reader/page_content_retriever.py +69 -0
  140. notionary/page/search_filter_builder.py +2 -1
  141. notionary/page/writer/handler/__init__.py +22 -0
  142. notionary/page/writer/handler/code_handler.py +100 -0
  143. notionary/page/writer/handler/column_handler.py +141 -0
  144. notionary/page/writer/handler/column_list_handler.py +139 -0
  145. notionary/page/writer/handler/line_handler.py +35 -0
  146. notionary/page/writer/handler/line_processing_context.py +54 -0
  147. notionary/page/writer/handler/regular_line_handler.py +92 -0
  148. notionary/page/writer/handler/table_handler.py +130 -0
  149. notionary/page/writer/handler/toggle_handler.py +153 -0
  150. notionary/page/writer/handler/toggleable_heading_handler.py +167 -0
  151. notionary/page/writer/markdown_to_notion_converter.py +76 -0
  152. notionary/telemetry/__init__.py +2 -2
  153. notionary/telemetry/service.py +4 -3
  154. notionary/user/__init__.py +2 -2
  155. notionary/user/base_notion_user.py +2 -1
  156. notionary/user/client.py +2 -3
  157. notionary/user/models.py +1 -0
  158. notionary/user/notion_bot_user.py +4 -5
  159. notionary/user/notion_user.py +3 -4
  160. notionary/user/notion_user_manager.py +3 -2
  161. notionary/user/notion_user_provider.py +1 -1
  162. notionary/util/__init__.py +3 -2
  163. notionary/util/fuzzy.py +2 -1
  164. notionary/util/logging_mixin.py +2 -2
  165. notionary/util/singleton_metaclass.py +1 -1
  166. notionary/workspace.py +3 -2
  167. {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/METADATA +12 -8
  168. notionary-0.2.21.dist-info/RECORD +185 -0
  169. notionary/blocks/document/__init__.py +0 -7
  170. notionary/blocks/document/document_element.py +0 -102
  171. notionary/blocks/document/document_markdown_node.py +0 -31
  172. notionary/blocks/image/__init__.py +0 -7
  173. notionary/blocks/image/image_element.py +0 -151
  174. notionary/blocks/markdown_builder.py +0 -356
  175. notionary/blocks/mention/__init__.py +0 -7
  176. notionary/blocks/mention/mention_element.py +0 -229
  177. notionary/blocks/mention/mention_markdown_node.py +0 -38
  178. notionary/blocks/prompts/element_prompt_builder.py +0 -83
  179. notionary/blocks/prompts/element_prompt_content.py +0 -41
  180. notionary/blocks/shared/__init__.py +0 -0
  181. notionary/blocks/shared/models.py +0 -710
  182. notionary/blocks/shared/notion_block_element.py +0 -37
  183. notionary/blocks/shared/text_inline_formatter.py +0 -262
  184. notionary/blocks/shared/text_inline_formatter_new.py +0 -139
  185. notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
  186. notionary/database/models/page_result.py +0 -10
  187. notionary/models/notion_block_response.py +0 -264
  188. notionary/models/notion_page_response.py +0 -78
  189. notionary/models/search_response.py +0 -0
  190. notionary/page/__init__.py +0 -0
  191. notionary/page/content/notion_text_length_utils.py +0 -87
  192. notionary/page/content/page_content_retriever.py +0 -52
  193. notionary/page/formatting/line_processor.py +0 -153
  194. notionary/page/formatting/markdown_to_notion_converter.py +0 -153
  195. notionary/page/markdown_syntax_prompt_generator.py +0 -114
  196. notionary/page/notion_to_markdown_converter.py +0 -179
  197. notionary/page/properites/property_value_extractor.py +0 -0
  198. notionary-0.2.18.dist-info/RECORD +0 -149
  199. /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
  200. /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
  201. /notionary/page/{content/markdown_whitespace_processor.py → markdown_whitespace_processor.py} +0 -0
  202. /notionary/{blocks/mention/mention_models.py → page/reader/handler/context.py} +0 -0
  203. {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/LICENSE +0 -0
  204. {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/WHEEL +0 -0
@@ -1,98 +1,93 @@
1
- import re
2
- from typing import Dict, Any, Optional
1
+ from __future__ import annotations
3
2
 
4
- from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import (
6
- ElementPromptContent,
7
- ElementPromptBuilder,
8
- NotionBlockResult,
3
+ import re
4
+ from typing import Optional, cast
5
+
6
+ from notionary.blocks.base_block_element import BaseBlockElement
7
+ from notionary.blocks.heading.heading_models import (
8
+ CreateHeading1Block,
9
+ CreateHeading2Block,
10
+ CreateHeading3Block,
11
+ HeadingBlock,
9
12
  )
10
- from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
13
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
14
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
15
+ from notionary.blocks.types import BlockColor
11
16
 
12
17
 
13
- class HeadingElement(NotionBlockElement):
18
+ class HeadingElement(BaseBlockElement):
14
19
  """Handles conversion between Markdown headings and Notion heading blocks."""
15
20
 
16
- # Pattern: #, ## oder ###, dann mind. 1 Leerzeichen/Tab, dann mind. 1 sichtbares Zeichen (kein Whitespace-only)
17
21
  PATTERN = re.compile(r"^(#{1,3})[ \t]+(.+)$")
18
22
 
19
23
  @classmethod
20
- def match_markdown(cls, text: str) -> bool:
21
- """Check if text is a markdown heading with non-empty content."""
22
- match = cls.PATTERN.match(text)
23
- if not match:
24
- return False
25
- content = match.group(2)
26
- return bool(content.strip()) # Reject headings with only whitespace
27
-
28
- @classmethod
29
- def match_notion(cls, block: Dict[str, Any]) -> bool:
30
- """Check if block is a Notion heading."""
31
- block_type: str = block.get("type", "")
32
- return block_type.startswith("heading_") and block_type[-1] in "123"
24
+ def match_notion(cls, block: Block) -> bool:
25
+ return (
26
+ block.type
27
+ in (
28
+ BlockType.HEADING_1,
29
+ BlockType.HEADING_2,
30
+ BlockType.HEADING_3,
31
+ )
32
+ and getattr(block, block.type.value) is not None
33
+ )
33
34
 
34
35
  @classmethod
35
- def markdown_to_notion(cls, text: str) -> NotionBlockResult:
36
- """Convert markdown heading to Notion heading block with preceding empty paragraph."""
37
- match = cls.PATTERN.match(text)
36
+ def markdown_to_notion(cls, text: str) -> BlockCreateResult:
37
+ """Convert markdown headings (#, ##, ###) to Notion HeadingBlock."""
38
+ match = cls.PATTERN.match(text.strip())
38
39
  if not match:
39
40
  return None
40
41
 
41
42
  level = len(match.group(1))
42
- if not 1 <= level <= 3:
43
+ if level < 1 or level > 3:
44
+ return None
45
+
46
+ content = match.group(2).strip()
47
+ if not content:
43
48
  return None
44
49
 
45
- content = match.group(2).lstrip() # Entferne führende Leerzeichen im Content
46
- if not content.strip():
47
- return None # Leerer Inhalt nach Entfernen der Whitespaces
50
+ rich_text = TextInlineFormatter.parse_inline_formatting(content)
51
+ heading_content = HeadingBlock(
52
+ rich_text=rich_text, color=BlockColor.DEFAULT, is_toggleable=False
53
+ )
48
54
 
49
- header_block = {
50
- "type": f"heading_{level}",
51
- f"heading_{level}": {
52
- "rich_text": TextInlineFormatter.parse_inline_formatting(content)
53
- },
54
- }
55
- return [header_block]
55
+ if level == 1:
56
+ return CreateHeading1Block(heading_1=heading_content)
57
+ elif level == 2:
58
+ return CreateHeading2Block(heading_2=heading_content)
59
+ else:
60
+ return CreateHeading3Block(heading_3=heading_content)
56
61
 
57
62
  @classmethod
58
- def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
59
- """Convert Notion heading block to markdown heading."""
60
- block_type = block.get("type", "")
61
-
62
- if not block_type.startswith("heading_"):
63
+ def notion_to_markdown(cls, block: Block) -> Optional[str]:
64
+ # Only handle heading blocks via BlockType enum
65
+ if block.type not in (
66
+ BlockType.HEADING_1,
67
+ BlockType.HEADING_2,
68
+ BlockType.HEADING_3,
69
+ ):
63
70
  return None
64
71
 
65
- try:
66
- level = int(block_type[-1])
67
- if not 1 <= level <= 3:
68
- return None
69
- except ValueError:
70
- return None
72
+ # Determine heading level from enum
73
+ if block.type == BlockType.HEADING_1:
74
+ level = 1
75
+ elif block.type == BlockType.HEADING_2:
76
+ level = 2
77
+ else:
78
+ level = 3
71
79
 
72
- heading_data = block.get(block_type, {})
73
- rich_text = heading_data.get("rich_text", [])
80
+ heading_obj = getattr(block, block.type.value)
81
+ if not heading_obj:
82
+ return None
74
83
 
75
- text = TextInlineFormatter.extract_text_with_formatting(rich_text)
76
- prefix = "#" * level
77
- return f"{prefix} {text}" if text else None
84
+ heading_data = cast(HeadingBlock, heading_obj)
85
+ if not heading_data.rich_text:
86
+ return None
78
87
 
79
- @classmethod
80
- def is_multiline(cls) -> bool:
81
- return False
88
+ text = TextInlineFormatter.extract_text_with_formatting(heading_data.rich_text)
89
+ if not text:
90
+ return None
82
91
 
83
- @classmethod
84
- def get_llm_prompt_content(cls) -> ElementPromptContent:
85
- return (
86
- ElementPromptBuilder()
87
- .with_description(
88
- "Use Markdown headings (#, ##, ###) to structure content hierarchically."
89
- )
90
- .with_usage_guidelines(
91
- "Use to group content into sections and define a visual hierarchy."
92
- )
93
- .with_avoidance_guidelines(
94
- "Only H1-H3 syntax is supported. H4 and deeper heading levels are not available."
95
- )
96
- .with_standard_markdown()
97
- .build()
98
- )
92
+ # Use hash-style for all heading levels
93
+ return f"{('#' * level)} {text}"
@@ -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 HeadingMarkdownBlockParams(BaseModel):
@@ -0,0 +1,29 @@
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.rich_text_models import RichTextObject
7
+ from notionary.blocks.types import BlockColor
8
+
9
+
10
+ class HeadingBlock(BaseModel):
11
+ rich_text: list[RichTextObject]
12
+ color: BlockColor = BlockColor.DEFAULT
13
+ is_toggleable: bool = False
14
+ children: list[Block] = Field(default_factory=list)
15
+
16
+
17
+ class CreateHeading1Block(BaseModel):
18
+ type: Literal["heading_1"] = "heading_1"
19
+ heading_1: HeadingBlock
20
+
21
+
22
+ class CreateHeading2Block(BaseModel):
23
+ type: Literal["heading_2"] = "heading_2"
24
+ heading_2: HeadingBlock
25
+
26
+
27
+ class CreateHeading3Block(BaseModel):
28
+ type: Literal["heading_3"] = "heading_3"
29
+ heading_3: HeadingBlock
@@ -0,0 +1,13 @@
1
+ from notionary.blocks.image_block.image_element import ImageElement
2
+ from notionary.blocks.image_block.image_markdown_node import (
3
+ ImageMarkdownBlockParams,
4
+ ImageMarkdownNode,
5
+ )
6
+ from notionary.blocks.image_block.image_models import CreateImageBlock
7
+
8
+ __all__ = [
9
+ "ImageElement",
10
+ "CreateImageBlock",
11
+ "ImageMarkdownNode",
12
+ "ImageMarkdownBlockParams",
13
+ ]
@@ -0,0 +1,84 @@
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.file.file_element_models import ExternalFile, FileType
8
+ from notionary.blocks.image_block.image_models import CreateImageBlock, FileBlock
9
+ from notionary.blocks.models import Block, BlockCreateResult, BlockType
10
+ from notionary.blocks.paragraph.paragraph_models import (
11
+ CreateParagraphBlock,
12
+ ParagraphBlock,
13
+ )
14
+ from notionary.blocks.rich_text.rich_text_models import RichTextObject
15
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
16
+
17
+
18
+ class ImageElement(BaseBlockElement):
19
+ """
20
+ Handles conversion between Markdown images and Notion image blocks.
21
+
22
+ Markdown image syntax:
23
+ - [image](https://example.com/image.jpg) - URL only
24
+ - [image](https://example.com/image.jpg "Caption") - URL + caption
25
+ """
26
+
27
+ PATTERN = re.compile(
28
+ r"^\[image\]\(" # prefix
29
+ r"(https?://[^\s\"]+)" # URL (exclude whitespace and ")
30
+ r"(?:\s+\"([^\"]+)\")?" # optional caption
31
+ r"\)$"
32
+ )
33
+
34
+ @classmethod
35
+ def match_notion(cls, block: Block) -> bool:
36
+ return block.type == BlockType.IMAGE and block.image
37
+
38
+ @classmethod
39
+ def markdown_to_notion(cls, text: str) -> BlockCreateResult:
40
+ """Convert markdown image syntax to Notion ImageBlock followed by an empty paragraph."""
41
+ match = cls.PATTERN.match(text.strip())
42
+ if not match:
43
+ return None
44
+
45
+ url, caption_text = match.group(1), match.group(2) or ""
46
+ # Build ImageBlock
47
+ image_block = FileBlock(
48
+ type="external", external=ExternalFile(url=url), caption=[]
49
+ )
50
+ if caption_text.strip():
51
+ rt = RichTextObject.from_plain_text(caption_text.strip())
52
+ image_block.caption = [rt]
53
+
54
+ empty_para = ParagraphBlock(rich_text=[])
55
+
56
+ return [
57
+ CreateImageBlock(image=image_block),
58
+ CreateParagraphBlock(paragraph=empty_para),
59
+ ]
60
+
61
+ @classmethod
62
+ def notion_to_markdown(cls, block: Block) -> Optional[str]:
63
+ if block.type != BlockType.IMAGE or not block.image:
64
+ return None
65
+
66
+ fo = block.image
67
+
68
+ if fo.type == FileType.EXTERNAL and fo.external:
69
+ url = fo.external.url
70
+ elif fo.type == FileType.FILE and fo.file:
71
+ url = fo.file.url
72
+ else:
73
+ return None
74
+
75
+ captions = fo.caption or []
76
+ if not captions:
77
+ return f"[image]({url})"
78
+
79
+ caption_text = "".join(
80
+ (rt.plain_text or TextInlineFormatter.extract_text_with_formatting([rt]))
81
+ for rt in captions
82
+ )
83
+
84
+ return f'[image]({url} "{caption_text}")'
@@ -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 ImageMarkdownBlockParams(BaseModel):
@@ -0,0 +1,10 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.blocks.file.file_element_models import FileBlock
6
+
7
+
8
+ class CreateImageBlock(BaseModel):
9
+ type: Literal["image"] = "image"
10
+ image: FileBlock
@@ -0,0 +1,172 @@
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
+
52
+
53
+ class BlockChildrenResponse(BaseModel):
54
+ object: Literal["list"]
55
+ results: list["Block"]
56
+ next_cursor: Optional[str] = None
57
+ has_more: bool
58
+ type: Literal["block"]
59
+ block: dict = {}
60
+ request_id: str
61
+
62
+
63
+ class PageParent(BaseModel):
64
+ type: Literal["page_id"]
65
+ page_id: str
66
+
67
+
68
+ class DatabaseParent(BaseModel):
69
+ type: Literal["database_id"]
70
+ database_id: str
71
+
72
+
73
+ class BlockParent(BaseModel):
74
+ type: Literal["block_id"]
75
+ block_id: str
76
+
77
+
78
+ class WorkspaceParent(BaseModel):
79
+ type: Literal["workspace"]
80
+ workspace: bool = True
81
+
82
+
83
+ ParentObject = Union[PageParent, DatabaseParent, BlockParent, WorkspaceParent]
84
+
85
+
86
+ class PartialUser(BaseModel):
87
+ object: Literal["user"]
88
+ id: str
89
+
90
+
91
+ class Block(BaseModel):
92
+ object: Literal["block"]
93
+ id: str
94
+ parent: Optional[ParentObject] = None
95
+ type: BlockType
96
+ created_time: str
97
+ last_edited_time: str
98
+ created_by: PartialUser
99
+ last_edited_by: PartialUser
100
+ archived: bool = False
101
+ in_trash: bool = False
102
+ has_children: bool = False
103
+
104
+ children: Optional[list[Block]] = None
105
+
106
+ # Block type-specific content (only one will be populated based on type)
107
+ audio: Optional[FileBlock] = None
108
+ bookmark: Optional[BookmarkBlock] = None
109
+ breadcrumb: Optional[BreadcrumbBlock] = None
110
+ bulleted_list_item: Optional[BulletedListItemBlock] = None
111
+ callout: Optional[CalloutBlock] = None
112
+ child_page: Optional[ChildPageBlock] = None
113
+ code: Optional[CodeBlock] = None
114
+ column_list: Optional[ColumnListBlock] = None
115
+ column: Optional[ColumnBlock] = None
116
+ divider: Optional[DividerBlock] = None
117
+ embed: Optional[EmbedBlock] = None
118
+ equation: Optional[EquationBlock] = None
119
+ file: Optional[FileBlock] = None
120
+ heading_1: Optional[HeadingBlock] = None
121
+ heading_2: Optional[HeadingBlock] = None
122
+ heading_3: Optional[HeadingBlock] = None
123
+ image: Optional[FileBlock] = None
124
+ numbered_list_item: Optional[NumberedListItemBlock] = None
125
+ paragraph: Optional[ParagraphBlock] = None
126
+ quote: Optional[QuoteBlock] = None
127
+ table: Optional[TableBlock] = None
128
+ table_row: Optional[TableRowBlock] = None
129
+ to_do: Optional[ToDoBlock] = None
130
+ toggle: Optional[ToggleBlock] = None
131
+ video: Optional[FileBlock] = None
132
+ pdf: Optional[FileBlock] = None
133
+ table_of_contents: Optional[TableOfContentsBlock] = None
134
+
135
+ def get_block_content(self) -> Optional[Any]:
136
+ """Get the content object for this block based on its type."""
137
+ return getattr(self, self.type, None)
138
+
139
+
140
+ if TYPE_CHECKING:
141
+ BlockCreateRequest = Union[
142
+ CreateBookmarkBlock,
143
+ CreateBreadcrumbBlock,
144
+ CreateBulletedListItemBlock,
145
+ CreateCalloutBlock,
146
+ CreateChildPageBlock,
147
+ CreateCodeBlock,
148
+ CreateColumnListBlock,
149
+ CreateColumnBlock,
150
+ CreateDividerBlock,
151
+ CreateEmbedBlock,
152
+ CreateEquationBlock,
153
+ CreateFileBlock,
154
+ CreateHeading1Block,
155
+ CreateHeading2Block,
156
+ CreateHeading3Block,
157
+ CreateImageBlock,
158
+ CreateNumberedListItemBlock,
159
+ CreateParagraphBlock,
160
+ CreateQuoteBlock,
161
+ CreateToDoBlock,
162
+ CreateToggleBlock,
163
+ CreateVideoBlock,
164
+ CreateTableOfContentsBlock,
165
+ CreatePdfBlock,
166
+ CreateTableBlock,
167
+ ]
168
+ BlockCreateResult = Optional[Union[list[BlockCreateRequest], BlockCreateRequest]]
169
+ else:
170
+ # at runtime there are no typings anyway
171
+ BlockCreateRequest = Any
172
+ 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,48 @@
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.models import Block, BlockCreateResult, BlockType
8
+ from notionary.blocks.numbered_list.numbered_list_models import (
9
+ CreateNumberedListItemBlock,
10
+ NumberedListItemBlock,
8
11
  )
9
- from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
12
+ from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
13
+ from notionary.blocks.types import BlockColor
14
+
10
15
 
16
+ class NumberedListElement(BaseBlockElement):
17
+ """Converts between Markdown numbered lists and Notion numbered list items."""
11
18
 
12
- class NumberedListElement(NotionBlockElement):
13
- """Class for converting between Markdown numbered lists and Notion numbered list items."""
19
+ PATTERN = re.compile(r"^\s*(\d+)\.\s+(.+)$")
14
20
 
15
21
  @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
22
+ def match_notion(cls, block: Block) -> bool:
23
+ return block.type == BlockType.NUMBERED_LIST_ITEM and block.numbered_list_item
22
24
 
23
- content = numbered_match.group(2)
25
+ @classmethod
26
+ def markdown_to_notion(cls, text: str) -> BlockCreateResult:
27
+ """Convert markdown numbered list item to Notion NumberedListItemBlock."""
28
+ match = cls.PATTERN.match(text.strip())
29
+ if not match:
30
+ return None
24
31
 
25
- # Use parse_inline_formatting to handle rich text
32
+ content = match.group(2)
26
33
  rich_text = TextInlineFormatter.parse_inline_formatting(content)
27
34
 
28
- return {
29
- "type": "numbered_list_item",
30
- "numbered_list_item": {"rich_text": rich_text, "color": "default"},
31
- }
35
+ numbered_list_content = NumberedListItemBlock(
36
+ rich_text=rich_text, color=BlockColor.DEFAULT
37
+ )
38
+ return CreateNumberedListItemBlock(numbered_list_item=numbered_list_content)
32
39
 
40
+ # FIX: Roundtrip conversions will never work this way here
33
41
  @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":
42
+ def notion_to_markdown(cls, block: Block) -> Optional[str]:
43
+ if block.type != BlockType.NUMBERED_LIST_ITEM or not block.numbered_list_item:
37
44
  return None
38
45
 
39
- rich_text = block.get("numbered_list_item", {}).get("rich_text", [])
40
- content = TextInlineFormatter.extract_text_with_formatting(rich_text)
41
-
46
+ rich = block.numbered_list_item.rich_text
47
+ content = TextInlineFormatter.extract_text_with_formatting(rich)
42
48
  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))
49
-
50
- @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"
54
-
55
- @classmethod
56
- def is_multiline(cls) -> bool:
57
- return False
58
-
59
- @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()
73
- )
@@ -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