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,76 +1,51 @@
1
1
  from typing import Optional
2
2
 
3
- from notionary.blocks import BlockRegistry
4
- from notionary.blocks.shared.block_client import NotionBlockClient
5
- from notionary.models.notion_block_response import Block
6
- from notionary.page.content.markdown_whitespace_processor import (
7
- MarkdownWhitespaceProcessor,
8
- )
9
- from notionary.page.content.notion_text_length_utils import fix_blocks_content_length
10
- from notionary.page.formatting.markdown_to_notion_converter import (
11
- MarkdownToNotionConverter,
12
- )
13
-
3
+ from notionary.blocks.client import NotionBlockClient
4
+ from notionary.blocks.models import Block
5
+ from notionary.blocks.registry.block_registry import BlockRegistry
6
+ from notionary.page.reader.page_content_retriever import PageContentRetriever
14
7
  from notionary.util import LoggingMixin
15
8
 
16
9
 
17
- class PageContentWriter(LoggingMixin):
10
+ class PageContentDeletingService(LoggingMixin):
11
+ """Service responsible for deleting page content and blocks."""
12
+
18
13
  def __init__(self, page_id: str, block_registry: BlockRegistry):
19
14
  self.page_id = page_id
20
15
  self.block_registry = block_registry
21
16
  self._block_client = NotionBlockClient()
17
+ self._content_retriever = PageContentRetriever(block_registry=block_registry)
22
18
 
23
- self._markdown_to_notion_converter = MarkdownToNotionConverter(
24
- block_registry=block_registry
25
- )
26
-
27
- async def append_markdown(self, markdown_text: str, append_divider=True) -> bool:
28
- """Append markdown text to a Notion page, automatically handling content length limits."""
29
- if append_divider:
30
- markdown_text = markdown_text + "---\n"
31
-
32
- markdown_text = self._process_markdown_whitespace(markdown_text)
33
-
34
- try:
35
- blocks = self._markdown_to_notion_converter.convert(markdown_text)
36
-
37
- fixed_blocks = fix_blocks_content_length(blocks)
38
-
39
- result = await self._block_client.append_block_children(
40
- block_id=self.page_id, children=fixed_blocks
41
- )
42
- self.logger.debug("Append block children result: %r", result)
43
- return bool(result)
44
- except Exception as e:
45
- import traceback
46
-
47
- self.logger.error(
48
- "Error appending markdown: %s\nTraceback:\n%s",
49
- str(e),
50
- traceback.format_exc(),
51
- )
52
- return False
53
-
54
- async def clear_page_content(self) -> bool:
55
- """Clear all content of the page."""
19
+ async def clear_page_content(self) -> Optional[str]:
20
+ """Clear all content of the page and return deleted content as markdown."""
56
21
  try:
57
22
  children_response = await self._block_client.get_block_children(
58
23
  block_id=self.page_id
59
24
  )
60
25
 
61
26
  if not children_response or not children_response.results:
62
- return True
27
+ return None
28
+
29
+ # Use PageContentRetriever for sophisticated markdown conversion
30
+ deleted_content = self._content_retriever._convert_blocks_to_markdown(
31
+ children_response.results, indent_level=0
32
+ )
63
33
 
34
+ # Delete blocks
64
35
  success = True
65
36
  for block in children_response.results:
66
37
  block_success = await self._delete_block_with_children(block)
67
38
  if not block_success:
68
39
  success = False
69
40
 
70
- return success
71
- except Exception as e:
72
- self.logger.error("Error clearing page content: %s", str(e))
73
- return False
41
+ if not success:
42
+ self.logger.warning("Some blocks could not be deleted")
43
+
44
+ return deleted_content if deleted_content else None
45
+
46
+ except Exception:
47
+ self.logger.error("Error clearing page content", exc_info=True)
48
+ return None
74
49
 
75
50
  async def _delete_block_with_children(self, block: Block) -> bool:
76
51
  """Delete a block and all its children recursively."""
@@ -140,12 +115,3 @@ class PageContentWriter(LoggingMixin):
140
115
  else:
141
116
  self.logger.warning("Block %s was not properly archived/deleted", block.id)
142
117
  return False
