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
@@ -0,0 +1,137 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from textwrap import dedent
|
5
|
+
from typing import TYPE_CHECKING, Optional
|
6
|
+
|
7
|
+
|
8
|
+
if TYPE_CHECKING:
|
9
|
+
from notionary.blocks.registry.block_registry import BlockRegistry
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass
|
13
|
+
class BlockElementMarkdownInformation:
|
14
|
+
"""Metadata describing how a Notion block maps to Markdown syntax."""
|
15
|
+
|
16
|
+
block_type: str
|
17
|
+
description: str
|
18
|
+
syntax_examples: list[str]
|
19
|
+
usage_guidelines: str
|
20
|
+
|
21
|
+
|
22
|
+
class SyntaxPromptBuilder:
|
23
|
+
"""
|
24
|
+
Builds a comprehensive markdown syntax reference from a block registry.
|
25
|
+
Iterates over all registered elements and collects their system prompt information.
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, block_registry: BlockRegistry):
|
29
|
+
self.block_registry = block_registry
|
30
|
+
|
31
|
+
def build_markdown_reference(self) -> str:
|
32
|
+
"""
|
33
|
+
Build a complete markdown syntax reference string.
|
34
|
+
"""
|
35
|
+
sections = [
|
36
|
+
self._build_header(),
|
37
|
+
*self._build_element_sections(),
|
38
|
+
]
|
39
|
+
|
40
|
+
return "\n\n".join(sections)
|
41
|
+
|
42
|
+
def build_concise_reference(self) -> str:
|
43
|
+
"""
|
44
|
+
Build a more concise reference suitable for system prompts.
|
45
|
+
"""
|
46
|
+
lines = ["# Notionary Markdown Syntax"]
|
47
|
+
|
48
|
+
for element_class in self.block_registry.get_elements():
|
49
|
+
info: Optional[BlockElementMarkdownInformation] = (
|
50
|
+
element_class.get_system_prompt_information()
|
51
|
+
)
|
52
|
+
if info and info.syntax_examples:
|
53
|
+
# Just show the first example for conciseness
|
54
|
+
example = info.syntax_examples[0]
|
55
|
+
lines.append(f"- {info.block_type}: `{example}`")
|
56
|
+
|
57
|
+
return "\n".join(lines)
|
58
|
+
|
59
|
+
def get_blocks_with_information(self) -> list[str]:
|
60
|
+
"""Get list of block names that provide system prompt information."""
|
61
|
+
blocks = []
|
62
|
+
|
63
|
+
for element_class in self.block_registry.get_elements():
|
64
|
+
info: Optional[BlockElementMarkdownInformation] = (
|
65
|
+
element_class.get_system_prompt_information()
|
66
|
+
)
|
67
|
+
if info:
|
68
|
+
blocks.append(info.block_type)
|
69
|
+
|
70
|
+
return blocks
|
71
|
+
|
72
|
+
def _build_header(self) -> str:
|
73
|
+
"""Build the header section of the reference."""
|
74
|
+
return dedent(
|
75
|
+
"""
|
76
|
+
# Notionary Markdown Syntax Reference
|
77
|
+
|
78
|
+
This comprehensive reference documents all supported markdown syntax for converting between Markdown and Notion blocks.
|
79
|
+
|
80
|
+
Each block type includes:
|
81
|
+
- **Description:** What the block does
|
82
|
+
- **When to use:** Guidelines for appropriate usage
|
83
|
+
- **Syntax:** Complete syntax examples with variations
|
84
|
+
"""
|
85
|
+
).strip()
|
86
|
+
|
87
|
+
def _build_element_sections(self) -> list[str]:
|
88
|
+
"""Build sections for all registered elements."""
|
89
|
+
sections = []
|
90
|
+
|
91
|
+
for element_class in self.block_registry.get_elements():
|
92
|
+
info = element_class.get_system_prompt_information()
|
93
|
+
if info:
|
94
|
+
sections.append(self._build_element_section(info))
|
95
|
+
|
96
|
+
return sections
|
97
|
+
|
98
|
+
def _build_element_section(self, info: BlockElementMarkdownInformation) -> str:
|
99
|
+
"""Build a well-structured section for a single block element."""
|
100
|
+
section_parts = [
|
101
|
+
f"## {info.block_type}",
|
102
|
+
"",
|
103
|
+
f"**Description:** {info.description}",
|
104
|
+
"",
|
105
|
+
]
|
106
|
+
|
107
|
+
if info.usage_guidelines:
|
108
|
+
section_parts.extend(["**When to use:**", info.usage_guidelines, ""])
|
109
|
+
|
110
|
+
if info.syntax_examples:
|
111
|
+
section_parts.extend(
|
112
|
+
[
|
113
|
+
"**Syntax:**",
|
114
|
+
"",
|
115
|
+
*self._format_syntax_examples(info.syntax_examples),
|
116
|
+
"",
|
117
|
+
]
|
118
|
+
)
|
119
|
+
|
120
|
+
return "\n".join(section_parts).rstrip()
|
121
|
+
|
122
|
+
def _format_syntax_examples(self, examples: list[str]) -> list[str]:
|
123
|
+
"""Format syntax examples with proper markdown and clear structure."""
|
124
|
+
formatted = []
|
125
|
+
|
126
|
+
for i, example in enumerate(examples, 1):
|
127
|
+
if len(examples) > 1:
|
128
|
+
formatted.append(f"**Example {i}:**")
|
129
|
+
|
130
|
+
if "\n" in example:
|
131
|
+
# Multi-line example - use code block
|
132
|
+
formatted.extend(["```", example, "```", ""])
|
133
|
+
else:
|
134
|
+
# Single line - use inline code with description
|
135
|
+
formatted.extend([f"`{example}`", ""])
|
136
|
+
|
137
|
+
return formatted
|
@@ -1,7 +1,21 @@
|
|
1
|
-
from .table_element import TableElement
|
2
|
-
from .table_markdown_node import
|
1
|
+
from notionary.blocks.table.table_element import TableElement
|
2
|
+
from notionary.blocks.table.table_markdown_node import (
|
3
|
+
TableMarkdownBlockParams,
|
4
|
+
TableMarkdownNode,
|
5
|
+
)
|
6
|
+
from notionary.blocks.table.table_models import (
|
7
|
+
CreateTableBlock,
|
8
|
+
CreateTableRowBlock,
|
9
|
+
TableBlock,
|
10
|
+
TableRowBlock,
|
11
|
+
)
|
3
12
|
|
4
13
|
__all__ = [
|
5
14
|
"TableElement",
|
15
|
+
"TableBlock",
|
16
|
+
"TableRowBlock",
|
17
|
+
"CreateTableRowBlock",
|
18
|
+
"CreateTableBlock",
|
6
19
|
"TableMarkdownNode",
|
20
|
+
"TableMarkdownBlockParams",
|
7
21
|
]
|
@@ -1,106 +1,158 @@
|
|
1
|
-
import
|
2
|
-
from typing import Dict, Any, Optional, List, Tuple
|
1
|
+
from __future__ import annotations
|
3
2
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
import re
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from notionary.blocks.base_block_element import BaseBlockElement
|
7
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
8
|
+
from notionary.blocks.models import Block, BlockCreateResult
|
9
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
10
|
+
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
11
|
+
from notionary.blocks.table.table_models import (
|
12
|
+
CreateTableBlock,
|
13
|
+
TableBlock,
|
14
|
+
CreateTableRowBlock,
|
15
|
+
TableRowBlock,
|
9
16
|
)
|
10
|
-
from notionary.blocks.
|
17
|
+
from notionary.blocks.types import BlockType
|
11
18
|
|
12
19
|
|
13
|
-
class TableElement(
|
20
|
+
class TableElement(BaseBlockElement):
|
14
21
|
"""
|
15
22
|
Handles conversion between Markdown tables and Notion table blocks.
|
23
|
+
Now integrated into the LineProcessor stack system.
|
16
24
|
|
17
25
|
Markdown table syntax:
|
18
26
|
| Header 1 | Header 2 | Header 3 |
|
19
27
|
| -------- | -------- | -------- |
|
20
28
|
| Cell 1 | Cell 2 | Cell 3 |
|
21
|
-
| Cell 4 | Cell 5 | Cell 6 |
|
22
|
-
|
23
|
-
The second line with dashes and optional colons defines column alignment.
|
24
29
|
"""
|
25
30
|
|
26
31
|
ROW_PATTERN = re.compile(r"^\s*\|(.+)\|\s*$")
|
27
32
|
SEPARATOR_PATTERN = re.compile(r"^\s*\|([\s\-:|]+)\|\s*$")
|
28
33
|
|
29
34
|
@classmethod
|
30
|
-
def
|
31
|
-
"""
|
32
|
-
|
33
|
-
Accepts tables with only header + separator, as well as header + separator + data rows.
|
34
|
-
"""
|
35
|
-
lines = text.split("\n")
|
35
|
+
def match_notion(cls, block: Block) -> bool:
|
36
|
+
"""Check if block is a Notion table."""
|
37
|
+
return block.type == BlockType.TABLE and block.table
|
36
38
|
|
37
|
-
|
38
|
-
|
39
|
+
@classmethod
|
40
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
41
|
+
"""Convert opening table row to Notion table block."""
|
42
|
+
if not cls.ROW_PATTERN.match(text.strip()):
|
43
|
+
return None
|
39
44
|
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
cls.ROW_PATTERN.match(line)
|
44
|
-
and cls.SEPARATOR_PATTERN.match(lines[i + 1])
|
45
|
-
):
|
46
|
-
return True
|
45
|
+
# Parse the header row to determine column count
|
46
|
+
header_cells = cls._parse_table_row(text)
|
47
|
+
col_count = len(header_cells)
|
47
48
|
|
48
|
-
|
49
|
+
table_block = TableBlock(
|
50
|
+
table_width=col_count,
|
51
|
+
has_column_header=True,
|
52
|
+
has_row_header=False,
|
53
|
+
children=[], # Will be populated by stack processor
|
54
|
+
)
|
49
55
|
|
50
|
-
|
51
|
-
def match_notion(cls, block: Dict[str, Any]) -> bool:
|
52
|
-
"""Check if block is a Notion table."""
|
53
|
-
return block.get("type") == "table"
|
56
|
+
return CreateTableBlock(table=table_block)
|
54
57
|
|
55
58
|
@classmethod
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
+
async def create_from_markdown_table(
|
60
|
+
cls, table_lines: list[str]
|
61
|
+
) -> BlockCreateResult:
|
62
|
+
"""
|
63
|
+
Create a complete table block from markdown table lines.
|
64
|
+
"""
|
65
|
+
if not table_lines:
|
59
66
|
return None
|
60
67
|
|
61
|
-
|
68
|
+
first_row = None
|
69
|
+
for line in table_lines:
|
70
|
+
line = line.strip()
|
71
|
+
if line and cls.ROW_PATTERN.match(line):
|
72
|
+
first_row = line
|
73
|
+
break
|
62
74
|
|
63
|
-
|
64
|
-
if table_start is None:
|
75
|
+
if not first_row:
|
65
76
|
return None
|
66
77
|
|
67
|
-
|
68
|
-
|
78
|
+
# Parse header row to determine column count
|
79
|
+
header_cells = cls._parse_table_row(first_row)
|
80
|
+
col_count = len(header_cells)
|
69
81
|
|
70
|
-
|
71
|
-
|
72
|
-
return None
|
82
|
+
# Process all table lines
|
83
|
+
table_rows, separator_found = await cls._process_table_lines(table_lines)
|
73
84
|
|
74
|
-
|
75
|
-
|
85
|
+
# Create complete TableBlock
|
86
|
+
table_block = TableBlock(
|
87
|
+
table_width=col_count,
|
88
|
+
has_column_header=separator_found,
|
89
|
+
has_row_header=False,
|
90
|
+
children=table_rows,
|
91
|
+
)
|
76
92
|
|
77
|
-
|
78
|
-
"type": "table",
|
79
|
-
"table": {
|
80
|
-
"table_width": column_count,
|
81
|
-
"has_column_header": True,
|
82
|
-
"has_row_header": False,
|
83
|
-
"children": TableElement._create_table_rows(rows),
|
84
|
-
},
|
85
|
-
}
|
93
|
+
return CreateTableBlock(table=table_block)
|
86
94
|
|
87
|
-
|
88
|
-
|
95
|
+
@classmethod
|
96
|
+
async def _process_table_lines(
|
97
|
+
cls, table_lines: list[str]
|
98
|
+
) -> tuple[list[CreateTableRowBlock], bool]:
|
99
|
+
"""Process all table lines and return rows and separator status."""
|
100
|
+
table_rows = []
|
101
|
+
separator_found = False
|
89
102
|
|
90
|
-
|
103
|
+
for line in table_lines:
|
104
|
+
line = line.strip()
|
105
|
+
if not line:
|
106
|
+
continue
|
107
|
+
|
108
|
+
if cls._is_separator_line(line):
|
109
|
+
separator_found = True
|
110
|
+
continue
|
111
|
+
|
112
|
+
if cls.ROW_PATTERN.match(line):
|
113
|
+
table_row = await cls._create_table_row_from_line(line)
|
114
|
+
table_rows.append(table_row)
|
115
|
+
|
116
|
+
return table_rows, separator_found
|
117
|
+
|
118
|
+
@classmethod
|
119
|
+
def _is_separator_line(cls, line: str) -> bool:
|
120
|
+
"""Check if line is a table separator (|---|---|)."""
|
121
|
+
return cls.SEPARATOR_PATTERN.match(line) is not None
|
122
|
+
|
123
|
+
@classmethod
|
124
|
+
async def _create_table_row_from_line(cls, line: str) -> CreateTableRowBlock:
|
125
|
+
"""Create a table row block from a markdown line."""
|
126
|
+
cells = cls._parse_table_row(line)
|
127
|
+
rich_text_cells = []
|
128
|
+
for cell in cells:
|
129
|
+
rich_text_cell = await cls._convert_cell_to_rich_text(cell)
|
130
|
+
rich_text_cells.append(rich_text_cell)
|
131
|
+
table_row = TableRowBlock(cells=rich_text_cells)
|
132
|
+
return CreateTableRowBlock(table_row=table_row)
|
133
|
+
|
134
|
+
@classmethod
|
135
|
+
async def _convert_cell_to_rich_text(cls, cell: str) -> list[RichTextObject]:
|
136
|
+
"""Convert cell text to rich text objects."""
|
137
|
+
rich_text = await TextInlineFormatter.parse_inline_formatting(cell)
|
138
|
+
if not rich_text:
|
139
|
+
rich_text = [RichTextObject.from_plain_text(cell)]
|
140
|
+
return rich_text
|
91
141
|
|
92
142
|
@classmethod
|
93
|
-
def notion_to_markdown(cls, block:
|
143
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
94
144
|
"""Convert Notion table block to markdown table."""
|
95
|
-
if block.
|
145
|
+
if block.type != BlockType.TABLE:
|
96
146
|
return None
|
97
147
|
|
98
|
-
|
99
|
-
|
148
|
+
if not block.table:
|
149
|
+
return None
|
100
150
|
|
101
|
-
|
102
|
-
|
151
|
+
table_data = block.table
|
152
|
+
children = block.children or []
|
103
153
|
|
154
|
+
if not children:
|
155
|
+
table_width = table_data.table_width or 3
|
104
156
|
header = (
|
105
157
|
"| " + " | ".join([f"Column {i+1}" for i in range(table_width)]) + " |"
|
106
158
|
)
|
@@ -110,7 +162,6 @@ class TableElement(NotionBlockElement):
|
|
110
162
|
data_row = (
|
111
163
|
"| " + " | ".join([" " for _ in range(table_width)]) + " |"
|
112
164
|
)
|
113
|
-
|
114
165
|
table_rows = [header, separator, data_row]
|
115
166
|
return "\n".join(table_rows)
|
116
167
|
|
@@ -118,86 +169,34 @@ class TableElement(NotionBlockElement):
|
|
118
169
|
header_processed = False
|
119
170
|
|
120
171
|
for child in children:
|
121
|
-
if child.
|
172
|
+
if child.type != BlockType.TABLE_ROW:
|
122
173
|
continue
|
123
174
|
|
124
|
-
|
125
|
-
|
175
|
+
if not child.table_row:
|
176
|
+
continue
|
177
|
+
|
178
|
+
row_data = child.table_row
|
179
|
+
cells = row_data.cells or []
|
126
180
|
|
127
181
|
row_cells = []
|
128
182
|
for cell in cells:
|
129
|
-
cell_text = TextInlineFormatter.extract_text_with_formatting(cell)
|
183
|
+
cell_text = await TextInlineFormatter.extract_text_with_formatting(cell)
|
130
184
|
row_cells.append(cell_text or "")
|
131
185
|
|
132
186
|
row = "| " + " | ".join(row_cells) + " |"
|
133
187
|
table_rows.append(row)
|
134
188
|
|
135
|
-
if not header_processed and table_data.
|
189
|
+
if not header_processed and table_data.has_column_header:
|
136
190
|
header_processed = True
|
137
191
|
separator = (
|
138
192
|
"| " + " | ".join(["--------" for _ in range(len(cells))]) + " |"
|
139
193
|
)
|
140
194
|
table_rows.append(separator)
|
141
195
|
|
142
|
-
if not table_rows:
|
143
|
-
return None
|
144
|
-
|
145
|
-
if len(table_rows) == 1 and table_data.get("has_column_header", True):
|
146
|
-
cells_count = len(children[0].get("table_row", {}).get("cells", []))
|
147
|
-
separator = (
|
148
|
-
"| " + " | ".join(["--------" for _ in range(cells_count)]) + " |"
|
149
|
-
)
|
150
|
-
table_rows.insert(1, separator)
|
151
|
-
|
152
196
|
return "\n".join(table_rows)
|
153
197
|
|
154
198
|
@classmethod
|
155
|
-
def
|
156
|
-
"""Indicates if this element handles content that spans multiple lines."""
|
157
|
-
return True
|
158
|
-
|
159
|
-
@classmethod
|
160
|
-
def _find_table_start(cls, lines: List[str]) -> Optional[int]:
|
161
|
-
"""Find the start index of a table in the lines."""
|
162
|
-
for i in range(len(lines) - 2):
|
163
|
-
if (
|
164
|
-
TableElement.ROW_PATTERN.match(lines[i])
|
165
|
-
and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
|
166
|
-
and TableElement.ROW_PATTERN.match(lines[i + 2])
|
167
|
-
):
|
168
|
-
return i
|
169
|
-
return None
|
170
|
-
|
171
|
-
@classmethod
|
172
|
-
def _find_table_end(cls, lines: List[str], start_idx: int) -> int:
|
173
|
-
"""Find the end index of a table, starting from start_idx."""
|
174
|
-
end_idx = start_idx + 3 # Minimum: Header, Separator, one data row
|
175
|
-
while end_idx < len(lines) and TableElement.ROW_PATTERN.match(lines[end_idx]):
|
176
|
-
end_idx += 1
|
177
|
-
return end_idx
|
178
|
-
|
179
|
-
@classmethod
|
180
|
-
def _extract_table_rows(cls, table_lines: List[str]) -> List[List[str]]:
|
181
|
-
"""Extract row contents from table lines, excluding separator line."""
|
182
|
-
rows = []
|
183
|
-
for i, line in enumerate(table_lines):
|
184
|
-
if i != 1 and TableElement.ROW_PATTERN.match(line): # Skip separator line
|
185
|
-
cells = TableElement._parse_table_row(line)
|
186
|
-
if cells:
|
187
|
-
rows.append(cells)
|
188
|
-
return rows
|
189
|
-
|
190
|
-
@classmethod
|
191
|
-
def _normalize_row_lengths(cls, rows: List[List[str]], column_count: int) -> None:
|
192
|
-
"""Normalize row lengths to the specified column count."""
|
193
|
-
for row in rows:
|
194
|
-
if len(row) < column_count:
|
195
|
-
row.extend([""] * (column_count - len(row)))
|
196
|
-
elif len(row) > column_count:
|
197
|
-
del row[column_count:]
|
198
|
-
|
199
|
-
@classmethod
|
200
|
-
def _parse_table_row(cls, row_text: str) -> List[str]:
|
199
|
+
def _parse_table_row(cls, row_text: str) -> list[str]:
|
201
200
|
"""Convert table row text to cell contents."""
|
202
201
|
row_content = row_text.strip()
|
203
202
|
|
@@ -209,109 +208,18 @@ class TableElement(NotionBlockElement):
|
|
209
208
|
return [cell.strip() for cell in row_content.split("|")]
|
210
209
|
|
211
210
|
@classmethod
|
212
|
-
def
|
213
|
-
"""
|
214
|
-
|
215
|
-
|
216
|
-
for row in rows:
|
217
|
-
cells_data = []
|
218
|
-
|
219
|
-
for cell_content in row:
|
220
|
-
rich_text = TextInlineFormatter.parse_inline_formatting(cell_content)
|
221
|
-
|
222
|
-
if not rich_text:
|
223
|
-
rich_text = [
|
224
|
-
{
|
225
|
-
"type": "text",
|
226
|
-
"text": {"content": ""},
|
227
|
-
"annotations": {
|
228
|
-
"bold": False,
|
229
|
-
"italic": False,
|
230
|
-
"strikethrough": False,
|
231
|
-
"underline": False,
|
232
|
-
"code": False,
|
233
|
-
"color": "default",
|
234
|
-
},
|
235
|
-
"plain_text": "",
|
236
|
-
"href": None,
|
237
|
-
}
|
238
|
-
]
|
239
|
-
|
240
|
-
cells_data.append(rich_text)
|
241
|
-
|
242
|
-
table_rows.append({"type": "table_row", "table_row": {"cells": cells_data}})
|
243
|
-
|
244
|
-
return table_rows
|
245
|
-
|
246
|
-
@classmethod
|
247
|
-
def find_matches(cls, text: str) -> List[Tuple[int, int, Dict[str, Any]]]:
|
248
|
-
"""
|
249
|
-
Find all tables in the text and return their positions.
|
250
|
-
|
251
|
-
Args:
|
252
|
-
text: The text to search in
|
253
|
-
|
254
|
-
Returns:
|
255
|
-
List of tuples with (start_pos, end_pos, block)
|
256
|
-
"""
|
257
|
-
matches = []
|
258
|
-
lines = text.split("\n")
|
259
|
-
|
260
|
-
i = 0
|
261
|
-
while i < len(lines) - 2:
|
262
|
-
if (
|
263
|
-
TableElement.ROW_PATTERN.match(lines[i])
|
264
|
-
and TableElement.SEPARATOR_PATTERN.match(lines[i + 1])
|
265
|
-
and TableElement.ROW_PATTERN.match(lines[i + 2])
|
266
|
-
):
|
267
|
-
|
268
|
-
start_line = i
|
269
|
-
end_line = TableElement._find_table_end(lines, start_line)
|
270
|
-
|
271
|
-
start_pos = TableElement._calculate_position(lines, 0, start_line)
|
272
|
-
end_pos = start_pos + TableElement._calculate_position(
|
273
|
-
lines, start_line, end_line
|
274
|
-
)
|
275
|
-
|
276
|
-
table_text = "\n".join(lines[start_line:end_line])
|
277
|
-
table_block = TableElement.markdown_to_notion(table_text)
|
278
|
-
|
279
|
-
if table_block:
|
280
|
-
matches.append((start_pos, end_pos, table_block))
|
281
|
-
|
282
|
-
i = end_line
|
283
|
-
else:
|
284
|
-
i += 1
|
285
|
-
|
286
|
-
return matches
|
287
|
-
|
288
|
-
@classmethod
|
289
|
-
def _calculate_position(cls, lines: List[str], start: int, end: int) -> int:
|
290
|
-
"""Calculate the text position in characters from line start to end."""
|
291
|
-
position = 0
|
292
|
-
for i in range(start, end):
|
293
|
-
position += len(lines[i]) + 1 # +1 for newline
|
294
|
-
return position
|
211
|
+
def is_table_row(cls, line: str) -> bool:
|
212
|
+
"""Check if a line is a valid table row."""
|
213
|
+
return bool(cls.ROW_PATTERN.match(line.strip()))
|
295
214
|
|
296
215
|
@classmethod
|
297
|
-
def
|
298
|
-
"""
|
299
|
-
return (
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
)
|
307
|
-
.with_syntax(
|
308
|
-
"| Header 1 | Header 2 | Header 3 |\n| -------- | -------- | -------- |\n| Cell 1 | Cell 2 | Cell 3 |"
|
309
|
-
)
|
310
|
-
.with_examples(
|
311
|
-
[
|
312
|
-
"| Product | Price | Stock |\n| ------- | ----- | ----- |\n| Widget A | $10.99 | 42 |\n| Widget B | $14.99 | 27 |",
|
313
|
-
"| Name | Role | Department |\n| ---- | ---- | ---------- |\n| John Smith | Manager | Marketing |\n| Jane Doe | Director | Sales |",
|
314
|
-
]
|
315
|
-
)
|
316
|
-
.build()
|
216
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
217
|
+
"""Get system prompt information for table blocks."""
|
218
|
+
return BlockElementMarkdownInformation(
|
219
|
+
block_type=cls.__name__,
|
220
|
+
description="Table blocks create structured data in rows and columns with headers",
|
221
|
+
syntax_examples=[
|
222
|
+
"| Name | Age | City |\n| -------- | -------- | -------- |\n| Alice | 25 | Berlin |\n| Bob | 30 | Munich |"
|
223
|
+
],
|
224
|
+
usage_guidelines="Use for structured data presentation. First row is header, second row is separator with dashes, following rows are data. Cells are separated by | characters.",
|
317
225
|
)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import Literal
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
8
|
+
|
9
|
+
|
10
|
+
class TableBlock(BaseModel):
|
11
|
+
table_width: int
|
12
|
+
has_column_header: bool = False
|
13
|
+
has_row_header: bool = False
|
14
|
+
children: list[CreateTableRowBlock] = []
|
15
|
+
|
16
|
+
|
17
|
+
class TableRowBlock(BaseModel):
|
18
|
+
cells: list[list[RichTextObject]]
|
19
|
+
|
20
|
+
|
21
|
+
class CreateTableRowBlock(BaseModel):
|
22
|
+
type: Literal["table_row"] = "table_row"
|
23
|
+
table_row: TableRowBlock
|
24
|
+
|
25
|
+
|
26
|
+
class CreateTableBlock(BaseModel):
|
27
|
+
type: Literal["table"] = "table"
|
28
|
+
table: TableBlock
|
@@ -0,0 +1,19 @@
|
|
1
|
+
from notionary.blocks.table_of_contents.table_of_contents_element import (
|
2
|
+
TableOfContentsElement,
|
3
|
+
)
|
4
|
+
from notionary.blocks.table_of_contents.table_of_contents_markdown_node import (
|
5
|
+
TableOfContentsMarkdownBlockParams,
|
6
|
+
TableOfContentsMarkdownNode,
|
7
|
+
)
|
8
|
+
from notionary.blocks.table_of_contents.table_of_contents_models import (
|
9
|
+
CreateTableOfContentsBlock,
|
10
|
+
TableOfContentsBlock,
|
11
|
+
)
|
12
|
+
|
13
|
+
__all__ = [
|
14
|
+
"TableOfContentsElement",
|
15
|
+
"TableOfContentsBlock",
|
16
|
+
"CreateTableOfContentsBlock",
|
17
|
+
"TableOfContentsMarkdownNode",
|
18
|
+
"TableOfContentsMarkdownBlockParams",
|
19
|
+
]
|