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
@@ -1,98 +1,93 @@
|
|
1
|
-
import
|
2
|
-
from typing import Dict, Any, Optional
|
1
|
+
from __future__ import annotations
|
3
2
|
|
4
|
-
|
5
|
-
from
|
6
|
-
|
7
|
-
|
8
|
-
|
3
|
+
import re
|
4
|
+
from typing import Optional, cast
|
5
|
+
|
6
|
+
from notionary.blocks.base_block_element import BaseBlockElement
|
7
|
+
from notionary.blocks.heading.heading_models import (
|
8
|
+
CreateHeading1Block,
|
9
|
+
CreateHeading2Block,
|
10
|
+
CreateHeading3Block,
|
11
|
+
HeadingBlock,
|
9
12
|
)
|
10
|
-
from notionary.blocks.
|
13
|
+
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
14
|
+
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
15
|
+
from notionary.blocks.types import BlockColor
|
11
16
|
|
12
17
|
|
13
|
-
class HeadingElement(
|
18
|
+
class HeadingElement(BaseBlockElement):
|
14
19
|
"""Handles conversion between Markdown headings and Notion heading blocks."""
|
15
20
|
|
16
|
-
# Pattern: #, ## oder ###, dann mind. 1 Leerzeichen/Tab, dann mind. 1 sichtbares Zeichen (kein Whitespace-only)
|
17
21
|
PATTERN = re.compile(r"^(#{1,3})[ \t]+(.+)$")
|
18
22
|
|
19
23
|
@classmethod
|
20
|
-
def
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
"""Check if block is a Notion heading."""
|
31
|
-
block_type: str = block.get("type", "")
|
32
|
-
return block_type.startswith("heading_") and block_type[-1] in "123"
|
24
|
+
def match_notion(cls, block: Block) -> bool:
|
25
|
+
return (
|
26
|
+
block.type
|
27
|
+
in (
|
28
|
+
BlockType.HEADING_1,
|
29
|
+
BlockType.HEADING_2,
|
30
|
+
BlockType.HEADING_3,
|
31
|
+
)
|
32
|
+
and getattr(block, block.type.value) is not None
|
33
|
+
)
|
33
34
|
|
34
35
|
@classmethod
|
35
|
-
def markdown_to_notion(cls, text: str) ->
|
36
|
-
"""Convert markdown
|
37
|
-
match = cls.PATTERN.match(text)
|
36
|
+
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
37
|
+
"""Convert markdown headings (#, ##, ###) to Notion HeadingBlock."""
|
38
|
+
match = cls.PATTERN.match(text.strip())
|
38
39
|
if not match:
|
39
40
|
return None
|
40
41
|
|
41
42
|
level = len(match.group(1))
|
42
|
-
if
|
43
|
+
if level < 1 or level > 3:
|
44
|
+
return None
|
45
|
+
|
46
|
+
content = match.group(2).strip()
|
47
|
+
if not content:
|
43
48
|
return None
|
44
49
|
|
45
|
-
|
46
|
-
|
47
|
-
|
50
|
+
rich_text = TextInlineFormatter.parse_inline_formatting(content)
|
51
|
+
heading_content = HeadingBlock(
|
52
|
+
rich_text=rich_text, color=BlockColor.DEFAULT, is_toggleable=False
|
53
|
+
)
|
48
54
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
return [header_block]
|
55
|
+
if level == 1:
|
56
|
+
return CreateHeading1Block(heading_1=heading_content)
|
57
|
+
elif level == 2:
|
58
|
+
return CreateHeading2Block(heading_2=heading_content)
|
59
|
+
else:
|
60
|
+
return CreateHeading3Block(heading_3=heading_content)
|
56
61
|
|
57
62
|
@classmethod
|
58
|
-
def notion_to_markdown(cls, block:
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
+
def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
64
|
+
# Only handle heading blocks via BlockType enum
|
65
|
+
if block.type not in (
|
66
|
+
BlockType.HEADING_1,
|
67
|
+
BlockType.HEADING_2,
|
68
|
+
BlockType.HEADING_3,
|
69
|
+
):
|
63
70
|
return None
|
64
71
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
72
|
+
# Determine heading level from enum
|
73
|
+
if block.type == BlockType.HEADING_1:
|
74
|
+
level = 1
|
75
|
+
elif block.type == BlockType.HEADING_2:
|
76
|
+
level = 2
|
77
|
+
else:
|
78
|
+
level = 3
|
71
79
|
|
72
|
-
|
73
|
-
|
80
|
+
heading_obj = getattr(block, block.type.value)
|
81
|
+
if not heading_obj:
|
82
|
+
return None
|
74
83
|
|
75
|
-
|
76
|
-
|
77
|
-
|
84
|
+
heading_data = cast(HeadingBlock, heading_obj)
|
85
|
+
if not heading_data.rich_text:
|
86
|
+
return None
|
78
87
|
|
79
|
-
|
80
|
-
|
81
|
-
|
88
|
+
text = TextInlineFormatter.extract_text_with_formatting(heading_data.rich_text)
|
89
|
+
if not text:
|
90
|
+
return None
|
82
91
|
|
83
|
-
|
84
|
-
|
85
|
-
return (
|
86
|
-
ElementPromptBuilder()
|
87
|
-
.with_description(
|
88
|
-
"Use Markdown headings (#, ##, ###) to structure content hierarchically."
|
89
|
-
)
|
90
|
-
.with_usage_guidelines(
|
91
|
-
"Use to group content into sections and define a visual hierarchy."
|
92
|
-
)
|
93
|
-
.with_avoidance_guidelines(
|
94
|
-
"Only H1-H3 syntax is supported. H4 and deeper heading levels are not available."
|
95
|
-
)
|
96
|
-
.with_standard_markdown()
|
97
|
-
.build()
|
98
|
-
)
|
92
|
+
# Use hash-style for all heading levels
|
93
|
+
return f"{('#' * level)} {text}"
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from typing import Literal
|
2
|
+
|
3
|
+
from pydantic import BaseModel, Field
|
4
|
+
|
5
|
+
from notionary.blocks.models import Block
|
6
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
7
|
+
from notionary.blocks.types import BlockColor
|
8
|
+
|
9
|
+
|
10
|
+
class HeadingBlock(BaseModel):
|
11
|
+
rich_text: list[RichTextObject]
|
12
|
+
color: BlockColor = BlockColor.DEFAULT
|
13
|
+
is_toggleable: bool = False
|
14
|
+
children: list[Block] = Field(default_factory=list)
|
15
|
+
|
16
|
+
|
17
|
+
class CreateHeading1Block(BaseModel):
|
18
|
+
type: Literal["heading_1"] = "heading_1"
|
19
|
+
heading_1: HeadingBlock
|
20
|
+
|
21
|
+
|
22
|
+
class CreateHeading2Block(BaseModel):
|
23
|
+
type: Literal["heading_2"] = "heading_2"
|
24
|
+
heading_2: HeadingBlock
|
25
|
+
|
26
|
+
|
27
|
+
class CreateHeading3Block(BaseModel):
|
28
|
+
type: Literal["heading_3"] = "heading_3"
|
29
|
+
heading_3: HeadingBlock
|
@@ -0,0 +1,13 @@
|
|
1
|
+
from notionary.blocks.image_block.image_element import ImageElement
|
2
|
+
from notionary.blocks.image_block.image_markdown_node import (
|
3
|
+
ImageMarkdownBlockParams,
|
4
|
+
ImageMarkdownNode,
|
5
|
+
)
|
6
|
+
from notionary.blocks.image_block.image_models import CreateImageBlock
|
7
|
+
|
8
|
+
__all__ = [
|
9
|
+
"ImageElement",
|
10
|
+
"CreateImageBlock",
|
11
|
+
"ImageMarkdownNode",
|
12
|
+
"ImageMarkdownBlockParams",
|
13
|
+
]
|
@@ -0,0 +1,84 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import re
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from notionary.blocks.base_block_element import BaseBlockElement
|
7
|
+
from notionary.blocks.file.file_element_models import ExternalFile, FileType
|
8
|
+
from notionary.blocks.image_block.image_models import CreateImageBlock, FileBlock
|
9
|
+
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
10
|
+
from notionary.blocks.paragraph.paragraph_models import (
|
11
|
+
CreateParagraphBlock,
|
12
|
+
ParagraphBlock,
|
13
|
+
)
|
14
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
15
|
+
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
16
|
+
|
17
|
+
|
18
|
+
class ImageElement(BaseBlockElement):
|
19
|
+
"""
|
20
|
+
Handles conversion between Markdown images and Notion image blocks.
|
21
|
+
|
22
|
+
Markdown image syntax:
|
23
|
+
- [image](https://example.com/image.jpg) - URL only
|
24
|
+
- [image](https://example.com/image.jpg "Caption") - URL + caption
|
25
|
+
"""
|
26
|
+
|
27
|
+
PATTERN = re.compile(
|
28
|
+
r"^\[image\]\(" # prefix
|
29
|
+
r"(https?://[^\s\"]+)" # URL (exclude whitespace and ")
|
30
|
+
r"(?:\s+\"([^\"]+)\")?" # optional caption
|
31
|
+
r"\)$"
|
32
|
+
)
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def match_notion(cls, block: Block) -> bool:
|
36
|
+
return block.type == BlockType.IMAGE and block.image
|
37
|
+
|
38
|
+
@classmethod
|
39
|
+
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
40
|
+
"""Convert markdown image syntax to Notion ImageBlock followed by an empty paragraph."""
|
41
|
+
match = cls.PATTERN.match(text.strip())
|
42
|
+
if not match:
|
43
|
+
return None
|
44
|
+
|
45
|
+
url, caption_text = match.group(1), match.group(2) or ""
|
46
|
+
# Build ImageBlock
|
47
|
+
image_block = FileBlock(
|
48
|
+
type="external", external=ExternalFile(url=url), caption=[]
|
49
|
+
)
|
50
|
+
if caption_text.strip():
|
51
|
+
rt = RichTextObject.from_plain_text(caption_text.strip())
|
52
|
+
image_block.caption = [rt]
|
53
|
+
|
54
|
+
empty_para = ParagraphBlock(rich_text=[])
|
55
|
+
|
56
|
+
return [
|
57
|
+
CreateImageBlock(image=image_block),
|
58
|
+
CreateParagraphBlock(paragraph=empty_para),
|
59
|
+
]
|
60
|
+
|
61
|
+
@classmethod
|
62
|
+
def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
63
|
+
if block.type != BlockType.IMAGE or not block.image:
|
64
|
+
return None
|
65
|
+
|
66
|
+
fo = block.image
|
67
|
+
|
68
|
+
if fo.type == FileType.EXTERNAL and fo.external:
|
69
|
+
url = fo.external.url
|
70
|
+
elif fo.type == FileType.FILE and fo.file:
|
71
|
+
url = fo.file.url
|
72
|
+
else:
|
73
|
+
return None
|
74
|
+
|
75
|
+
captions = fo.caption or []
|
76
|
+
if not captions:
|
77
|
+
return f"[image]({url})"
|
78
|
+
|
79
|
+
caption_text = "".join(
|
80
|
+
(rt.plain_text or TextInlineFormatter.extract_text_with_formatting([rt]))
|
81
|
+
for rt in captions
|
82
|
+
)
|
83
|
+
|
84
|
+
return f'[image]({url} "{caption_text}")'
|
@@ -1,8 +1,10 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
from typing import Optional
|
4
|
+
|
4
5
|
from pydantic import BaseModel
|
5
|
-
|
6
|
+
|
7
|
+
from notionary.markdown.markdown_node import MarkdownNode
|
6
8
|
|
7
9
|
|
8
10
|
class ImageMarkdownBlockParams(BaseModel):
|
@@ -0,0 +1,172 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from typing import TYPE_CHECKING, Any, Literal, Optional, Union
|
4
|
+
|
5
|
+
from pydantic import BaseModel
|
6
|
+
|
7
|
+
from notionary.blocks.types import BlockType
|
8
|
+
|
9
|
+
if TYPE_CHECKING:
|
10
|
+
from notionary.blocks.bookmark import BookmarkBlock, CreateBookmarkBlock
|
11
|
+
from notionary.blocks.breadcrumbs import BreadcrumbBlock, CreateBreadcrumbBlock
|
12
|
+
from notionary.blocks.bulleted_list import (
|
13
|
+
BulletedListItemBlock,
|
14
|
+
CreateBulletedListItemBlock,
|
15
|
+
)
|
16
|
+
from notionary.blocks.callout import CalloutBlock, CreateCalloutBlock
|
17
|
+
from notionary.blocks.child_page import ChildPageBlock, CreateChildPageBlock
|
18
|
+
from notionary.blocks.code import CodeBlock, CreateCodeBlock
|
19
|
+
from notionary.blocks.column import (
|
20
|
+
ColumnBlock,
|
21
|
+
ColumnListBlock,
|
22
|
+
CreateColumnBlock,
|
23
|
+
CreateColumnListBlock,
|
24
|
+
)
|
25
|
+
from notionary.blocks.divider import CreateDividerBlock, DividerBlock
|
26
|
+
from notionary.blocks.embed import CreateEmbedBlock, EmbedBlock
|
27
|
+
from notionary.blocks.equation import CreateEquationBlock, EquationBlock
|
28
|
+
from notionary.blocks.file import CreateFileBlock, FileBlock
|
29
|
+
from notionary.blocks.heading import (
|
30
|
+
CreateHeading1Block,
|
31
|
+
CreateHeading2Block,
|
32
|
+
CreateHeading3Block,
|
33
|
+
HeadingBlock,
|
34
|
+
)
|
35
|
+
from notionary.blocks.image_block import CreateImageBlock
|
36
|
+
from notionary.blocks.numbered_list import (
|
37
|
+
CreateNumberedListItemBlock,
|
38
|
+
NumberedListItemBlock,
|
39
|
+
)
|
40
|
+
from notionary.blocks.paragraph import CreateParagraphBlock, ParagraphBlock
|
41
|
+
from notionary.blocks.pdf import CreatePdfBlock
|
42
|
+
from notionary.blocks.quote import CreateQuoteBlock, QuoteBlock
|
43
|
+
from notionary.blocks.table import CreateTableBlock, TableBlock, TableRowBlock
|
44
|
+
from notionary.blocks.table_of_contents import (
|
45
|
+
CreateTableOfContentsBlock,
|
46
|
+
TableOfContentsBlock,
|
47
|
+
)
|
48
|
+
from notionary.blocks.todo import CreateToDoBlock, ToDoBlock
|
49
|
+
from notionary.blocks.toggle import CreateToggleBlock, ToggleBlock
|
50
|
+
from notionary.blocks.video import CreateVideoBlock
|
51
|
+
|
52
|
+
|
53
|
+
class BlockChildrenResponse(BaseModel):
|
54
|
+
object: Literal["list"]
|
55
|
+
results: list["Block"]
|
56
|
+
next_cursor: Optional[str] = None
|
57
|
+
has_more: bool
|
58
|
+
type: Literal["block"]
|
59
|
+
block: dict = {}
|
60
|
+
request_id: str
|
61
|
+
|
62
|
+
|
63
|
+
class PageParent(BaseModel):
|
64
|
+
type: Literal["page_id"]
|
65
|
+
page_id: str
|
66
|
+
|
67
|
+
|
68
|
+
class DatabaseParent(BaseModel):
|
69
|
+
type: Literal["database_id"]
|
70
|
+
database_id: str
|
71
|
+
|
72
|
+
|
73
|
+
class BlockParent(BaseModel):
|
74
|
+
type: Literal["block_id"]
|
75
|
+
block_id: str
|
76
|
+
|
77
|
+
|
78
|
+
class WorkspaceParent(BaseModel):
|
79
|
+
type: Literal["workspace"]
|
80
|
+
workspace: bool = True
|
81
|
+
|
82
|
+
|
83
|
+
ParentObject = Union[PageParent, DatabaseParent, BlockParent, WorkspaceParent]
|
84
|
+
|
85
|
+
|
86
|
+
class PartialUser(BaseModel):
|
87
|
+
object: Literal["user"]
|
88
|
+
id: str
|
89
|
+
|
90
|
+
|
91
|
+
class Block(BaseModel):
|
92
|
+
object: Literal["block"]
|
93
|
+
id: str
|
94
|
+
parent: Optional[ParentObject] = None
|
95
|
+
type: BlockType
|
96
|
+
created_time: str
|
97
|
+
last_edited_time: str
|
98
|
+
created_by: PartialUser
|
99
|
+
last_edited_by: PartialUser
|
100
|
+
archived: bool = False
|
101
|
+
in_trash: bool = False
|
102
|
+
has_children: bool = False
|
103
|
+
|
104
|
+
children: Optional[list[Block]] = None
|
105
|
+
|
106
|
+
# Block type-specific content (only one will be populated based on type)
|
107
|
+
audio: Optional[FileBlock] = None
|
108
|
+
bookmark: Optional[BookmarkBlock] = None
|
109
|
+
breadcrumb: Optional[BreadcrumbBlock] = None
|
110
|
+
bulleted_list_item: Optional[BulletedListItemBlock] = None
|
111
|
+
callout: Optional[CalloutBlock] = None
|
112
|
+
child_page: Optional[ChildPageBlock] = None
|
113
|
+
code: Optional[CodeBlock] = None
|
114
|
+
column_list: Optional[ColumnListBlock] = None
|
115
|
+
column: Optional[ColumnBlock] = None
|
116
|
+
divider: Optional[DividerBlock] = None
|
117
|
+
embed: Optional[EmbedBlock] = None
|
118
|
+
equation: Optional[EquationBlock] = None
|
119
|
+
file: Optional[FileBlock] = None
|
120
|
+
heading_1: Optional[HeadingBlock] = None
|
121
|
+
heading_2: Optional[HeadingBlock] = None
|
122
|
+
heading_3: Optional[HeadingBlock] = None
|
123
|
+
image: Optional[FileBlock] = None
|
124
|
+
numbered_list_item: Optional[NumberedListItemBlock] = None
|
125
|
+
paragraph: Optional[ParagraphBlock] = None
|
126
|
+
quote: Optional[QuoteBlock] = None
|
127
|
+
table: Optional[TableBlock] = None
|
128
|
+
table_row: Optional[TableRowBlock] = None
|
129
|
+
to_do: Optional[ToDoBlock] = None
|
130
|
+
toggle: Optional[ToggleBlock] = None
|
131
|
+
video: Optional[FileBlock] = None
|
132
|
+
pdf: Optional[FileBlock] = None
|
133
|
+
table_of_contents: Optional[TableOfContentsBlock] = None
|
134
|
+
|
135
|
+
def get_block_content(self) -> Optional[Any]:
|
136
|
+
"""Get the content object for this block based on its type."""
|
137
|
+
return getattr(self, self.type, None)
|
138
|
+
|
139
|
+
|
140
|
+
if TYPE_CHECKING:
|
141
|
+
BlockCreateRequest = Union[
|
142
|
+
CreateBookmarkBlock,
|
143
|
+
CreateBreadcrumbBlock,
|
144
|
+
CreateBulletedListItemBlock,
|
145
|
+
CreateCalloutBlock,
|
146
|
+
CreateChildPageBlock,
|
147
|
+
CreateCodeBlock,
|
148
|
+
CreateColumnListBlock,
|
149
|
+
CreateColumnBlock,
|
150
|
+
CreateDividerBlock,
|
151
|
+
CreateEmbedBlock,
|
152
|
+
CreateEquationBlock,
|
153
|
+
CreateFileBlock,
|
154
|
+
CreateHeading1Block,
|
155
|
+
CreateHeading2Block,
|
156
|
+
CreateHeading3Block,
|
157
|
+
CreateImageBlock,
|
158
|
+
CreateNumberedListItemBlock,
|
159
|
+
CreateParagraphBlock,
|
160
|
+
CreateQuoteBlock,
|
161
|
+
CreateToDoBlock,
|
162
|
+
CreateToggleBlock,
|
163
|
+
CreateVideoBlock,
|
164
|
+
CreateTableOfContentsBlock,
|
165
|
+
CreatePdfBlock,
|
166
|
+
CreateTableBlock,
|
167
|
+
]
|
168
|
+
BlockCreateResult = Optional[Union[list[BlockCreateRequest], BlockCreateRequest]]
|
169
|
+
else:
|
170
|
+
# at runtime there are no typings anyway
|
171
|
+
BlockCreateRequest = Any
|
172
|
+
BlockCreateResult = Any
|
@@ -1,7 +1,17 @@
|
|
1
|
-
from .numbered_list_element import NumberedListElement
|
2
|
-
from .numbered_list_markdown_node import
|
1
|
+
from notionary.blocks.numbered_list.numbered_list_element import NumberedListElement
|
2
|
+
from notionary.blocks.numbered_list.numbered_list_markdown_node import (
|
3
|
+
NumberedListMarkdownBlockParams,
|
4
|
+
NumberedListMarkdownNode,
|
5
|
+
)
|
6
|
+
from notionary.blocks.numbered_list.numbered_list_models import (
|
7
|
+
CreateNumberedListItemBlock,
|
8
|
+
NumberedListItemBlock,
|
9
|
+
)
|
3
10
|
|
4
11
|
__all__ = [
|
5
12
|
"NumberedListElement",
|
13
|
+
"NumberedListItemBlock",
|
14
|
+
"CreateNumberedListItemBlock",
|
6
15
|
"NumberedListMarkdownNode",
|
16
|
+
"NumberedListMarkdownBlockParams",
|
7
17
|
]
|
@@ -1,73 +1,48 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import re
|
2
|
-
from typing import
|
3
|
-
|
4
|
-
from notionary.blocks import
|
5
|
-
|
6
|
-
|
7
|
-
|
4
|
+
from typing import Optional
|
5
|
+
|
6
|
+
from notionary.blocks.base_block_element import BaseBlockElement
|
7
|
+
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
8
|
+
from notionary.blocks.numbered_list.numbered_list_models import (
|
9
|
+
CreateNumberedListItemBlock,
|
10
|
+
NumberedListItemBlock,
|
8
11
|
)
|
9
|
-
from notionary.blocks.
|
12
|
+
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
13
|
+
from notionary.blocks.types import BlockColor
|
14
|
+
|
10
15
|
|
16
|
+
class NumberedListElement(BaseBlockElement):
|
17
|
+
"""Converts between Markdown numbered lists and Notion numbered list items."""
|
11
18
|
|
12
|
-
|
13
|
-
"""Class for converting between Markdown numbered lists and Notion numbered list items."""
|
19
|
+
PATTERN = re.compile(r"^\s*(\d+)\.\s+(.+)$")
|
14
20
|
|
15
21
|
@classmethod
|
16
|
-
def
|
17
|
-
|
18
|
-
pattern = re.compile(r"^\s*(\d+)\.\s+(.+)$")
|
19
|
-
numbered_match = pattern.match(text)
|
20
|
-
if not numbered_match:
|
21
|
-
return None
|
22
|
+
def match_notion(cls, block: Block) -> bool:
|
23
|
+
return block.type == BlockType.NUMBERED_LIST_ITEM and block.numbered_list_item
|
22
24
|
|
23
|
-
|
25
|
+
@classmethod
|
26
|
+
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
27
|
+
"""Convert markdown numbered list item to Notion NumberedListItemBlock."""
|
28
|
+
match = cls.PATTERN.match(text.strip())
|
29
|
+
if not match:
|
30
|
+
return None
|
24
31
|
|
25
|
-
|
32
|
+
content = match.group(2)
|
26
33
|
rich_text = TextInlineFormatter.parse_inline_formatting(content)
|
27
34
|
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
35
|
+
numbered_list_content = NumberedListItemBlock(
|
36
|
+
rich_text=rich_text, color=BlockColor.DEFAULT
|
37
|
+
)
|
38
|
+
return CreateNumberedListItemBlock(numbered_list_item=numbered_list_content)
|
32
39
|
|
40
|
+
# FIX: Roundtrip conversions will never work this way here
|
33
41
|
@classmethod
|
34
|
-
def notion_to_markdown(cls, block:
|
35
|
-
|
36
|
-
if block.get("type") != "numbered_list_item":
|
42
|
+
def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
43
|
+
if block.type != BlockType.NUMBERED_LIST_ITEM or not block.numbered_list_item:
|
37
44
|
return None
|
38
45
|
|
39
|
-
|
40
|
-
content = TextInlineFormatter.extract_text_with_formatting(
|
41
|
-
|
46
|
+
rich = block.numbered_list_item.rich_text
|
47
|
+
content = TextInlineFormatter.extract_text_with_formatting(rich)
|
42
48
|
return f"1. {content}"
|
43
|
-
|
44
|
-
@classmethod
|
45
|
-
def match_markdown(cls, text: str) -> bool:
|
46
|
-
"""Check if this element can handle the given markdown text."""
|
47
|
-
pattern = re.compile(r"^\s*\d+\.\s+(.+)$")
|
48
|
-
return bool(pattern.match(text))
|
49
|
-
|
50
|
-
@classmethod
|
51
|
-
def match_notion(cls, block: dict[str, Any]) -> bool:
|
52
|
-
"""Check if this element can handle the given Notion block."""
|
53
|
-
return block.get("type") == "numbered_list_item"
|
54
|
-
|
55
|
-
@classmethod
|
56
|
-
def is_multiline(cls) -> bool:
|
57
|
-
return False
|
58
|
-
|
59
|
-
@classmethod
|
60
|
-
def get_llm_prompt_content(cls) -> ElementPromptContent:
|
61
|
-
"""
|
62
|
-
Returns structured LLM prompt metadata for the numbered list element.
|
63
|
-
"""
|
64
|
-
return (
|
65
|
-
ElementPromptBuilder()
|
66
|
-
.with_description("Creates numbered list items for ordered sequences.")
|
67
|
-
.with_usage_guidelines(
|
68
|
-
"Use for lists where order matters, such as steps, rankings, or sequential items."
|
69
|
-
)
|
70
|
-
.with_syntax("1. Item text")
|
71
|
-
.with_standard_markdown()
|
72
|
-
.build()
|
73
|
-
)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
from pydantic import BaseModel, Field
|
2
|
+
from typing_extensions import Literal
|
3
|
+
|
4
|
+
from notionary.blocks.models import Block
|
5
|
+
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
6
|
+
from notionary.blocks.types import BlockColor
|
7
|
+
|
8
|
+
|
9
|
+
class NumberedListItemBlock(BaseModel):
|
10
|
+
rich_text: list[RichTextObject]
|
11
|
+
color: BlockColor = BlockColor.DEFAULT
|
12
|
+
children: list[Block] = Field(default_factory=list)
|
13
|
+
|
14
|
+
|
15
|
+
class CreateNumberedListItemBlock(BaseModel):
|
16
|
+
type: Literal["numbered_list_item"] = "numbered_list_item"
|
17
|
+
numbered_list_item: NumberedListItemBlock
|