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.
- notionary/__init__.py +8 -4
- notionary/base_notion_client.py +3 -1
- notionary/blocks/__init__.py +2 -91
- notionary/blocks/_bootstrap.py +271 -0
- notionary/blocks/audio/__init__.py +8 -2
- notionary/blocks/audio/audio_element.py +69 -106
- notionary/blocks/audio/audio_markdown_node.py +13 -5
- notionary/blocks/audio/audio_models.py +6 -55
- notionary/blocks/base_block_element.py +42 -0
- notionary/blocks/bookmark/__init__.py +9 -2
- notionary/blocks/bookmark/bookmark_element.py +49 -139
- notionary/blocks/bookmark/bookmark_markdown_node.py +19 -18
- notionary/blocks/bookmark/bookmark_models.py +15 -0
- notionary/blocks/breadcrumbs/__init__.py +17 -0
- notionary/blocks/breadcrumbs/breadcrumb_element.py +39 -0
- notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +32 -0
- notionary/blocks/breadcrumbs/breadcrumb_models.py +12 -0
- notionary/blocks/bulleted_list/__init__.py +12 -2
- notionary/blocks/bulleted_list/bulleted_list_element.py +55 -53
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +2 -1
- notionary/blocks/bulleted_list/bulleted_list_models.py +18 -0
- notionary/blocks/callout/__init__.py +9 -2
- notionary/blocks/callout/callout_element.py +53 -86
- notionary/blocks/callout/callout_markdown_node.py +3 -1
- notionary/blocks/callout/callout_models.py +33 -0
- notionary/blocks/child_database/__init__.py +14 -0
- notionary/blocks/child_database/child_database_element.py +61 -0
- notionary/blocks/child_database/child_database_models.py +12 -0
- notionary/blocks/child_page/__init__.py +9 -0
- notionary/blocks/child_page/child_page_element.py +94 -0
- notionary/blocks/child_page/child_page_models.py +12 -0
- notionary/blocks/{shared/block_client.py → client.py} +54 -54
- notionary/blocks/code/__init__.py +6 -2
- notionary/blocks/code/code_element.py +96 -181
- notionary/blocks/code/code_markdown_node.py +64 -13
- notionary/blocks/code/code_models.py +94 -0
- notionary/blocks/column/__init__.py +25 -1
- notionary/blocks/column/column_element.py +44 -312
- notionary/blocks/column/column_list_element.py +52 -0
- notionary/blocks/column/column_list_markdown_node.py +50 -0
- notionary/blocks/column/column_markdown_node.py +59 -0
- notionary/blocks/column/column_models.py +26 -0
- notionary/blocks/divider/__init__.py +9 -2
- notionary/blocks/divider/divider_element.py +18 -49
- notionary/blocks/divider/divider_markdown_node.py +2 -1
- notionary/blocks/divider/divider_models.py +12 -0
- notionary/blocks/embed/__init__.py +9 -2
- notionary/blocks/embed/embed_element.py +65 -111
- notionary/blocks/embed/embed_markdown_node.py +3 -1
- notionary/blocks/embed/embed_models.py +14 -0
- notionary/blocks/equation/__init__.py +14 -0
- notionary/blocks/equation/equation_element.py +133 -0
- notionary/blocks/equation/equation_element_markdown_node.py +35 -0
- notionary/blocks/equation/equation_models.py +11 -0
- notionary/blocks/file/__init__.py +25 -0
- notionary/blocks/file/file_element.py +112 -0
- notionary/blocks/file/file_element_markdown_node.py +37 -0
- notionary/blocks/file/file_element_models.py +39 -0
- notionary/blocks/guards.py +22 -0
- notionary/blocks/heading/__init__.py +16 -2
- notionary/blocks/heading/heading_element.py +83 -69
- notionary/blocks/heading/heading_markdown_node.py +2 -1
- notionary/blocks/heading/heading_models.py +29 -0
- notionary/blocks/image_block/__init__.py +13 -0
- notionary/blocks/image_block/image_element.py +89 -0
- notionary/blocks/{image → image_block}/image_markdown_node.py +13 -6
- notionary/blocks/image_block/image_models.py +10 -0
- notionary/blocks/mixins/captions/__init__.py +4 -0
- notionary/blocks/mixins/captions/caption_markdown_node_mixin.py +31 -0
- notionary/blocks/mixins/captions/caption_mixin.py +92 -0
- notionary/blocks/models.py +174 -0
- notionary/blocks/numbered_list/__init__.py +12 -2
- notionary/blocks/numbered_list/numbered_list_element.py +48 -56
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -1
- notionary/blocks/numbered_list/numbered_list_models.py +17 -0
- notionary/blocks/paragraph/__init__.py +12 -2
- notionary/blocks/paragraph/paragraph_element.py +40 -66
- notionary/blocks/paragraph/paragraph_markdown_node.py +2 -1
- notionary/blocks/paragraph/paragraph_models.py +16 -0
- notionary/blocks/pdf/__init__.py +13 -0
- notionary/blocks/pdf/pdf_element.py +97 -0
- notionary/blocks/pdf/pdf_markdown_node.py +37 -0
- notionary/blocks/pdf/pdf_models.py +11 -0
- notionary/blocks/quote/__init__.py +11 -2
- notionary/blocks/quote/quote_element.py +45 -62
- notionary/blocks/quote/quote_markdown_node.py +6 -3
- notionary/blocks/quote/quote_models.py +18 -0
- notionary/blocks/registry/__init__.py +4 -0
- notionary/blocks/registry/block_registry.py +60 -121
- notionary/blocks/registry/block_registry_builder.py +115 -59
- notionary/blocks/rich_text/__init__.py +33 -0
- notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
- notionary/blocks/rich_text/rich_text_models.py +221 -0
- notionary/blocks/rich_text/text_inline_formatter.py +456 -0
- notionary/blocks/syntax_prompt_builder.py +137 -0
- notionary/blocks/table/__init__.py +16 -2
- notionary/blocks/table/table_element.py +136 -228
- notionary/blocks/table/table_markdown_node.py +2 -1
- notionary/blocks/table/table_models.py +28 -0
- notionary/blocks/table_of_contents/__init__.py +19 -0
- notionary/blocks/table_of_contents/table_of_contents_element.py +68 -0
- notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +35 -0
- notionary/blocks/table_of_contents/table_of_contents_models.py +18 -0
- notionary/blocks/todo/__init__.py +9 -2
- notionary/blocks/todo/todo_element.py +52 -92
- notionary/blocks/todo/todo_markdown_node.py +2 -1
- notionary/blocks/todo/todo_models.py +19 -0
- notionary/blocks/toggle/__init__.py +13 -3
- notionary/blocks/toggle/toggle_element.py +69 -260
- notionary/blocks/toggle/toggle_markdown_node.py +25 -15
- notionary/blocks/toggle/toggle_models.py +17 -0
- notionary/blocks/toggleable_heading/__init__.py +6 -2
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +86 -241
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
- notionary/blocks/types.py +130 -0
- notionary/blocks/video/__init__.py +8 -2
- notionary/blocks/video/video_element.py +70 -141
- notionary/blocks/video/video_element_models.py +10 -0
- notionary/blocks/video/video_markdown_node.py +13 -6
- notionary/database/client.py +26 -8
- notionary/database/database.py +13 -14
- notionary/database/database_filter_builder.py +2 -2
- notionary/database/database_provider.py +5 -4
- notionary/database/models.py +337 -0
- notionary/database/notion_database.py +6 -7
- notionary/file_upload/client.py +5 -7
- notionary/file_upload/models.py +3 -2
- notionary/file_upload/notion_file_upload.py +2 -3
- notionary/markdown/markdown_builder.py +729 -0
- notionary/markdown/markdown_document_model.py +228 -0
- notionary/{blocks → markdown}/markdown_node.py +1 -0
- notionary/models/notion_database_response.py +0 -338
- notionary/page/client.py +34 -15
- notionary/page/models.py +327 -0
- notionary/page/notion_page.py +136 -58
- notionary/page/{content/page_content_writer.py → page_content_deleting_service.py} +25 -59
- notionary/page/page_content_writer.py +177 -0
- notionary/page/page_context.py +65 -0
- notionary/page/reader/handler/__init__.py +19 -0
- notionary/page/reader/handler/base_block_renderer.py +44 -0
- notionary/page/reader/handler/block_processing_context.py +35 -0
- notionary/page/reader/handler/block_rendering_context.py +48 -0
- notionary/page/reader/handler/column_list_renderer.py +51 -0
- notionary/page/reader/handler/column_renderer.py +60 -0
- notionary/page/reader/handler/line_renderer.py +73 -0
- notionary/page/reader/handler/numbered_list_renderer.py +85 -0
- notionary/page/reader/handler/toggle_renderer.py +69 -0
- notionary/page/reader/handler/toggleable_heading_renderer.py +89 -0
- notionary/page/reader/page_content_retriever.py +81 -0
- notionary/page/search_filter_builder.py +2 -1
- notionary/page/writer/handler/__init__.py +24 -0
- notionary/page/writer/handler/code_handler.py +72 -0
- notionary/page/writer/handler/column_handler.py +141 -0
- notionary/page/writer/handler/column_list_handler.py +139 -0
- notionary/page/writer/handler/equation_handler.py +74 -0
- notionary/page/writer/handler/line_handler.py +35 -0
- notionary/page/writer/handler/line_processing_context.py +54 -0
- notionary/page/writer/handler/regular_line_handler.py +86 -0
- notionary/page/writer/handler/table_handler.py +66 -0
- notionary/page/writer/handler/toggle_handler.py +155 -0
- notionary/page/writer/handler/toggleable_heading_handler.py +173 -0
- notionary/page/writer/markdown_to_notion_converter.py +95 -0
- notionary/page/writer/markdown_to_notion_converter_context.py +30 -0
- notionary/page/writer/markdown_to_notion_formatting_post_processor.py +73 -0
- notionary/page/writer/notion_text_length_processor.py +150 -0
- notionary/telemetry/__init__.py +2 -2
- notionary/telemetry/service.py +3 -3
- notionary/user/__init__.py +2 -2
- notionary/user/base_notion_user.py +2 -1
- notionary/user/client.py +2 -3
- notionary/user/models.py +1 -0
- notionary/user/notion_bot_user.py +4 -5
- notionary/user/notion_user.py +3 -4
- notionary/user/notion_user_manager.py +23 -95
- notionary/util/__init__.py +3 -2
- notionary/util/fuzzy.py +2 -1
- notionary/util/logging_mixin.py +2 -2
- notionary/util/singleton_metaclass.py +1 -1
- notionary/workspace.py +6 -5
- notionary-0.2.22.dist-info/METADATA +237 -0
- notionary-0.2.22.dist-info/RECORD +200 -0
- notionary/blocks/document/__init__.py +0 -7
- notionary/blocks/document/document_element.py +0 -102
- notionary/blocks/document/document_markdown_node.py +0 -31
- notionary/blocks/image/__init__.py +0 -7
- notionary/blocks/image/image_element.py +0 -151
- notionary/blocks/markdown_builder.py +0 -356
- notionary/blocks/mention/__init__.py +0 -7
- notionary/blocks/mention/mention_element.py +0 -229
- notionary/blocks/mention/mention_markdown_node.py +0 -38
- notionary/blocks/prompts/element_prompt_builder.py +0 -83
- notionary/blocks/prompts/element_prompt_content.py +0 -41
- notionary/blocks/shared/models.py +0 -713
- notionary/blocks/shared/notion_block_element.py +0 -37
- notionary/blocks/shared/text_inline_formatter.py +0 -262
- notionary/blocks/shared/text_inline_formatter_new.py +0 -139
- notionary/database/models/page_result.py +0 -10
- notionary/models/notion_block_response.py +0 -264
- notionary/models/notion_page_response.py +0 -78
- notionary/models/search_response.py +0 -0
- notionary/page/__init__.py +0 -0
- notionary/page/content/markdown_whitespace_processor.py +0 -80
- notionary/page/content/notion_text_length_utils.py +0 -87
- notionary/page/content/page_content_retriever.py +0 -60
- notionary/page/formatting/line_processor.py +0 -153
- notionary/page/formatting/markdown_to_notion_converter.py +0 -153
- notionary/page/markdown_syntax_prompt_generator.py +0 -114
- notionary/page/notion_to_markdown_converter.py +0 -179
- notionary/page/properites/property_value_extractor.py +0 -0
- notionary/user/notion_user_provider.py +0 -1
- notionary-0.2.19.dist-info/METADATA +0 -225
- notionary-0.2.19.dist-info/RECORD +0 -150
- /notionary/{blocks/document/document_models.py → markdown/___init__.py} +0 -0
- /notionary/{blocks/image/image_models.py → markdown/makdown_document_model.py} +0 -0
- /notionary/{blocks/mention/mention_models.py → page/reader/handler/equation_renderer.py} +0 -0
- /notionary/{blocks/shared/__init__.py → page/writer/markdown_to_notion_post_processor.py} +0 -0
- /notionary/{blocks/toggleable_heading/toggleable_heading_models.py → page/writer/markdown_to_notion_text_length_post_processor.py} +0 -0
- /notionary/{elements/__init__.py → util/concurrency_limiter.py} +0 -0
- {notionary-0.2.19.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
- {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
|
4
|
-
from notionary.blocks.
|
5
|
-
from notionary.
|
6
|
-
from notionary.page.
|
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
|
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
|
-
|
24
|
-
|
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
|
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
|
-
|
71
|
-
|
72
|
-
|
73
|
-
return
|
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"
|