143
-
144
- def _process_markdown_whitespace(self, markdown_text: str) -> str:
145
- """Process markdown text to normalize whitespace while preserving code blocks."""
146
- lines = markdown_text.split("\n")
147
- if not lines:
148
- return ""
149
-
150
- processor = MarkdownWhitespaceProcessor()
151
- return processor.process_lines(lines)
@@ -0,0 +1,177 @@
1
+ from typing import Callable, Optional, Union
2
+
3
+ from notionary.blocks.client import NotionBlockClient
4
+ from notionary.blocks.divider import DividerElement
5
+ from notionary.blocks.registry.block_registry import BlockRegistry
6
+ from notionary.blocks.table_of_contents import TableOfContentsElement
7
+ from notionary.markdown.markdown_builder import MarkdownBuilder
8
+ from notionary.page.writer.markdown_to_notion_converter import MarkdownToNotionConverter
9
+ from notionary.util import LoggingMixin
10
+
11
+
12
+ class PageContentWriter(LoggingMixin):
13
+ def __init__(self, page_id: str, block_registry: BlockRegistry):
14
+ self.page_id = page_id
15
+ self.block_registry = block_registry
16
+ self._block_client = NotionBlockClient()
17
+
18
+ self._markdown_to_notion_converter = MarkdownToNotionConverter(
19
+ block_registry=block_registry
20
+ )
21
+
22
+ async def append_markdown(
23
+ self,
24
+ content: Union[str, Callable[[MarkdownBuilder], MarkdownBuilder]],
25
+ *,
26
+ append_divider: bool = True,
27
+ prepend_table_of_contents: bool = False,
28
+ ) -> Optional[str]:
29
+ """
30
+ Append markdown content to a Notion page using either text or builder callback.
31
+ """
32
+
33
+ if isinstance(content, str):
34
+ final_markdown = content
35
+ elif callable(content):
36
+ builder = MarkdownBuilder()
37
+ content(builder)
38
+ final_markdown = builder.build()
39
+ else:
40
+ raise ValueError(
41
+ "content must be either a string or a callable that takes a MarkdownBuilder"
42
+ )
43
+
44
+ # Add optional components
45
+ if prepend_table_of_contents:
46
+ self._ensure_table_of_contents_exists_in_registry()
47
+ final_markdown = "[toc]\n\n" + final_markdown
48
+
49
+ if append_divider:
50
+ self._ensure_divider_exists_in_registry()
51
+ final_markdown = final_markdown + "\n\n---\n"
52
+
53
+ processed_markdown = self._process_markdown_whitespace(final_markdown)
54
+
55
+ try:
56
+ blocks = await self._markdown_to_notion_converter.convert(
57
+ processed_markdown
58
+ )
59
+
60
+ result = await self._block_client.append_block_children(
61
+ block_id=self.page_id, children=blocks
62
+ )
63
+
64
+ if result:
65
+ self.logger.debug("Successfully appended %d blocks", len(blocks))
66
+ return processed_markdown
67
+ else:
68
+ self.logger.error("Failed to append blocks")
69
+ return None
70
+
71
+ except Exception as e:
72
+ self.logger.error("Error appending markdown: %s", str(e), exc_info=True)
73
+ return None
74
+
75
+ def _process_markdown_whitespace(self, markdown_text: str) -> str:
76
+ """Process markdown text to normalize whitespace while preserving code blocks."""
77
+ lines = markdown_text.split("\n")
78
+ if not lines:
79
+ return ""
80
+
81
+ return self._process_whitespace_lines(lines)
82
+
83
+ def _process_whitespace_lines(self, lines: list[str]) -> str:
84
+ """Process all lines and return the processed markdown."""
85
+ processed_lines = []
86
+ in_code_block = False
87
+ current_code_block = []
88
+
89
+ for line in lines:
90
+ processed_lines, in_code_block, current_code_block = (
91
+ self._process_single_line(
92
+ line, processed_lines, in_code_block, current_code_block
93
+ )
94
+ )
95
+
96
+ return "\n".join(processed_lines)
97
+
98
+ def _process_single_line(
99
+ self,
100
+ line: str,
101
+ processed_lines: list[str],
102
+ in_code_block: bool,
103
+ current_code_block: list[str],
104
+ ) -> tuple[list[str], bool, list[str]]:
105
+ """Process a single line and return updated state."""
106
+ if self._is_code_block_marker(line):
107
+ return self._handle_code_block_marker(
108
+ line, processed_lines, in_code_block, current_code_block
109
+ )
110
+ if in_code_block:
111
+ current_code_block.append(line)
112
+ return processed_lines, in_code_block, current_code_block
113
+ else:
114
+ processed_lines.append(line.lstrip())
115
+ return processed_lines, in_code_block, current_code_block
116
+
117
+ def _handle_code_block_marker(
118
+ self,
119
+ line: str,
120
+ processed_lines: list[str],
121
+ in_code_block: bool,
122
+ current_code_block: list[str],
123
+ ) -> tuple[list[str], bool, list[str]]:
124
+ """Handle code block start/end markers."""
125
+ if not in_code_block:
126
+ return self._start_code_block(line, processed_lines)
127
+ else:
128
+ return self._end_code_block(processed_lines, current_code_block)
129
+
130
+ def _start_code_block(
131
+ self, line: str, processed_lines: list[str]
132
+ ) -> tuple[list[str], bool, list[str]]:
133
+ """Start a new code block."""
134
+ processed_lines.append(self._normalize_code_block_start(line))
135
+ return processed_lines, True, []
136
+
137
+ def _end_code_block(
138
+ self, processed_lines: list[str], current_code_block: list[str]
139
+ ) -> tuple[list[str], bool, list[str]]:
140
+ """End the current code block."""
141
+ processed_lines.extend(self._normalize_code_block_content(current_code_block))
142
+ processed_lines.append("```")
143
+ return processed_lines, False, []
144
+
145
+ def _is_code_block_marker(self, line: str) -> bool:
146
+ """Check if line is a code block marker."""
147
+ return line.lstrip().startswith("```")
148
+
149
+ def _normalize_code_block_start(self, line: str) -> str:
150
+ """Normalize code block opening marker."""
151
+ language = line.lstrip().replace("```", "", 1).strip()
152
+ return "```" + language
153
+
154
+ def _normalize_code_block_content(self, code_lines: list[str]) -> list[str]:
155
+ """Normalize code block indentation."""
156
+ if not code_lines:
157
+ return []
158
+
159
+ # Find minimum indentation from non-empty lines
160
+ non_empty_lines = [line for line in code_lines if line.strip()]
161
+ if not non_empty_lines:
162
+ return [""] * len(code_lines)
163
+
164
+ min_indent = min(len(line) - len(line.lstrip()) for line in non_empty_lines)
165
+ if min_indent == 0:
166
+ return code_lines
167
+
168
+ # Remove common indentation
169
+ return ["" if not line.strip() else line[min_indent:] for line in code_lines]
170
+
171
+ def _ensure_table_of_contents_exists_in_registry(self) -> None:
172
+ """Ensure TableOfContents is registered in the block registry."""
173
+ self.block_registry.register(TableOfContentsElement)
174
+
175
+ def _ensure_divider_exists_in_registry(self) -> None:
176
+ """Ensure DividerBlock is registered in the block registry."""
177
+ self.block_registry.register(DividerElement)
@@ -0,0 +1,65 @@
1
+ # notionary/blocks/context/page_context.py
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING, Optional
5
+ from dataclasses import dataclass
6
+ from contextvars import ContextVar
7
+
8
+ if TYPE_CHECKING:
9
+ from notionary.database.client import NotionDatabaseClient
10
+ from notionary.file_upload import NotionFileUploadClient
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class PageContextProvider:
15
+ """Context object that provides dependencies for block conversion operations."""
16
+
17
+ page_id: str
18
+ database_client: NotionDatabaseClient
19
+ file_upload_client: NotionFileUploadClient
20
+
21
+
22
+ # Context variable
23
+ _page_context: ContextVar[Optional[PageContextProvider]] = ContextVar(
24
+ "page_context", default=None
25
+ )
26
+
27
+
28
+ def get_page_context() -> PageContextProvider:
29
+ """Get current page context or raise if not available."""
30
+ context = _page_context.get()
31
+ if context is None:
32
+ raise RuntimeError(
33
+ "No page context available. Use 'async with page_context(...)'"
34
+ )
35
+ return context
36
+
37
+
38
+ def get_page_context_optional() -> Optional[PageContextProvider]:
39
+ """Get current page context or None if not available."""
40
+ return _page_context.get()
41
+
42
+
43
+ class page_context:
44
+ """Async-only context manager for page operations."""
45
+
46
+ def __init__(self, provider: PageContextProvider):
47
+ self.provider = provider
48
+ self._token = None
49
+
50
+ def _set_context(self) -> PageContextProvider:
51
+ """Helper to set context and return provider."""
52
+ self._token = _page_context.set(self.provider)
53
+ return self.provider
54
+
55
+ def _reset_context(self) -> None:
56
+ """Helper to reset context."""
57
+ if self._token is not None:
58
+ _page_context.reset(self._token)
59
+
60
+ async def __aenter__(self) -> PageContextProvider:
61
+ return self._set_context()
62
+
63
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
64
+ self._reset_context()
65
+ return False
@@ -0,0 +1,19 @@
1
+ from .base_block_renderer import BlockHandler
2
+ from .block_rendering_context import BlockRenderingContext
3
+ from .column_list_renderer import ColumnListRenderer
4
+ from .column_renderer import ColumnRenderer
5
+ from .line_renderer import LineRenderer
6
+ from .numbered_list_renderer import NumberedListRenderer
7
+ from .toggle_renderer import ToggleRenderer
8
+ from .toggleable_heading_renderer import ToggleableHeadingRenderer
9
+
10
+ __all__ = [
11
+ "BlockHandler",
12
+ "BlockRenderingContext",
13
+ "ColumnListRenderer",
14
+ "ColumnRenderer",
15
+ "LineRenderer",
16
+ "NumberedListRenderer",
17
+ "ToggleRenderer",
18
+ "ToggleableHeadingRenderer",
19
+ ]
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Optional
5
+
6
+ from notionary.page.reader.handler.block_rendering_context import BlockRenderingContext
7
+
8
+
9
+ class BlockHandler(ABC):
10
+ """Abstract base class for block handlers."""
11
+
12
+ def __init__(self):
13
+ self._next_handler: Optional[BlockHandler] = None
14
+
15
+ def set_next(self, handler: BlockHandler) -> BlockHandler:
16
+ """Set the next handler in the chain."""
17
+ self._next_handler = handler
18
+ return handler
19
+
20
+ async def handle(self, context: BlockRenderingContext) -> None:
21
+ """Handle the block or pass to next handler."""
22
+ if self._can_handle(context):
23
+ await self._process(context)
24
+ elif self._next_handler:
25
+ await self._next_handler.handle(context)
26
+
27
+ @abstractmethod
28
+ def _can_handle(self, context: BlockRenderingContext) -> bool:
29
+ """Check if this handler can process the current block."""
30
+ pass
31
+
32
+ @abstractmethod
33
+ async def _process(self, context: BlockRenderingContext) -> None:
34
+ """Process the block and update context."""
35
+ pass
36
+
37
+ def _indent_text(self, text: str, spaces: int = 4) -> str:
38
+ """Indent each line of text with specified number of spaces."""
39
+ if not text:
40
+ return text
41
+
42
+ indent = " " * spaces
43
+ lines = text.split("\n")
44
+ return "\n".join(f"{indent}{line}" if line.strip() else line for line in lines)
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Optional
5
+
6
+ from notionary.blocks.models import Block
7
+ from notionary.blocks.registry.block_registry import BlockRegistry
8
+
9
+
10
+ @dataclass
11
+ class BlockProcessingContext:
12
+ """Context for processing blocks during markdown conversion."""
13
+
14
+ block: Block
15
+ indent_level: int
16
+ block_registry: BlockRegistry
17
+
18
+ # Result
19
+ markdown_result: Optional[str] = None
20
+ children_result: Optional[str] = None
21
+ was_processed: bool = False
22
+
23
+ def has_children(self) -> bool:
24
+ """Check if block has children that need processing."""
25
+ return (
26
+ self.block.has_children
27
+ and self.block.children is not None
28
+ and len(self.block.children) > 0
29
+ )
30
+
31
+ def get_children_blocks(self) -> list[Block]:
32
+ """Get the children blocks safely."""
33
+ if self.has_children():
34
+ return self.block.children
35
+ return []
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Callable, Optional
5
+
6
+ from notionary.blocks.models import Block
7
+ from notionary.blocks.registry.block_registry import BlockRegistry
8
+
9
+
10
+ @dataclass
11
+ class BlockRenderingContext:
12
+ """Context for processing blocks during markdown conversion."""
13
+
14
+ block: Block
15
+ indent_level: int
16
+ block_registry: BlockRegistry
17
+ convert_children_callback: Optional[Callable[[list[Block], int], str]] = None
18
+
19
+ # For batch processing
20
+ all_blocks: Optional[list[Block]] = None
21
+ current_block_index: Optional[int] = None
22
+ blocks_consumed: int = 0
23
+
24
+ # Result
25
+ markdown_result: Optional[str] = None
26
+ children_result: Optional[str] = None
27
+ was_processed: bool = False
28
+
29
+ def has_children(self) -> bool:
30
+ """Check if block has children that need processing."""
31
+ return (
32
+ self.block.has_children
33
+ and self.block.children is not None
34
+ and len(self.block.children) > 0
35
+ )
36
+
37
+ def get_children_blocks(self) -> list[Block]:
38
+ """Get the children blocks safely."""
39
+ if self.has_children():
40
+ return self.block.children
41
+ return []
42
+
43
+ def convert_children_to_markdown(self, indent_level: int = 0) -> str:
44
+ """Convert children blocks to markdown using the callback."""
45
+ if not self.has_children() or not self.convert_children_callback:
46
+ return ""
47
+
48
+ return self.convert_children_callback(self.get_children_blocks(), indent_level)
@@ -0,0 +1,51 @@
1
+ from notionary.blocks.column.column_list_element import ColumnListElement
2
+ from notionary.page.reader.handler import BlockHandler, BlockRenderingContext
3
+
4
+
5
+ class ColumnListRenderer(BlockHandler):
6
+ """Handles column list blocks with their column children."""
7
+
8
+ def _can_handle(self, context: BlockRenderingContext) -> bool:
9
+ return ColumnListElement.match_notion(context.block)
10
+
11
+ def _process(self, context: BlockRenderingContext) -> None:
12
+ # Create column list start line
13
+ column_list_start = "::: columns"
14
+
15
+ # Apply indentation if needed
16
+ if context.indent_level > 0:
17
+ column_list_start = self._indent_text(
18
+ column_list_start, spaces=context.indent_level * 4
19
+ )
20
+
21
+ # Process children if they exist
22
+ children_markdown = ""
23
+ if context.has_children():
24
+ # Import here to avoid circular dependency
25
+ from notionary.page.reader.page_content_retriever import (
26
+ PageContentRetriever,
27
+ )
28
+
29
+ # Create a temporary retriever to process children
30
+ retriever = PageContentRetriever(context.block_registry)
31
+ children_markdown = retriever._convert_blocks_to_markdown(
32
+ context.get_children_blocks(),
33
+ indent_level=0, # No indentation for content inside column lists
34
+ )
35
+
36
+ # Create column list end line
37
+ column_list_end = ":::"
38
+ if context.indent_level > 0:
39
+ column_list_end = self._indent_text(
40
+ column_list_end, spaces=context.indent_level * 4
41
+ )
42
+
43
+ # Combine column list with children content
44
+ if children_markdown:
45
+ context.markdown_result = (
46
+ f"{column_list_start}\n{children_markdown}\n{column_list_end}"
47
+ )
48
+ else:
49
+ context.markdown_result = f"{column_list_start}\n{column_list_end}"
50
+
51
+ context.was_processed = True
@@ -0,0 +1,60 @@
1
+ from notionary.blocks.column.column_element import ColumnElement
2
+ from notionary.page.reader.handler import BlockHandler, BlockRenderingContext
3
+
4
+
5
+ class ColumnRenderer(BlockHandler):
6
+ """Handles individual column blocks with their children content."""
7
+
8
+ def _can_handle(self, context: BlockRenderingContext) -> bool:
9
+ return ColumnElement.match_notion(context.block)
10
+
11
+ def _process(self, context: BlockRenderingContext) -> None:
12
+ # Get the column start line with potential width ratio
13
+ column_start = self._extract_column_start(context.block)
14
+
15
+ # Apply indentation if needed
16
+ if context.indent_level > 0:
17
+ column_start = self._indent_text(
18
+ column_start, spaces=context.indent_level * 4
19
+ )
20
+
21
+ # Process children if they exist
22
+ children_markdown = ""
23
+ if context.has_children():
24
+ # Import here to avoid circular dependency
25
+ from notionary.page.reader.page_content_retriever import (
26
+ PageContentRetriever,
27
+ )
28
+
29
+ # Create a temporary retriever to process children
30
+ retriever = PageContentRetriever(context.block_registry)
31
+ children_markdown = retriever._convert_blocks_to_markdown(
32
+ context.get_children_blocks(),
33
+ indent_level=0, # No indentation for content inside columns
34
+ )
35
+
36
+ # Create column end line
37
+ column_end = ":::"
38
+ if context.indent_level > 0:
39
+ column_end = self._indent_text(column_end, spaces=context.indent_level * 4)
40
+
41
+ # Combine column with children content
42
+ if children_markdown:
43
+ context.markdown_result = (
44
+ f"{column_start}\n{children_markdown}\n{column_end}"
45
+ )
46
+ else:
47
+ context.markdown_result = f"{column_start}\n{column_end}"
48
+
49
+ context.was_processed = True
50
+
51
+ def _extract_column_start(self, block) -> str:
52
+ """Extract column start line with potential width ratio."""
53
+ if not block.column:
54
+ return "::: column"
55
+
56
+ width_ratio = block.column.width_ratio
57
+ if width_ratio:
58
+ return f"::: column {width_ratio}"
59
+ else:
60
+ return "::: column"