notionary 0.2.18__py3-none-any.whl → 0.2.21__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notionary/__init__.py +8 -4
- notionary/base_notion_client.py +3 -1
- notionary/blocks/__init__.py +2 -91
- notionary/blocks/_bootstrap.py +263 -0
- notionary/blocks/audio/__init__.py +8 -2
- notionary/blocks/audio/audio_element.py +42 -104
- notionary/blocks/audio/audio_markdown_node.py +3 -1
- notionary/blocks/audio/audio_models.py +6 -55
- notionary/blocks/base_block_element.py +30 -0
- notionary/blocks/bookmark/__init__.py +9 -2
- notionary/blocks/bookmark/bookmark_element.py +46 -139
- notionary/blocks/bookmark/bookmark_markdown_node.py +3 -1
- 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 +40 -55
- 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 +40 -89
- notionary/blocks/callout/callout_markdown_node.py +3 -1
- notionary/blocks/callout/callout_models.py +33 -0
- notionary/blocks/child_database/__init__.py +7 -0
- notionary/blocks/child_database/child_database_models.py +19 -0
- notionary/blocks/child_page/__init__.py +9 -0
- notionary/blocks/child_page/child_page_models.py +12 -0
- notionary/blocks/{shared/block_client.py → client.py} +55 -54
- notionary/blocks/code/__init__.py +6 -2
- notionary/blocks/code/code_element.py +53 -187
- notionary/blocks/code/code_markdown_node.py +13 -13
- notionary/blocks/code/code_models.py +94 -0
- notionary/blocks/column/__init__.py +25 -1
- notionary/blocks/column/column_element.py +40 -314
- notionary/blocks/column/column_list_element.py +37 -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 +26 -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 +47 -114
- 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 +80 -0
- notionary/blocks/equation/equation_element_markdown_node.py +36 -0
- notionary/blocks/equation/equation_models.py +11 -0
- notionary/blocks/file/__init__.py +25 -0
- notionary/blocks/file/file_element.py +93 -0
- notionary/blocks/file/file_element_markdown_node.py +35 -0
- notionary/blocks/file/file_element_models.py +39 -0
- notionary/blocks/heading/__init__.py +16 -2
- notionary/blocks/heading/heading_element.py +67 -72
- 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 +84 -0
- notionary/blocks/{image → image_block}/image_markdown_node.py +3 -1
- notionary/blocks/image_block/image_models.py +10 -0
- notionary/blocks/models.py +172 -0
- notionary/blocks/numbered_list/__init__.py +12 -2
- notionary/blocks/numbered_list/numbered_list_element.py +33 -58
- 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 +27 -69
- 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 +91 -0
- notionary/blocks/pdf/pdf_markdown_node.py +35 -0
- notionary/blocks/pdf/pdf_models.py +11 -0
- notionary/blocks/quote/__init__.py +11 -2
- notionary/blocks/quote/quote_element.py +31 -65
- notionary/blocks/quote/quote_markdown_node.py +4 -1
- notionary/blocks/quote/quote_models.py +18 -0
- notionary/blocks/registry/__init__.py +4 -0
- notionary/blocks/registry/block_registry.py +75 -91
- notionary/blocks/registry/block_registry_builder.py +107 -59
- notionary/blocks/rich_text/__init__.py +33 -0
- notionary/blocks/rich_text/rich_text_models.py +188 -0
- notionary/blocks/rich_text/text_inline_formatter.py +125 -0
- notionary/blocks/table/__init__.py +16 -2
- notionary/blocks/table/table_element.py +48 -241
- 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 +51 -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 +38 -95
- 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 +57 -264
- notionary/blocks/toggle/toggle_markdown_node.py +24 -14
- notionary/blocks/toggle/toggle_models.py +17 -0
- notionary/blocks/toggleable_heading/__init__.py +6 -2
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +74 -244
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +26 -18
- notionary/blocks/types.py +61 -0
- notionary/blocks/video/__init__.py +8 -2
- notionary/blocks/video/video_element.py +67 -143
- notionary/blocks/video/video_element_models.py +10 -0
- notionary/blocks/video/video_markdown_node.py +3 -1
- notionary/database/client.py +3 -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 +2 -1
- notionary/file_upload/notion_file_upload.py +2 -3
- notionary/markdown/markdown_builder.py +722 -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 +9 -10
- notionary/page/models.py +327 -0
- notionary/page/notion_page.py +99 -52
- notionary/page/notion_text_length_utils.py +119 -0
- notionary/page/{content/page_content_writer.py → page_content_writer.py} +88 -38
- notionary/page/reader/handler/__init__.py +17 -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 +43 -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 +60 -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 +69 -0
- notionary/page/search_filter_builder.py +2 -1
- notionary/page/writer/handler/__init__.py +22 -0
- notionary/page/writer/handler/code_handler.py +100 -0
- notionary/page/writer/handler/column_handler.py +141 -0
- notionary/page/writer/handler/column_list_handler.py +139 -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 +92 -0
- notionary/page/writer/handler/table_handler.py +130 -0
- notionary/page/writer/handler/toggle_handler.py +153 -0
- notionary/page/writer/handler/toggleable_heading_handler.py +167 -0
- notionary/page/writer/markdown_to_notion_converter.py +76 -0
- notionary/telemetry/__init__.py +2 -2
- notionary/telemetry/service.py +4 -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 +3 -2
- notionary/user/notion_user_provider.py +1 -1
- 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 +3 -2
- {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/METADATA +12 -8
- notionary-0.2.21.dist-info/RECORD +185 -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/__init__.py +0 -0
- notionary/blocks/shared/models.py +0 -710
- 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/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
- 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/notion_text_length_utils.py +0 -87
- notionary/page/content/page_content_retriever.py +0 -52
- 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-0.2.18.dist-info/RECORD +0 -149
- /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/page/{content/markdown_whitespace_processor.py → markdown_whitespace_processor.py} +0 -0
- /notionary/{blocks/mention/mention_models.py → page/reader/handler/context.py} +0 -0
- {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/LICENSE +0 -0
- {notionary-0.2.18.dist-info → notionary-0.2.21.dist-info}/WHEEL +0 -0
@@ -0,0 +1,92 @@
|
|
1
|
+
from notionary.blocks.column.column_element import ColumnElement
|
2
|
+
from notionary.blocks.column.column_list_element import ColumnListElement
|
3
|
+
from notionary.blocks.models import BlockCreateRequest, BlockCreateResult
|
4
|
+
from notionary.page.writer.handler import LineHandler, LineProcessingContext
|
5
|
+
|
6
|
+
|
7
|
+
class RegularLineHandler(LineHandler):
|
8
|
+
"""Handles regular lines - respects parent contexts like columns."""
|
9
|
+
|
10
|
+
def _can_handle(self, context: LineProcessingContext) -> bool:
|
11
|
+
return context.line.strip()
|
12
|
+
|
13
|
+
def _process(self, context: LineProcessingContext) -> None:
|
14
|
+
if self._is_in_column_context(context):
|
15
|
+
self._add_to_column_context(context)
|
16
|
+
context.was_processed = True
|
17
|
+
context.should_continue = True
|
18
|
+
return
|
19
|
+
|
20
|
+
block_created = self._process_single_line_content(context)
|
21
|
+
if not block_created:
|
22
|
+
self._process_as_paragraph(context)
|
23
|
+
|
24
|
+
context.was_processed = True
|
25
|
+
|
26
|
+
def _is_in_column_context(self, context: LineProcessingContext) -> bool:
|
27
|
+
"""Check if we're inside a Column/ColumnList context."""
|
28
|
+
if not context.parent_stack:
|
29
|
+
return False
|
30
|
+
|
31
|
+
current_parent = context.parent_stack[-1]
|
32
|
+
return issubclass(
|
33
|
+
current_parent.element_type, (ColumnListElement, ColumnElement)
|
34
|
+
)
|
35
|
+
|
36
|
+
def _add_to_column_context(self, context: LineProcessingContext) -> None:
|
37
|
+
"""Add line as child to the current Column context."""
|
38
|
+
context.parent_stack[-1].add_child_line(context.line)
|
39
|
+
|
40
|
+
def _process_single_line_content(self, context: LineProcessingContext) -> bool:
|
41
|
+
"""Process a regular line for simple elements (lists, etc.)."""
|
42
|
+
for element in context.block_registry.get_elements():
|
43
|
+
# Skip all elements that have specialized handlers
|
44
|
+
from notionary.blocks.code import CodeElement
|
45
|
+
from notionary.blocks.paragraph import ParagraphElement
|
46
|
+
from notionary.blocks.table import TableElement
|
47
|
+
from notionary.blocks.toggle import ToggleElement
|
48
|
+
from notionary.blocks.toggleable_heading import ToggleableHeadingElement
|
49
|
+
|
50
|
+
specialized_elements = (
|
51
|
+
ColumnListElement,
|
52
|
+
ColumnElement,
|
53
|
+
ToggleElement,
|
54
|
+
ToggleableHeadingElement,
|
55
|
+
TableElement,
|
56
|
+
CodeElement,
|
57
|
+
ParagraphElement, # Skip paragraph to ensure equations are processed first
|
58
|
+
)
|
59
|
+
|
60
|
+
if issubclass(element, specialized_elements):
|
61
|
+
continue
|
62
|
+
|
63
|
+
result = element.markdown_to_notion(context.line)
|
64
|
+
if not result:
|
65
|
+
continue
|
66
|
+
|
67
|
+
blocks = self._normalize_to_list(result)
|
68
|
+
for block in blocks:
|
69
|
+
context.result_blocks.append(block)
|
70
|
+
|
71
|
+
return True
|
72
|
+
|
73
|
+
return False
|
74
|
+
|
75
|
+
def _process_as_paragraph(self, context: LineProcessingContext) -> None:
|
76
|
+
"""Process a line as a paragraph."""
|
77
|
+
from notionary.blocks.paragraph.paragraph_element import ParagraphElement
|
78
|
+
|
79
|
+
paragraph_element = ParagraphElement()
|
80
|
+
result = paragraph_element.markdown_to_notion(context.line)
|
81
|
+
|
82
|
+
if result:
|
83
|
+
blocks = self._normalize_to_list(result)
|
84
|
+
for block in blocks:
|
85
|
+
context.result_blocks.append(block)
|
86
|
+
|
87
|
+
@staticmethod
|
88
|
+
def _normalize_to_list(result: BlockCreateResult) -> list[BlockCreateRequest]:
|
89
|
+
"""Normalize the result to a list."""
|
90
|
+
if result is None:
|
91
|
+
return []
|
92
|
+
return result if isinstance(result, list) else [result]
|
@@ -0,0 +1,130 @@
|
|
1
|
+
import re
|
2
|
+
|
3
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
4
|
+
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
5
|
+
from notionary.blocks.table.table_element import TableElement
|
6
|
+
from notionary.blocks.table.table_models import CreateTableRowBlock, TableRowBlock
|
7
|
+
from notionary.page.writer.handler import LineHandler, LineProcessingContext
|
8
|
+
|
9
|
+
|
10
|
+
class TableHandler(LineHandler):
|
11
|
+
"""Handles table specific logic with batching."""
|
12
|
+
|
13
|
+
def __init__(self):
|
14
|
+
super().__init__()
|
15
|
+
self._table_row_pattern = re.compile(r"^\s*\|(.+)\|\s*$")
|
16
|
+
self._separator_pattern = re.compile(r"^\s*\|([\s\-:|]+)\|\s*$")
|
17
|
+
|
18
|
+
def _can_handle(self, context: LineProcessingContext) -> bool:
|
19
|
+
if self._is_inside_parent_context(context):
|
20
|
+
return False
|
21
|
+
return self._is_table_start(context)
|
22
|
+
|
23
|
+
def _process(self, context: LineProcessingContext) -> None:
|
24
|
+
if not self._is_table_start(context):
|
25
|
+
return
|
26
|
+
|
27
|
+
self._process_complete_table(context)
|
28
|
+
context.was_processed = True
|
29
|
+
context.should_continue = True
|
30
|
+
|
31
|
+
def _is_inside_parent_context(self, context: LineProcessingContext) -> bool:
|
32
|
+
"""Check if we're currently inside any parent context (toggle, heading, etc.)."""
|
33
|
+
return len(context.parent_stack) > 0
|
34
|
+
|
35
|
+
def _is_table_start(self, context: LineProcessingContext) -> bool:
|
36
|
+
"""Check if this line starts a table."""
|
37
|
+
return self._table_row_pattern.match(context.line.strip()) is not None
|
38
|
+
|
39
|
+
def _process_complete_table(self, context: LineProcessingContext) -> None:
|
40
|
+
"""Process the entire table in one go."""
|
41
|
+
# Create table element
|
42
|
+
table_element = TableElement()
|
43
|
+
result = table_element.markdown_to_notion(context.line)
|
44
|
+
if not result:
|
45
|
+
return
|
46
|
+
|
47
|
+
block = result if not isinstance(result, list) else result[0]
|
48
|
+
|
49
|
+
# Collect all table lines (including the current one)
|
50
|
+
table_lines = [context.line]
|
51
|
+
remaining_lines = context.get_remaining_lines()
|
52
|
+
lines_to_consume = 0
|
53
|
+
|
54
|
+
# Find all consecutive table rows
|
55
|
+
for i, line in enumerate(remaining_lines):
|
56
|
+
line_stripped = line.strip()
|
57
|
+
if not line_stripped:
|
58
|
+
# Empty line - continue to allow for spacing in tables
|
59
|
+
table_lines.append(line)
|
60
|
+
continue
|
61
|
+
|
62
|
+
if self._table_row_pattern.match(
|
63
|
+
line_stripped
|
64
|
+
) or self._separator_pattern.match(line_stripped):
|
65
|
+
table_lines.append(line)
|
66
|
+
else:
|
67
|
+
# Not a table line - stop here
|
68
|
+
lines_to_consume = i
|
69
|
+
break
|
70
|
+
else:
|
71
|
+
# Consumed all remaining lines
|
72
|
+
lines_to_consume = len(remaining_lines)
|
73
|
+
|
74
|
+
# Process the table content
|
75
|
+
table_rows, separator_found = self._process_table_lines(table_lines)
|
76
|
+
|
77
|
+
table = block.table
|
78
|
+
table.children = table_rows
|
79
|
+
table.has_column_header = bool(separator_found)
|
80
|
+
|
81
|
+
# Tell the main loop to skip the consumed lines
|
82
|
+
context.lines_consumed = lines_to_consume
|
83
|
+
context.result_blocks.append(block)
|
84
|
+
|
85
|
+
def _process_table_lines(
|
86
|
+
self, table_lines: list[str]
|
87
|
+
) -> tuple[list[CreateTableRowBlock], bool]:
|
88
|
+
"""Process all table lines and return rows and separator status."""
|
89
|
+
table_rows = []
|
90
|
+
separator_found = False
|
91
|
+
|
92
|
+
for line in table_lines:
|
93
|
+
line = line.strip()
|
94
|
+
if not line:
|
95
|
+
continue
|
96
|
+
|
97
|
+
if self._is_separator_line(line):
|
98
|
+
separator_found = True
|
99
|
+
continue
|
100
|
+
|
101
|
+
if self._table_row_pattern.match(line):
|
102
|
+
table_row = self._create_table_row_from_line(line)
|
103
|
+
table_rows.append(table_row)
|
104
|
+
|
105
|
+
return table_rows, separator_found
|
106
|
+
|
107
|
+
def _is_separator_line(self, line: str) -> bool:
|
108
|
+
return self._separator_pattern.match(line) is not None
|
109
|
+
|
110
|
+
def _create_table_row_from_line(self, line: str) -> CreateTableRowBlock:
|
111
|
+
cells = self._parse_table_row(line)
|
112
|
+
rich_text_cells = [self._convert_cell_to_rich_text(cell) for cell in cells]
|
113
|
+
table_row = TableRowBlock(cells=rich_text_cells)
|
114
|
+
return CreateTableRowBlock(table_row=table_row)
|
115
|
+
|
116
|
+
def _convert_cell_to_rich_text(self, cell: str) -> list[RichTextObject]:
|
117
|
+
rich_text = TextInlineFormatter.parse_inline_formatting(cell)
|
118
|
+
if not rich_text:
|
119
|
+
rich_text = [RichTextObject.from_plain_text(cell)]
|
120
|
+
return rich_text
|
121
|
+
|
122
|
+
def _parse_table_row(self, row_text: str) -> list[str]:
|
123
|
+
row_content = row_text.strip()
|
124
|
+
|
125
|
+
if row_content.startswith("|"):
|
126
|
+
row_content = row_content[1:]
|
127
|
+
if row_content.endswith("|"):
|
128
|
+
row_content = row_content[:-1]
|
129
|
+
|
130
|
+
return [cell.strip() for cell in row_content.split("|")]
|
@@ -0,0 +1,153 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
|
5
|
+
from notionary.blocks.toggle.toggle_element import ToggleElement
|
6
|
+
from notionary.page.writer.handler import (
|
7
|
+
LineHandler,
|
8
|
+
LineProcessingContext,
|
9
|
+
ParentBlockContext,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class ToggleHandler(LineHandler):
|
14
|
+
"""Handles regular toggle blocks with ultra-simplified +++ syntax."""
|
15
|
+
|
16
|
+
def __init__(self):
|
17
|
+
super().__init__()
|
18
|
+
self._start_pattern = re.compile(r"^[+]{3}\s+(.+)$", re.IGNORECASE)
|
19
|
+
self._end_pattern = re.compile(r"^[+]{3}\s*$")
|
20
|
+
|
21
|
+
def _can_handle(self, context: LineProcessingContext) -> bool:
|
22
|
+
return (
|
23
|
+
self._is_toggle_start(context)
|
24
|
+
or self._is_toggle_end(context)
|
25
|
+
or self._is_toggle_content(context)
|
26
|
+
)
|
27
|
+
|
28
|
+
def _process(self, context: LineProcessingContext) -> None:
|
29
|
+
# Explicit, readable branches (small duplication is acceptable)
|
30
|
+
if self._is_toggle_start(context):
|
31
|
+
self._start_toggle(context)
|
32
|
+
context.was_processed = True
|
33
|
+
context.should_continue = True
|
34
|
+
|
35
|
+
if self._is_toggle_end(context):
|
36
|
+
self._finalize_toggle(context)
|
37
|
+
context.was_processed = True
|
38
|
+
context.should_continue = True
|
39
|
+
|
40
|
+
if self._is_toggle_content(context):
|
41
|
+
self._add_toggle_content(context)
|
42
|
+
context.was_processed = True
|
43
|
+
context.should_continue = True
|
44
|
+
|
45
|
+
def _is_toggle_start(self, context: LineProcessingContext) -> bool:
|
46
|
+
"""Check if line starts a toggle (+++ Title)."""
|
47
|
+
line = context.line.strip()
|
48
|
+
|
49
|
+
# Must match our pattern
|
50
|
+
if not self._start_pattern.match(line):
|
51
|
+
return False
|
52
|
+
|
53
|
+
# But NOT match toggleable heading pattern (has # after +++)
|
54
|
+
toggleable_heading_pattern = re.compile(r"^[+]{3}#{1,3}\s+.+$", re.IGNORECASE)
|
55
|
+
if toggleable_heading_pattern.match(line):
|
56
|
+
return False
|
57
|
+
|
58
|
+
return True
|
59
|
+
|
60
|
+
def _is_toggle_end(self, context: LineProcessingContext) -> bool:
|
61
|
+
"""Check if we need to end a toggle (+++)."""
|
62
|
+
if not self._end_pattern.match(context.line.strip()):
|
63
|
+
return False
|
64
|
+
|
65
|
+
if not context.parent_stack:
|
66
|
+
return False
|
67
|
+
|
68
|
+
# Check if top of stack is a Toggle
|
69
|
+
current_parent = context.parent_stack[-1]
|
70
|
+
return issubclass(current_parent.element_type, ToggleElement)
|
71
|
+
|
72
|
+
def _start_toggle(self, context: LineProcessingContext) -> None:
|
73
|
+
"""Start a new toggle block."""
|
74
|
+
toggle_element = ToggleElement()
|
75
|
+
|
76
|
+
# Create the block
|
77
|
+
result = toggle_element.markdown_to_notion(context.line)
|
78
|
+
if not result:
|
79
|
+
return
|
80
|
+
|
81
|
+
block = result if not isinstance(result, list) else result[0]
|
82
|
+
|
83
|
+
# Push to parent stack
|
84
|
+
parent_context = ParentBlockContext(
|
85
|
+
block=block,
|
86
|
+
element_type=ToggleElement,
|
87
|
+
child_lines=[],
|
88
|
+
)
|
89
|
+
context.parent_stack.append(parent_context)
|
90
|
+
|
91
|
+
def _finalize_toggle(self, context: LineProcessingContext) -> None:
|
92
|
+
"""Finalize a toggle block and add it to result_blocks."""
|
93
|
+
toggle_context = context.parent_stack.pop()
|
94
|
+
|
95
|
+
if toggle_context.has_children():
|
96
|
+
all_children = self._get_all_children(
|
97
|
+
toggle_context, context.block_registry
|
98
|
+
)
|
99
|
+
toggle_context.block.toggle.children = all_children
|
100
|
+
|
101
|
+
# Check if we have a parent context to add this toggle to
|
102
|
+
if context.parent_stack:
|
103
|
+
# Add this toggle as a child block to the parent
|
104
|
+
parent_context = context.parent_stack[-1]
|
105
|
+
parent_context.add_child_block(toggle_context.block)
|
106
|
+
else:
|
107
|
+
# No parent, add to top level
|
108
|
+
context.result_blocks.append(toggle_context.block)
|
109
|
+
|
110
|
+
def _is_toggle_content(self, context: LineProcessingContext) -> bool:
|
111
|
+
"""Check if we're inside a toggle context and should handle content."""
|
112
|
+
if not context.parent_stack:
|
113
|
+
return False
|
114
|
+
|
115
|
+
current_parent = context.parent_stack[-1]
|
116
|
+
if not issubclass(current_parent.element_type, ToggleElement):
|
117
|
+
return False
|
118
|
+
|
119
|
+
# Handle all content inside toggle (not start/end patterns)
|
120
|
+
line = context.line.strip()
|
121
|
+
return not (self._start_pattern.match(line) or self._end_pattern.match(line))
|
122
|
+
|
123
|
+
def _add_toggle_content(self, context: LineProcessingContext) -> None:
|
124
|
+
"""Add content to the current toggle context."""
|
125
|
+
context.parent_stack[-1].add_child_line(context.line)
|
126
|
+
|
127
|
+
def _convert_children_text(self, text: str, block_registry) -> list:
|
128
|
+
"""Convert children text to blocks."""
|
129
|
+
from notionary.page.writer.markdown_to_notion_converter import (
|
130
|
+
MarkdownToNotionConverter,
|
131
|
+
)
|
132
|
+
|
133
|
+
if not text.strip():
|
134
|
+
return []
|
135
|
+
|
136
|
+
child_converter = MarkdownToNotionConverter(block_registry)
|
137
|
+
return child_converter._process_lines(text)
|
138
|
+
|
139
|
+
def _get_all_children(self, parent_context, block_registry) -> list:
|
140
|
+
"""Helper method to combine text-based and direct block children."""
|
141
|
+
children_blocks = []
|
142
|
+
|
143
|
+
# Process text lines
|
144
|
+
if parent_context.child_lines:
|
145
|
+
children_text = "\n".join(parent_context.child_lines)
|
146
|
+
text_blocks = self._convert_children_text(children_text, block_registry)
|
147
|
+
children_blocks.extend(text_blocks)
|
148
|
+
|
149
|
+
# Add direct blocks (like processed columns)
|
150
|
+
if hasattr(parent_context, "child_blocks") and parent_context.child_blocks:
|
151
|
+
children_blocks.extend(parent_context.child_blocks)
|
152
|
+
|
153
|
+
return children_blocks
|
@@ -0,0 +1,167 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
|
5
|
+
from notionary.blocks.models import BlockCreateRequest
|
6
|
+
from notionary.blocks.toggleable_heading.toggleable_heading_element import (
|
7
|
+
ToggleableHeadingElement,
|
8
|
+
)
|
9
|
+
from notionary.blocks.types import BlockType
|
10
|
+
from notionary.page.writer.handler import (
|
11
|
+
LineHandler,
|
12
|
+
LineProcessingContext,
|
13
|
+
ParentBlockContext,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class ToggleableHeadingHandler(LineHandler):
|
18
|
+
"""Handles toggleable heading blocks with +++# syntax."""
|
19
|
+
|
20
|
+
def __init__(self):
|
21
|
+
super().__init__()
|
22
|
+
self._start_pattern = re.compile(
|
23
|
+
r"^[+]{3}(?P<level>#{1,3})\s+(.+)$", re.IGNORECASE
|
24
|
+
)
|
25
|
+
# +++
|
26
|
+
self._end_pattern = re.compile(r"^[+]{3}\s*$")
|
27
|
+
|
28
|
+
def _can_handle(self, context: LineProcessingContext) -> bool:
|
29
|
+
return (
|
30
|
+
self._is_toggleable_heading_start(context)
|
31
|
+
or self._is_toggleable_heading_end(context)
|
32
|
+
or self._is_toggleable_heading_content(context)
|
33
|
+
)
|
34
|
+
|
35
|
+
def _process(self, context: LineProcessingContext) -> None:
|
36
|
+
"""Process toggleable heading start, end, or content with unified handling."""
|
37
|
+
|
38
|
+
def _handle(action):
|
39
|
+
action(context)
|
40
|
+
context.was_processed = True
|
41
|
+
context.should_continue = True
|
42
|
+
return True
|
43
|
+
|
44
|
+
if self._is_toggleable_heading_start(context):
|
45
|
+
return _handle(self._start_toggleable_heading)
|
46
|
+
if self._is_toggleable_heading_end(context):
|
47
|
+
return _handle(self._finalize_toggleable_heading)
|
48
|
+
if self._is_toggleable_heading_content(context):
|
49
|
+
return _handle(self._add_toggleable_heading_content)
|
50
|
+
|
51
|
+
def _is_toggleable_heading_start(self, context: LineProcessingContext) -> bool:
|
52
|
+
"""Check if line starts a toggleable heading (+++# "Title")."""
|
53
|
+
return self._start_pattern.match(context.line.strip()) is not None
|
54
|
+
|
55
|
+
def _is_toggleable_heading_end(self, context: LineProcessingContext) -> bool:
|
56
|
+
"""Check if we need to end a toggleable heading (+++)."""
|
57
|
+
if not self._end_pattern.match(context.line.strip()):
|
58
|
+
return False
|
59
|
+
|
60
|
+
if not context.parent_stack:
|
61
|
+
return False
|
62
|
+
|
63
|
+
# Check if top of stack is a ToggleableHeading
|
64
|
+
current_parent = context.parent_stack[-1]
|
65
|
+
return issubclass(current_parent.element_type, ToggleableHeadingElement)
|
66
|
+
|
67
|
+
def _start_toggleable_heading(self, context: LineProcessingContext) -> None:
|
68
|
+
"""Start a new toggleable heading block."""
|
69
|
+
toggleable_heading_element = ToggleableHeadingElement()
|
70
|
+
|
71
|
+
# Create the block
|
72
|
+
result = toggleable_heading_element.markdown_to_notion(context.line)
|
73
|
+
if not result:
|
74
|
+
return
|
75
|
+
|
76
|
+
block = result if not isinstance(result, list) else result[0]
|
77
|
+
|
78
|
+
# Push to parent stack
|
79
|
+
parent_context = ParentBlockContext(
|
80
|
+
block=block,
|
81
|
+
element_type=ToggleableHeadingElement,
|
82
|
+
child_lines=[],
|
83
|
+
)
|
84
|
+
context.parent_stack.append(parent_context)
|
85
|
+
|
86
|
+
def _is_toggleable_heading_content(self, context: LineProcessingContext) -> bool:
|
87
|
+
"""Check if we're inside a toggleable heading context and should handle content."""
|
88
|
+
if not context.parent_stack:
|
89
|
+
return False
|
90
|
+
|
91
|
+
current_parent = context.parent_stack[-1]
|
92
|
+
if not issubclass(current_parent.element_type, ToggleableHeadingElement):
|
93
|
+
return False
|
94
|
+
|
95
|
+
# Handle all content inside toggleable heading (not start/end patterns)
|
96
|
+
line = context.line.strip()
|
97
|
+
return not (self._start_pattern.match(line) or self._end_pattern.match(line))
|
98
|
+
|
99
|
+
def _add_toggleable_heading_content(self, context: LineProcessingContext) -> None:
|
100
|
+
"""Add content to the current toggleable heading context."""
|
101
|
+
context.parent_stack[-1].add_child_line(context.line)
|
102
|
+
|
103
|
+
def _finalize_toggleable_heading(self, context: LineProcessingContext) -> None:
|
104
|
+
"""Finalize a toggleable heading block and add it to result_blocks."""
|
105
|
+
heading_context = context.parent_stack.pop()
|
106
|
+
|
107
|
+
if heading_context.has_children():
|
108
|
+
all_children = self._get_all_children(
|
109
|
+
heading_context, context.block_registry
|
110
|
+
)
|
111
|
+
self._assign_heading_children(heading_context.block, all_children)
|
112
|
+
|
113
|
+
# Check if we have a parent context to add this heading to
|
114
|
+
if context.parent_stack:
|
115
|
+
# Add this heading as a child block to the parent
|
116
|
+
parent_context = context.parent_stack[-1]
|
117
|
+
if hasattr(parent_context, "add_child_block"):
|
118
|
+
parent_context.add_child_block(heading_context.block)
|
119
|
+
else:
|
120
|
+
# Fallback: add to result_blocks for backward compatibility
|
121
|
+
context.result_blocks.append(heading_context.block)
|
122
|
+
else:
|
123
|
+
# No parent, add to top level
|
124
|
+
context.result_blocks.append(heading_context.block)
|
125
|
+
|
126
|
+
def _get_all_children(
|
127
|
+
self, parent_context: ParentBlockContext, block_registry
|
128
|
+
) -> list:
|
129
|
+
"""Helper method to combine text-based and direct block children."""
|
130
|
+
children_blocks = []
|
131
|
+
|
132
|
+
# Process text lines
|
133
|
+
if parent_context.child_lines:
|
134
|
+
children_text = "\n".join(parent_context.child_lines)
|
135
|
+
text_blocks = self._convert_children_text(children_text, block_registry)
|
136
|
+
children_blocks.extend(text_blocks)
|
137
|
+
|
138
|
+
# Add direct blocks
|
139
|
+
if hasattr(parent_context, "child_blocks") and parent_context.child_blocks:
|
140
|
+
children_blocks.extend(parent_context.child_blocks)
|
141
|
+
|
142
|
+
return children_blocks
|
143
|
+
|
144
|
+
def _assign_heading_children(
|
145
|
+
self, parent_block: BlockCreateRequest, children: list[BlockCreateRequest]
|
146
|
+
) -> None:
|
147
|
+
"""Assign children to toggleable heading blocks."""
|
148
|
+
block_type = parent_block.type
|
149
|
+
|
150
|
+
if block_type == BlockType.HEADING_1:
|
151
|
+
parent_block.heading_1.children = children
|
152
|
+
elif block_type == BlockType.HEADING_2:
|
153
|
+
parent_block.heading_2.children = children
|
154
|
+
elif block_type == BlockType.HEADING_3:
|
155
|
+
parent_block.heading_3.children = children
|
156
|
+
|
157
|
+
def _convert_children_text(self, text: str, block_registry) -> list:
|
158
|
+
"""Convert children text to blocks."""
|
159
|
+
from notionary.page.writer.markdown_to_notion_converter import (
|
160
|
+
MarkdownToNotionConverter,
|
161
|
+
)
|
162
|
+
|
163
|
+
if not text.strip():
|
164
|
+
return []
|
165
|
+
|
166
|
+
child_converter = MarkdownToNotionConverter(block_registry)
|
167
|
+
return child_converter._process_lines(text)
|
@@ -0,0 +1,76 @@
|
|
1
|
+
from notionary.blocks.models import BlockCreateRequest
|
2
|
+
from notionary.blocks.registry.block_registry import BlockRegistry
|
3
|
+
from notionary.page.notion_text_length_utils import fix_blocks_content_length
|
4
|
+
from notionary.page.writer.handler import (
|
5
|
+
CodeHandler,
|
6
|
+
ColumnHandler,
|
7
|
+
ColumnListHandler,
|
8
|
+
LineProcessingContext,
|
9
|
+
ParentBlockContext,
|
10
|
+
RegularLineHandler,
|
11
|
+
TableHandler,
|
12
|
+
ToggleableHeadingHandler,
|
13
|
+
ToggleHandler,
|
14
|
+
)
|
15
|
+
|
16
|
+
|
17
|
+
class MarkdownToNotionConverter:
|
18
|
+
"""Converts Markdown text to Notion API block format with unified stack-based processing."""
|
19
|
+
|
20
|
+
def __init__(self, block_registry: BlockRegistry) -> None:
|
21
|
+
self._block_registry = block_registry
|
22
|
+
self._setup_handler_chain()
|
23
|
+
|
24
|
+
def _setup_handler_chain(self) -> None:
|
25
|
+
code_handler = CodeHandler()
|
26
|
+
table_handler = TableHandler()
|
27
|
+
column_list_handler = ColumnListHandler()
|
28
|
+
column_handler = ColumnHandler()
|
29
|
+
toggle_handler = ToggleHandler()
|
30
|
+
toggleable_heading_handler = ToggleableHeadingHandler()
|
31
|
+
regular_handler = RegularLineHandler()
|
32
|
+
|
33
|
+
# register more specific elements first
|
34
|
+
code_handler.set_next(table_handler).set_next(column_list_handler).set_next(
|
35
|
+
column_handler
|
36
|
+
).set_next(toggleable_heading_handler).set_next(toggle_handler).set_next(
|
37
|
+
regular_handler
|
38
|
+
)
|
39
|
+
|
40
|
+
self._handler_chain = code_handler
|
41
|
+
|
42
|
+
def convert(self, markdown_text: str) -> list[BlockCreateRequest]:
|
43
|
+
if not markdown_text.strip():
|
44
|
+
return []
|
45
|
+
|
46
|
+
all_blocks = self._process_lines(markdown_text)
|
47
|
+
return fix_blocks_content_length(all_blocks)
|
48
|
+
|
49
|
+
def _process_lines(self, text: str) -> list[BlockCreateRequest]:
|
50
|
+
lines = text.split("\n")
|
51
|
+
result_blocks: list[BlockCreateRequest] = []
|
52
|
+
parent_stack: list[ParentBlockContext] = []
|
53
|
+
|
54
|
+
i = 0
|
55
|
+
while i < len(lines):
|
56
|
+
line = lines[i]
|
57
|
+
|
58
|
+
context = LineProcessingContext(
|
59
|
+
line=line,
|
60
|
+
result_blocks=result_blocks,
|
61
|
+
parent_stack=parent_stack,
|
62
|
+
block_registry=self._block_registry,
|
63
|
+
all_lines=lines,
|
64
|
+
current_line_index=i,
|
65
|
+
lines_consumed=0,
|
66
|
+
)
|
67
|
+
|
68
|
+
self._handler_chain.handle(context)
|
69
|
+
|
70
|
+
# Skip consumed lines
|
71
|
+
i += 1 + context.lines_consumed
|
72
|
+
|
73
|
+
if context.should_continue:
|
74
|
+
continue
|
75
|
+
|
76
|
+
return result_blocks
|
notionary/telemetry/__init__.py
CHANGED
@@ -2,10 +2,10 @@ from .service import ProductTelemetry
|
|
2
2
|
from .views import (
|
3
3
|
BaseTelemetryEvent,
|
4
4
|
DatabaseFactoryUsedEvent,
|
5
|
-
QueryOperationEvent,
|
6
|
-
NotionMarkdownSyntaxPromptEvent,
|
7
5
|
MarkdownToNotionConversionEvent,
|
6
|
+
NotionMarkdownSyntaxPromptEvent,
|
8
7
|
NotionToMarkdownConversionEvent,
|
8
|
+
QueryOperationEvent,
|
9
9
|
)
|
10
10
|
|
11
11
|
__all__ = [
|
notionary/telemetry/service.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
import os
|
2
2
|
import uuid
|
3
3
|
from pathlib import Path
|
4
|
-
from typing import
|
5
|
-
|
4
|
+
from typing import Any, Dict, Optional
|
5
|
+
|
6
6
|
from dotenv import load_dotenv
|
7
|
+
from posthog import Posthog
|
7
8
|
|
8
9
|
from notionary.telemetry.views import BaseTelemetryEvent
|
9
|
-
from notionary.util import
|
10
|
+
from notionary.util import LoggingMixin, SingletonMetaClass
|
10
11
|
|
11
12
|
load_dotenv()
|
12
13
|
|
notionary/user/__init__.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
from .notion_user import NotionUser
|
2
|
-
from .notion_user_manager import NotionUserManager
|
3
1
|
from .client import NotionUserClient
|
4
2
|
from .notion_bot_user import NotionBotUser
|
3
|
+
from .notion_user import NotionUser
|
4
|
+
from .notion_user_manager import NotionUserManager
|
5
5
|
|
6
6
|
__all__ = [
|
7
7
|
"NotionUser",
|
notionary/user/client.py
CHANGED
@@ -1,14 +1,13 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import List, Optional
|
2
|
+
|
2
3
|
from notionary.base_notion_client import BaseNotionClient
|
3
4
|
from notionary.user.models import (
|
4
5
|
NotionBotUserResponse,
|
5
6
|
NotionUserResponse,
|
6
7
|
NotionUsersListResponse,
|
7
8
|
)
|
8
|
-
from notionary.util import singleton
|
9
9
|
|
10
10
|
|
11
|
-
@singleton
|
12
11
|
class NotionUserClient(BaseNotionClient):
|
13
12
|
"""
|
14
13
|
Client for Notion user-specific operations.
|