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,10 +1,9 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Any, Optional
|
2
|
+
|
2
3
|
from notionary.base_notion_client import BaseNotionClient
|
3
|
-
from notionary.
|
4
|
-
from notionary.blocks.shared.models import Block, BlockChildrenResponse
|
4
|
+
from notionary.blocks.models import Block, BlockChildrenResponse, BlockCreateRequest
|
5
5
|
|
6
6
|
|
7
|
-
@singleton
|
8
7
|
class NotionBlockClient(BaseNotionClient):
|
9
8
|
"""
|
10
9
|
Client for Notion Block API operations.
|
@@ -25,6 +24,36 @@ class NotionBlockClient(BaseNotionClient):
|
|
25
24
|
return None
|
26
25
|
return None
|
27
26
|
|
27
|
+
async def get_blocks_by_page_id_recursively(
|
28
|
+
self, page_id: str, parent_id: Optional[str] = None
|
29
|
+
) -> list[Block]:
|
30
|
+
response = (
|
31
|
+
await self.get_block_children(block_id=page_id)
|
32
|
+
if parent_id is None
|
33
|
+
else await self.get_block_children(block_id=parent_id)
|
34
|
+
)
|
35
|
+
|
36
|
+
if not response or not response.results:
|
37
|
+
return []
|
38
|
+
|
39
|
+
blocks = response.results
|
40
|
+
|
41
|
+
for block in blocks:
|
42
|
+
if not block.has_children:
|
43
|
+
continue
|
44
|
+
|
45
|
+
block_id = block.id
|
46
|
+
if not block_id:
|
47
|
+
continue
|
48
|
+
|
49
|
+
children = await self.get_blocks_by_page_id_recursively(
|
50
|
+
page_id=page_id, parent_id=block_id
|
51
|
+
)
|
52
|
+
if children:
|
53
|
+
block.children = children
|
54
|
+
|
55
|
+
return blocks
|
56
|
+
|
28
57
|
async def get_block_children(
|
29
58
|
self, block_id: str, start_cursor: Optional[str] = None, page_size: int = 100
|
30
59
|
) -> Optional[BlockChildrenResponse]:
|
@@ -38,13 +67,15 @@ class NotionBlockClient(BaseNotionClient):
|
|
38
67
|
params["start_cursor"] = start_cursor
|
39
68
|
|
40
69
|
response = await self.get(f"blocks/{block_id}/children", params=params)
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
70
|
+
|
71
|
+
if not response:
|
72
|
+
return None
|
73
|
+
|
74
|
+
try:
|
75
|
+
return BlockChildrenResponse.model_validate(response)
|
76
|
+
except Exception as e:
|
77
|
+
self.logger.error("Failed to parse block children response: %s", str(e))
|
78
|
+
return None
|
48
79
|
|
49
80
|
async def get_all_block_children(self, block_id: str) -> list[Block]:
|
50
81
|
"""
|
@@ -74,7 +105,10 @@ class NotionBlockClient(BaseNotionClient):
|
|
74
105
|
return all_blocks
|
75
106
|
|
76
107
|
async def append_block_children(
|
77
|
-
self,
|
108
|
+
self,
|
109
|
+
block_id: str,
|
110
|
+
children: list[BlockCreateRequest],
|
111
|
+
after: Optional[str] = None,
|
78
112
|
) -> Optional[BlockChildrenResponse]:
|
79
113
|
"""
|
80
114
|
Appends new child blocks to a parent block.
|
@@ -86,15 +120,18 @@ class NotionBlockClient(BaseNotionClient):
|
|
86
120
|
|
87
121
|
self.logger.debug("Appending %d children to block: %s", len(children), block_id)
|
88
122
|
|
123
|
+
# Convert Pydantic models to dictionaries for API
|
124
|
+
children_dicts = [block.model_dump(exclude_none=True) for block in children]
|
125
|
+
|
89
126
|
# If 100 or fewer blocks, use single request
|
90
|
-
if len(
|
91
|
-
return await self._append_single_batch(block_id,
|
127
|
+
if len(children_dicts) <= 100:
|
128
|
+
return await self._append_single_batch(block_id, children_dicts, after)
|
92
129
|
|
93
130
|
# For more than 100 blocks, use batch processing
|
94
|
-
return await self._append_multiple_batches(block_id,
|
131
|
+
return await self._append_multiple_batches(block_id, children_dicts, after)
|
95
132
|
|
96
133
|
async def _append_single_batch(
|
97
|
-
self, block_id: str, children: list[
|
134
|
+
self, block_id: str, children: list[dict[str, Any]], after: Optional[str] = None
|
98
135
|
) -> Optional[BlockChildrenResponse]:
|
99
136
|
"""
|
100
137
|
Appends a single batch of blocks (≤100).
|
@@ -113,7 +150,7 @@ class NotionBlockClient(BaseNotionClient):
|
|
113
150
|
return None
|
114
151
|
|
115
152
|
async def _append_multiple_batches(
|
116
|
-
self, block_id: str, children: list[
|
153
|
+
self, block_id: str, children: list[dict[str, Any]], after: Optional[str] = None
|
117
154
|
) -> Optional[BlockChildrenResponse]:
|
118
155
|
"""
|
119
156
|
Appends multiple batches of blocks, handling pagination.
|
@@ -206,27 +243,6 @@ class NotionBlockClient(BaseNotionClient):
|
|
206
243
|
request_id=responses[-1].request_id, # Use last request ID
|
207
244
|
)
|
208
245
|
|
209
|
-
async def update_block(
|
210
|
-
self, block_id: str, block_data: Dict[str, Any], archived: Optional[bool] = None
|
211
|
-
) -> Optional[Block]:
|
212
|
-
"""
|
213
|
-
Updates an existing block.
|
214
|
-
"""
|
215
|
-
self.logger.debug("Updating block: %s", block_id)
|
216
|
-
|
217
|
-
data = block_data.copy()
|
218
|
-
if archived is not None:
|
219
|
-
data["archived"] = archived
|
220
|
-
|
221
|
-
response = await self.patch(f"blocks/{block_id}", data)
|
222
|
-
if response:
|
223
|
-
try:
|
224
|
-
return Block.model_validate(response)
|
225
|
-
except Exception as e:
|
226
|
-
self.logger.error("Failed to parse update response: %s", str(e))
|
227
|
-
return None
|
228
|
-
return None
|
229
|
-
|
230
246
|
async def delete_block(self, block_id: str) -> Optional[Block]:
|
231
247
|
"""
|
232
248
|
Deletes (archives) a block.
|
@@ -238,19 +254,3 @@ class NotionBlockClient(BaseNotionClient):
|
|
238
254
|
# After deletion, retrieve the block to return the updated state
|
239
255
|
return await self.get_block(block_id)
|
240
256
|
return None
|
241
|
-
|
242
|
-
async def archive_block(self, block_id: str) -> Optional[Block]:
|
243
|
-
"""
|
244
|
-
Archives a block by setting archived=True.
|
245
|
-
"""
|
246
|
-
self.logger.debug("Archiving block: %s", block_id)
|
247
|
-
|
248
|
-
return await self.update_block(block_id=block_id, block_data={}, archived=True)
|
249
|
-
|
250
|
-
async def unarchive_block(self, block_id: str) -> Optional[Block]:
|
251
|
-
"""
|
252
|
-
Unarchives a block by setting archived=False.
|
253
|
-
"""
|
254
|
-
self.logger.debug("Unarchiving block: %s", block_id)
|
255
|
-
|
256
|
-
return await self.update_block(block_id=block_id, block_data={}, archived=False)
|
@@ -1,7 +1,11 @@
|
|
1
|
-
from .code_element import CodeElement
|
2
|
-
from .code_markdown_node import CodeMarkdownNode
|
1
|
+
from notionary.blocks.code.code_element import CodeElement
|
2
|
+
from notionary.blocks.code.code_markdown_node import CodeMarkdownNode
|
3
|
+
from notionary.blocks.code.code_models import CodeBlock, CodeLanguage, CreateCodeBlock
|
3
4
|
|
4
5
|
__all__ = [
|
5
6
|
"CodeElement",
|
7
|
+
"CodeBlock",
|
8
|
+
"CodeLanguage",
|
9
|
+
"CreateCodeBlock",
|
6
10
|
"CodeMarkdownNode",
|
7
11
|
]
|
@@ -1,117 +1,108 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import re
|
4
|
+
from typing import Optional
|
2
5
|
|
3
|
-
from
|
4
|
-
from notionary.blocks import
|
5
|
-
from notionary.blocks import
|
6
|
-
|
7
|
-
|
8
|
-
NotionBlockResult,
|
9
|
-
)
|
10
|
-
from notionary.blocks.shared.models import RichTextObject
|
6
|
+
from notionary.blocks.base_block_element import BaseBlockElement
|
7
|
+
from notionary.blocks.code.code_models import CodeBlock, CodeLanguage, CreateCodeBlock
|
8
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
9
|
+
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
10
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
11
11
|
|
12
12
|
|
13
|
-
class CodeElement(
|
13
|
+
class CodeElement(BaseBlockElement):
|
14
14
|
"""
|
15
15
|
Handles conversion between Markdown code blocks and Notion code blocks.
|
16
|
+
Now integrated into the LineProcessor stack system.
|
16
17
|
|
17
18
|
Markdown code block syntax:
|
18
19
|
```language
|
19
|
-
code content
|
20
|
+
[code content as child lines]
|
20
21
|
```
|
21
|
-
Caption: optional caption text
|
22
|
-
|
23
|
-
Where:
|
24
|
-
- language is optional and specifies the programming language
|
25
|
-
- code content is the code to be displayed
|
26
|
-
- Caption line is optional and must appear immediately after the closing ```
|
27
22
|
"""
|
28
23
|
|
29
|
-
|
30
|
-
|
31
|
-
)
|
24
|
+
DEFAULT_LANGUAGE = "plain text"
|
25
|
+
CODE_START_PATTERN = re.compile(r"^```(\w*)\s*$")
|
26
|
+
CODE_START_WITH_CAPTION_PATTERN = re.compile(r"^```(\w*)\s*(?:\"([^\"]*)\")?\s*$")
|
32
27
|
|
33
28
|
@classmethod
|
34
|
-
def
|
35
|
-
"""Check if
|
36
|
-
return
|
29
|
+
def match_notion(cls, block: Block) -> bool:
|
30
|
+
"""Check if block is a Notion code block."""
|
31
|
+
return block.type == BlockType.CODE and block.code
|
37
32
|
|
38
33
|
@classmethod
|
39
|
-
def
|
40
|
-
"""
|
41
|
-
|
34
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
35
|
+
"""Convert opening ```language to Notion code block."""
|
36
|
+
if not (match := cls.CODE_START_PATTERN.match(text.strip())):
|
37
|
+
return None
|
38
|
+
|
39
|
+
language = (match.group(1) or cls.DEFAULT_LANGUAGE).lower()
|
40
|
+
language = cls._normalize_language(language)
|
41
|
+
|
42
|
+
# Create empty CodeBlock - content will be added by stack processor
|
43
|
+
code_block = CodeBlock(rich_text=[], language=language, caption=[])
|
44
|
+
return CreateCodeBlock(code=code_block)
|
42
45
|
|
43
46
|
@classmethod
|
44
|
-
def
|
45
|
-
|
46
|
-
|
47
|
+
def create_from_markdown_block(
|
48
|
+
cls, opening_line: str, code_lines: list[str]
|
49
|
+
) -> BlockCreateResult:
|
50
|
+
"""
|
51
|
+
Create a complete code block from markdown components.
|
52
|
+
"""
|
53
|
+
match = cls.CODE_START_WITH_CAPTION_PATTERN.match(opening_line.strip())
|
47
54
|
if not match:
|
48
55
|
return None
|
49
56
|
|
50
|
-
language = match.group(1) or
|
51
|
-
|
52
|
-
caption = match.group(3)
|
57
|
+
language = (match.group(1) or cls.DEFAULT_LANGUAGE).lower()
|
58
|
+
language = cls._normalize_language(language)
|
53
59
|
|
54
|
-
if
|
55
|
-
content = content[:-1]
|
60
|
+
caption = match.group(2) if match.group(2) else None
|
56
61
|
|
57
|
-
# Create
|
58
|
-
|
62
|
+
# Create rich text content from code lines
|
63
|
+
rich_text = []
|
64
|
+
if code_lines:
|
65
|
+
content = "\n".join(code_lines)
|
66
|
+
rich_text = [RichTextObject.for_code_block(content)]
|
59
67
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
"rich_text": [content_rich_text.model_dump()],
|
64
|
-
"language": language,
|
65
|
-
},
|
66
|
-
}
|
68
|
+
caption_list = []
|
69
|
+
if caption:
|
70
|
+
caption_list = [RichTextObject.for_caption(caption)]
|
67
71
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
block["code"]["caption"] = [caption_rich_text.model_dump()]
|
72
|
-
|
73
|
-
# Leerer Paragraph nach dem Code-Block
|
74
|
-
empty_paragraph = {"type": "paragraph", "paragraph": {"rich_text": []}}
|
72
|
+
code_block = CodeBlock(
|
73
|
+
rich_text=rich_text, language=language, caption=caption_list
|
74
|
+
)
|
75
75
|
|
76
|
-
return
|
76
|
+
return CreateCodeBlock(code=code_block)
|
77
77
|
|
78
78
|
@classmethod
|
79
|
-
def notion_to_markdown(cls, block:
|
79
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
80
80
|
"""Convert Notion code block to Markdown."""
|
81
|
-
if block.
|
81
|
+
if block.type != BlockType.CODE:
|
82
82
|
return None
|
83
83
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
)
|
97
|
-
|
98
|
-
def extract_caption(caption_list):
|
99
|
-
"""Extract caption text from caption array."""
|
100
|
-
return "".join(
|
101
|
-
c.get("text", {}).get("content", "")
|
102
|
-
for c in caption_list
|
103
|
-
if c.get("type") == "text"
|
104
|
-
)
|
105
|
-
|
106
|
-
code_content = extract_content(rich_text)
|
107
|
-
caption_text = extract_caption(caption)
|
84
|
+
if not block.code:
|
85
|
+
return None
|
86
|
+
|
87
|
+
language_enum = block.code.language
|
88
|
+
rich_text = block.code.rich_text or []
|
89
|
+
caption = block.code.caption or []
|
90
|
+
|
91
|
+
code_content = cls.extract_content(rich_text)
|
92
|
+
caption_text = cls.extract_caption(caption)
|
93
|
+
|
94
|
+
# Convert enum to string value
|
95
|
+
language = language_enum.value if language_enum else ""
|
108
96
|
|
109
97
|
# Handle language - convert "plain text" back to empty string for markdown
|
110
|
-
if language ==
|
98
|
+
if language == cls.DEFAULT_LANGUAGE:
|
111
99
|
language = ""
|
112
100
|
|
113
101
|
# Build markdown code block
|
114
|
-
|
102
|
+
if language:
|
103
|
+
result = f"```{language}\n{code_content}\n```"
|
104
|
+
else:
|
105
|
+
result = f"```\n{code_content}\n```"
|
115
106
|
|
116
107
|
# Add caption if present
|
117
108
|
if caption_text:
|
@@ -120,115 +111,39 @@ class CodeElement(NotionBlockElement):
|
|
120
111
|
return result
|
121
112
|
|
122
113
|
@classmethod
|
123
|
-
def
|
114
|
+
def _normalize_language(cls, language: str) -> CodeLanguage:
|
124
115
|
"""
|
125
|
-
|
126
|
-
|
127
|
-
Args:
|
128
|
-
text: The text to search in
|
129
|
-
|
130
|
-
Returns:
|
131
|
-
List of tuples with (start_pos, end_pos, block)
|
116
|
+
Normalize the language string to a valid CodeLanguage enum or default.
|
132
117
|
"""
|
133
|
-
|
134
|
-
for
|
135
|
-
|
136
|
-
|
137
|
-
caption = match.group(3)
|
138
|
-
|
139
|
-
# Remove trailing newline if present
|
140
|
-
if content.endswith("\n"):
|
141
|
-
content = content[:-1]
|
142
|
-
|
143
|
-
block = {
|
144
|
-
"type": "code",
|
145
|
-
"code": {
|
146
|
-
"rich_text": [
|
147
|
-
{
|
148
|
-
"type": "text",
|
149
|
-
"text": {"content": content},
|
150
|
-
"plain_text": content,
|
151
|
-
}
|
152
|
-
],
|
153
|
-
"language": language,
|
154
|
-
},
|
155
|
-
}
|
156
|
-
|
157
|
-
# Add caption if provided
|
158
|
-
if caption and caption.strip():
|
159
|
-
block["code"]["caption"] = [
|
160
|
-
{
|
161
|
-
"type": "text",
|
162
|
-
"text": {"content": caption.strip()},
|
163
|
-
"plain_text": caption.strip(),
|
164
|
-
}
|
165
|
-
]
|
166
|
-
|
167
|
-
matches.append((match.start(), match.end(), block))
|
168
|
-
|
169
|
-
return matches
|
118
|
+
# Try to find matching enum by value
|
119
|
+
for lang_enum in CodeLanguage:
|
120
|
+
if lang_enum.value.lower() == language.lower():
|
121
|
+
return lang_enum
|
170
122
|
|
171
|
-
|
172
|
-
|
173
|
-
return True
|
174
|
-
|
175
|
-
@classmethod
|
176
|
-
def get_llm_prompt_content(cls) -> ElementPromptContent:
|
177
|
-
"""
|
178
|
-
Returns structured LLM prompt metadata for the code block element.
|
179
|
-
"""
|
180
|
-
return (
|
181
|
-
ElementPromptBuilder()
|
182
|
-
.with_description(
|
183
|
-
"Use fenced code blocks to format content as code. Supports language annotations like "
|
184
|
-
"'python', 'json', or 'mermaid'. Useful for displaying code, configurations, command-line "
|
185
|
-
"examples, or diagram syntax. Also suitable for explaining or visualizing systems with diagram languages. "
|
186
|
-
"Code blocks can include optional captions for better documentation."
|
187
|
-
)
|
188
|
-
.with_usage_guidelines(
|
189
|
-
"Use code blocks when you want to present technical content like code snippets, terminal commands, "
|
190
|
-
"JSON structures, or system diagrams. Especially helpful when structure and formatting are essential. "
|
191
|
-
"Add captions to provide context, explanations, or titles for your code blocks."
|
192
|
-
)
|
193
|
-
.with_syntax(
|
194
|
-
"```language\ncode content\n```\nCaption: optional caption text\n\n"
|
195
|
-
"OR\n\n"
|
196
|
-
"```language\ncode content\n```"
|
197
|
-
)
|
198
|
-
.with_examples(
|
199
|
-
[
|
200
|
-
"```python\nprint('Hello, world!')\n```\nCaption: Basic Python greeting example",
|
201
|
-
'```json\n{"name": "Alice", "age": 30}\n```\nCaption: User data structure',
|
202
|
-
"```mermaid\nflowchart TD\n A --> B\n```\nCaption: Simple flow diagram",
|
203
|
-
'```bash\ngit commit -m "Initial commit"\n```',
|
204
|
-
]
|
205
|
-
)
|
206
|
-
.with_avoidance_guidelines(
|
207
|
-
"NEVER EVER wrap markdown content with ```markdown. Markdown should be written directly without code block formatting. "
|
208
|
-
"NEVER use ```markdown under any circumstances. "
|
209
|
-
"For Mermaid diagrams, use ONLY the default styling without colors, backgrounds, or custom styling attributes. "
|
210
|
-
"Keep Mermaid diagrams simple and minimal without any styling or color modifications. "
|
211
|
-
"Captions must appear immediately after the closing ``` on a new line starting with 'Caption:' - "
|
212
|
-
"no empty lines between the code block and the caption."
|
213
|
-
)
|
214
|
-
.build()
|
215
|
-
)
|
123
|
+
# Return default if not found
|
124
|
+
return CodeLanguage.PLAIN_TEXT
|
216
125
|
|
217
126
|
@staticmethod
|
218
|
-
def extract_content(rich_text_list: list[
|
127
|
+
def extract_content(rich_text_list: list[RichTextObject]) -> str:
|
219
128
|
"""Extract code content from rich_text array."""
|
220
|
-
return "".join(
|
221
|
-
text.get("text", {}).get("content", "")
|
222
|
-
if text.get("type") == "text"
|
223
|
-
else text.get("plain_text", "")
|
224
|
-
for text in rich_text_list
|
225
|
-
)
|
129
|
+
return "".join(rt.plain_text for rt in rich_text_list if rt.plain_text)
|
226
130
|
|
227
131
|
@staticmethod
|
228
|
-
def extract_caption(caption_list: list[
|
132
|
+
def extract_caption(caption_list: list[RichTextObject]) -> str:
|
229
133
|
"""Extract caption text from caption array."""
|
230
|
-
return "".join(
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
134
|
+
return "".join(rt.plain_text for rt in caption_list if rt.plain_text)
|
135
|
+
|
136
|
+
@classmethod
|
137
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
138
|
+
"""Get system prompt information for code blocks."""
|
139
|
+
return BlockElementMarkdownInformation(
|
140
|
+
block_type=cls.__name__,
|
141
|
+
description="Code blocks display syntax-highlighted code with optional language specification and captions",
|
142
|
+
syntax_examples=[
|
143
|
+
"```\nprint('Hello World')\n```",
|
144
|
+
"```python\nprint('Hello World')\n```",
|
145
|
+
"```python \"Example code\"\nprint('Hello World')\n```",
|
146
|
+
"```javascript\nconsole.log('Hello');\n```",
|
147
|
+
],
|
148
|
+
usage_guidelines="Use for displaying code snippets. Language specification enables syntax highlighting. Caption in quotes on first line provides description. Supports many programming languages.",
|
149
|
+
)
|
@@ -1,24 +1,20 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from typing import Optional
|
4
|
-
from pydantic import BaseModel
|
5
|
-
from notionary.blocks.markdown_node import MarkdownNode
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
code: str
|
10
|
-
language: Optional[str] = None
|
11
|
-
caption: Optional[str] = None
|
5
|
+
from notionary.blocks.code.code_models import CodeBlock
|
6
|
+
from notionary.markdown.markdown_node import MarkdownNode
|
12
7
|
|
13
8
|
|
14
9
|
class CodeMarkdownNode(MarkdownNode):
|
15
10
|
"""
|
16
11
|
Programmatic interface for creating Notion-style Markdown code blocks.
|
12
|
+
Automatically handles indentation normalization for multiline strings.
|
13
|
+
|
17
14
|
Example:
|
18
|
-
```python
|
15
|
+
```python "Basic usage"
|
19
16
|
print("Hello, world!")
|
20
17
|
```
|
21
|
-
Caption: Basic usage
|
22
18
|
"""
|
23
19
|
|
24
20
|
def __init__(
|
@@ -32,12 +28,67 @@ class CodeMarkdownNode(MarkdownNode):
|
|
32
28
|
self.caption = caption
|
33
29
|
|
34
30
|
@classmethod
|
35
|
-
def from_params(cls, params:
|
36
|
-
return cls(
|
31
|
+
def from_params(cls, params: CodeBlock) -> CodeMarkdownNode:
|
32
|
+
return cls(
|
33
|
+
code=params.rich_text, language=params.language, caption=params.caption
|
34
|
+
)
|
37
35
|
|
38
36
|
def to_markdown(self) -> str:
|
39
37
|
lang = self.language or ""
|
40
|
-
|
38
|
+
|
39
|
+
# Build the opening fence with optional caption
|
40
|
+
opening_fence = f"```{lang}"
|
41
41
|
if self.caption:
|
42
|
-
|
42
|
+
opening_fence += f' "{self.caption}"'
|
43
|
+
|
44
|
+
# Smart indentation normalization
|
45
|
+
normalized_code = self._normalize_indentation(self.code)
|
46
|
+
|
47
|
+
content = f"{opening_fence}\n{normalized_code}\n```"
|
43
48
|
return content
|
49
|
+
|
50
|
+
def _normalize_indentation(self, code: str) -> str:
|
51
|
+
"""Normalize indentation by removing common leading whitespace."""
|
52
|
+
lines = code.strip().split("\n")
|
53
|
+
|
54
|
+
if self._is_empty_or_single_line(lines):
|
55
|
+
return self._handle_simple_cases(lines)
|
56
|
+
|
57
|
+
min_indentation = self._find_minimum_indentation_excluding_first_line(lines)
|
58
|
+
return self._remove_common_indentation(lines, min_indentation)
|
59
|
+
|
60
|
+
def _is_empty_or_single_line(self, lines: list[str]) -> bool:
|
61
|
+
return not lines or len(lines) == 1
|
62
|
+
|
63
|
+
def _handle_simple_cases(self, lines: list[str]) -> str:
|
64
|
+
if not lines:
|
65
|
+
return ""
|
66
|
+
return lines[0].strip()
|
67
|
+
|
68
|
+
def _find_minimum_indentation_excluding_first_line(self, lines: list[str]) -> int:
|
69
|
+
non_empty_lines_after_first = [line for line in lines[1:] if line.strip()]
|
70
|
+
|
71
|
+
if not non_empty_lines_after_first:
|
72
|
+
return 0
|
73
|
+
|
74
|
+
return min(
|
75
|
+
len(line) - len(line.lstrip()) for line in non_empty_lines_after_first
|
76
|
+
)
|
77
|
+
|
78
|
+
def _remove_common_indentation(self, lines: list[str], min_indentation: int) -> str:
|
79
|
+
normalized_lines = [lines[0].strip()]
|
80
|
+
|
81
|
+
for line in lines[1:]:
|
82
|
+
normalized_line = self._normalize_single_line(line, min_indentation)
|
83
|
+
normalized_lines.append(normalized_line)
|
84
|
+
|
85
|
+
return "\n".join(normalized_lines)
|
86
|
+
|
87
|
+
def _normalize_single_line(self, line: str, min_indentation: int) -> str:
|
88
|
+
if not line.strip():
|
89
|
+
return ""
|
90
|
+
|
91
|
+
if len(line) > min_indentation:
|
92
|
+
return line[min_indentation:]
|
93
|
+
|
94
|
+
return line.strip()
|