notionary 0.2.21__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/blocks/_bootstrap.py +9 -1
- notionary/blocks/audio/audio_element.py +53 -28
- notionary/blocks/audio/audio_markdown_node.py +10 -4
- notionary/blocks/base_block_element.py +15 -3
- notionary/blocks/bookmark/bookmark_element.py +39 -36
- notionary/blocks/bookmark/bookmark_markdown_node.py +16 -17
- notionary/blocks/breadcrumbs/breadcrumb_element.py +2 -2
- notionary/blocks/bulleted_list/bulleted_list_element.py +21 -4
- notionary/blocks/callout/callout_element.py +20 -4
- notionary/blocks/child_database/__init__.py +11 -4
- notionary/blocks/child_database/child_database_element.py +61 -0
- notionary/blocks/child_database/child_database_models.py +7 -14
- notionary/blocks/child_page/child_page_element.py +94 -0
- notionary/blocks/client.py +0 -1
- notionary/blocks/code/code_element.py +51 -2
- notionary/blocks/code/code_markdown_node.py +52 -1
- notionary/blocks/column/column_element.py +9 -3
- notionary/blocks/column/column_list_element.py +18 -3
- notionary/blocks/divider/divider_element.py +3 -11
- notionary/blocks/embed/embed_element.py +27 -6
- notionary/blocks/equation/equation_element.py +94 -41
- notionary/blocks/equation/equation_element_markdown_node.py +8 -9
- notionary/blocks/file/file_element.py +56 -37
- notionary/blocks/file/file_element_markdown_node.py +9 -7
- notionary/blocks/guards.py +22 -0
- notionary/blocks/heading/heading_element.py +23 -4
- notionary/blocks/image_block/image_element.py +43 -38
- notionary/blocks/image_block/image_markdown_node.py +10 -5
- 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 +3 -1
- notionary/blocks/numbered_list/numbered_list_element.py +21 -4
- notionary/blocks/paragraph/paragraph_element.py +21 -5
- notionary/blocks/pdf/pdf_element.py +47 -41
- notionary/blocks/pdf/pdf_markdown_node.py +9 -7
- notionary/blocks/quote/quote_element.py +26 -9
- notionary/blocks/quote/quote_markdown_node.py +2 -2
- notionary/blocks/registry/block_registry.py +1 -46
- notionary/blocks/registry/block_registry_builder.py +8 -0
- notionary/blocks/rich_text/name_to_id_resolver.py +205 -0
- notionary/blocks/rich_text/rich_text_models.py +62 -29
- notionary/blocks/rich_text/text_inline_formatter.py +432 -101
- notionary/blocks/syntax_prompt_builder.py +137 -0
- notionary/blocks/table/table_element.py +110 -9
- notionary/blocks/table_of_contents/table_of_contents_element.py +19 -2
- notionary/blocks/todo/todo_element.py +21 -4
- notionary/blocks/toggle/toggle_element.py +19 -3
- notionary/blocks/toggle/toggle_markdown_node.py +1 -1
- notionary/blocks/toggleable_heading/toggleable_heading_element.py +19 -4
- notionary/blocks/types.py +69 -0
- notionary/blocks/video/video_element.py +44 -39
- notionary/blocks/video/video_markdown_node.py +10 -5
- notionary/database/client.py +23 -0
- notionary/file_upload/models.py +2 -2
- notionary/markdown/markdown_builder.py +34 -27
- notionary/page/client.py +26 -6
- notionary/page/notion_page.py +37 -6
- notionary/page/page_content_deleting_service.py +117 -0
- notionary/page/page_content_writer.py +89 -113
- notionary/page/page_context.py +65 -0
- notionary/page/reader/handler/__init__.py +2 -0
- notionary/page/reader/handler/base_block_renderer.py +4 -4
- notionary/page/reader/handler/block_rendering_context.py +5 -0
- notionary/page/reader/handler/line_renderer.py +16 -3
- notionary/page/reader/handler/numbered_list_renderer.py +85 -0
- notionary/page/reader/page_content_retriever.py +17 -5
- notionary/page/writer/handler/__init__.py +2 -0
- notionary/page/writer/handler/code_handler.py +12 -40
- notionary/page/writer/handler/column_handler.py +12 -12
- notionary/page/writer/handler/column_list_handler.py +13 -13
- notionary/page/writer/handler/equation_handler.py +74 -0
- notionary/page/writer/handler/line_handler.py +4 -4
- notionary/page/writer/handler/regular_line_handler.py +31 -37
- notionary/page/writer/handler/table_handler.py +8 -72
- notionary/page/writer/handler/toggle_handler.py +14 -12
- notionary/page/writer/handler/toggleable_heading_handler.py +22 -16
- notionary/page/writer/markdown_to_notion_converter.py +28 -9
- 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/markdown_to_notion_post_processor.py +0 -0
- notionary/page/writer/markdown_to_notion_text_length_post_processor.py +0 -0
- notionary/page/writer/notion_text_length_processor.py +150 -0
- notionary/telemetry/service.py +0 -1
- notionary/user/notion_user_manager.py +22 -95
- notionary/util/concurrency_limiter.py +0 -0
- notionary/workspace.py +4 -4
- notionary-0.2.22.dist-info/METADATA +237 -0
- {notionary-0.2.21.dist-info → notionary-0.2.22.dist-info}/RECORD +92 -77
- notionary/page/markdown_whitespace_processor.py +0 -80
- notionary/page/notion_text_length_utils.py +0 -119
- notionary/user/notion_user_provider.py +0 -1
- notionary-0.2.21.dist-info/METADATA +0 -229
- /notionary/page/reader/handler/{context.py → equation_renderer.py} +0 -0
- {notionary-0.2.21.dist-info → notionary-0.2.22.dist-info}/LICENSE +0 -0
- {notionary-0.2.21.dist-info → notionary-0.2.22.dist-info}/WHEEL +0 -0
@@ -5,34 +5,27 @@ from typing import Optional
|
|
5
5
|
|
6
6
|
from notionary.blocks.base_block_element import BaseBlockElement
|
7
7
|
from notionary.blocks.file.file_element_models import ExternalFile, FileBlock, FileType
|
8
|
+
from notionary.blocks.mixins.captions import CaptionMixin
|
9
|
+
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
8
10
|
from notionary.blocks.models import Block, BlockCreateResult
|
9
|
-
from notionary.blocks.paragraph.paragraph_models import (
|
10
|
-
CreateParagraphBlock,
|
11
|
-
ParagraphBlock,
|
12
|
-
)
|
13
|
-
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
14
|
-
from notionary.blocks.rich_text.text_inline_formatter import TextInlineFormatter
|
15
11
|
from notionary.blocks.types import BlockType
|
16
12
|
from notionary.blocks.video.video_element_models import CreateVideoBlock
|
17
13
|
|
18
14
|
|
19
|
-
class VideoElement(BaseBlockElement):
|
15
|
+
class VideoElement(BaseBlockElement, CaptionMixin):
|
20
16
|
"""
|
21
17
|
Handles conversion between Markdown video embeds and Notion video blocks.
|
22
18
|
|
23
19
|
Markdown video syntax:
|
24
20
|
- [video](https://example.com/video.mp4) - URL only
|
25
|
-
- [video](https://example.com/video.mp4
|
21
|
+
- [video](https://example.com/video.mp4)(caption:Demo Video) - URL with caption
|
22
|
+
- (caption:Tutorial video)[video](https://youtube.com/watch?v=abc123) - caption before URL
|
26
23
|
|
27
24
|
Supports YouTube, Vimeo, and direct file URLs.
|
28
25
|
"""
|
29
26
|
|
30
|
-
|
31
|
-
|
32
|
-
r"(https?://[^\s\"]+)" # URL
|
33
|
-
r"(?:\s+\"([^\"]+)\")?" # optional caption
|
34
|
-
r"\)$"
|
35
|
-
)
|
27
|
+
# Flexible pattern that can handle caption in any position
|
28
|
+
VIDEO_PATTERN = re.compile(r"\[video\]\((https?://[^\s\"]+)\)")
|
36
29
|
|
37
30
|
YOUTUBE_PATTERNS = [
|
38
31
|
re.compile(r"(?:https?://)?(?:www\.)?youtube\.com/watch\?v=([\w-]{11})"),
|
@@ -44,34 +37,33 @@ class VideoElement(BaseBlockElement):
|
|
44
37
|
return block.type == BlockType.VIDEO and block.video
|
45
38
|
|
46
39
|
@classmethod
|
47
|
-
def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
48
|
-
"""Convert markdown video syntax to a Notion VideoBlock
|
49
|
-
|
50
|
-
|
40
|
+
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
41
|
+
"""Convert markdown video syntax to a Notion VideoBlock."""
|
42
|
+
# Use our own regex to find the video URL
|
43
|
+
video_match = cls.VIDEO_PATTERN.search(text.strip())
|
44
|
+
if not video_match:
|
51
45
|
return None
|
52
46
|
|
53
|
-
url
|
47
|
+
url = video_match.group(1)
|
54
48
|
|
55
49
|
vid_id = cls._get_youtube_id(url)
|
56
50
|
if vid_id:
|
57
51
|
url = f"https://www.youtube.com/watch?v={vid_id}"
|
58
52
|
|
53
|
+
# Use mixin to extract caption (if present anywhere in text)
|
54
|
+
caption_text = cls.extract_caption(text.strip())
|
55
|
+
caption_rich_text = cls.build_caption_rich_text(caption_text or "")
|
56
|
+
|
59
57
|
video_block = FileBlock(
|
60
|
-
type=FileType.EXTERNAL,
|
58
|
+
type=FileType.EXTERNAL,
|
59
|
+
external=ExternalFile(url=url),
|
60
|
+
caption=caption_rich_text,
|
61
61
|
)
|
62
|
-
if caption_text.strip():
|
63
|
-
rt = RichTextObject.from_plain_text(caption_text.strip())
|
64
|
-
video_block.caption = [rt]
|
65
|
-
|
66
|
-
empty_para = ParagraphBlock(rich_text=[])
|
67
62
|
|
68
|
-
return
|
69
|
-
CreateVideoBlock(video=video_block),
|
70
|
-
CreateParagraphBlock(paragraph=empty_para),
|
71
|
-
]
|
63
|
+
return CreateVideoBlock(video=video_block)
|
72
64
|
|
73
65
|
@classmethod
|
74
|
-
def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
66
|
+
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
75
67
|
if block.type != BlockType.VIDEO or not block.video:
|
76
68
|
return None
|
77
69
|
|
@@ -85,17 +77,14 @@ class VideoElement(BaseBlockElement):
|
|
85
77
|
else:
|
86
78
|
return None # (file_upload o.ä. hier nicht supported)
|
87
79
|
|
88
|
-
|
89
|
-
captions = fo.caption or []
|
90
|
-
if not captions:
|
91
|
-
return f"[video]({url})"
|
80
|
+
result = f"[video]({url})"
|
92
81
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
82
|
+
# Add caption if present
|
83
|
+
caption_markdown = await cls.format_caption_for_markdown(fo.caption or [])
|
84
|
+
if caption_markdown:
|
85
|
+
result += caption_markdown
|
97
86
|
|
98
|
-
return
|
87
|
+
return result
|
99
88
|
|
100
89
|
@classmethod
|
101
90
|
def _get_youtube_id(cls, url: str) -> Optional[str]:
|
@@ -104,3 +93,19 @@ class VideoElement(BaseBlockElement):
|
|
104
93
|
if m:
|
105
94
|
return m.group(1)
|
106
95
|
return None
|
96
|
+
|
97
|
+
@classmethod
|
98
|
+
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
99
|
+
"""Get system prompt information for video blocks."""
|
100
|
+
return BlockElementMarkdownInformation(
|
101
|
+
block_type=cls.__name__,
|
102
|
+
description="Video blocks embed videos from external URLs like YouTube, Vimeo, or direct video files",
|
103
|
+
syntax_examples=[
|
104
|
+
"[video](https://youtube.com/watch?v=abc123)",
|
105
|
+
"[video](https://vimeo.com/123456789)",
|
106
|
+
"[video](https://example.com/video.mp4)(caption:Demo Video)",
|
107
|
+
"(caption:Tutorial)[video](https://youtu.be/abc123)",
|
108
|
+
"[video](https://youtube.com/watch?v=xyz)(caption:**Important** tutorial)",
|
109
|
+
],
|
110
|
+
usage_guidelines="Use for embedding videos from supported platforms or direct video file URLs. Supports YouTube, Vimeo, and direct video files. Caption supports rich text formatting and describes the video content.",
|
111
|
+
)
|
@@ -5,6 +5,7 @@ from typing import Optional
|
|
5
5
|
from pydantic import BaseModel
|
6
6
|
|
7
7
|
from notionary.markdown.markdown_node import MarkdownNode
|
8
|
+
from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
|
8
9
|
|
9
10
|
|
10
11
|
class VideoMarkdownBlockParams(BaseModel):
|
@@ -12,10 +13,9 @@ class VideoMarkdownBlockParams(BaseModel):
|
|
12
13
|
caption: Optional[str] = None
|
13
14
|
|
14
15
|
|
15
|
-
class VideoMarkdownNode(MarkdownNode):
|
16
|
+
class VideoMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
16
17
|
"""
|
17
18
|
Programmatic interface for creating Notion-style video blocks.
|
18
|
-
Example: [video](https://example.com/video.mp4 "Optional caption")
|
19
19
|
"""
|
20
20
|
|
21
21
|
def __init__(self, url: str, caption: Optional[str] = None):
|
@@ -27,6 +27,11 @@ class VideoMarkdownNode(MarkdownNode):
|
|
27
27
|
return cls(url=params.url, caption=params.caption)
|
28
28
|
|
29
29
|
def to_markdown(self) -> str:
|
30
|
-
|
31
|
-
|
32
|
-
|
30
|
+
"""Return the Markdown representation.
|
31
|
+
|
32
|
+
Examples:
|
33
|
+
- [video](https://example.com/movie.mp4)
|
34
|
+
- [video](https://www.youtube.com/watch?v=dQw4w9WgXcQ)(caption:Music Video)
|
35
|
+
"""
|
36
|
+
base_markdown = f"[video]({self.url})"
|
37
|
+
return self.append_caption_to_markdown(base_markdown, self.caption)
|
notionary/database/client.py
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
from typing import Any, Dict, Optional
|
2
2
|
|
3
|
+
from urllib3.util import response
|
4
|
+
|
3
5
|
from notionary.base_notion_client import BaseNotionClient
|
4
6
|
from notionary.database.models import (
|
5
7
|
NotionDatabaseResponse,
|
@@ -18,6 +20,27 @@ class NotionDatabaseClient(BaseNotionClient):
|
|
18
20
|
def __init__(self, token: Optional[str] = None, timeout: int = 30):
|
19
21
|
super().__init__(token, timeout)
|
20
22
|
|
23
|
+
async def create_database(
|
24
|
+
self,
|
25
|
+
title: str,
|
26
|
+
parent_page_id: Optional[str],
|
27
|
+
properties: Optional[Dict[str, Any]] = None,
|
28
|
+
) -> NotionDatabaseResponse:
|
29
|
+
"""
|
30
|
+
Creates a new database as child of the specified page.
|
31
|
+
"""
|
32
|
+
if properties is None:
|
33
|
+
properties = {"Name": {"title": {}}}
|
34
|
+
|
35
|
+
database_data = {
|
36
|
+
"parent": {"page_id": parent_page_id},
|
37
|
+
"title": [{"type": "text", "text": {"content": title}}],
|
38
|
+
"properties": properties,
|
39
|
+
}
|
40
|
+
|
41
|
+
response = await self.post("databases", database_data)
|
42
|
+
return NotionDatabaseResponse.model_validate(response)
|
43
|
+
|
21
44
|
async def get_database(self, database_id: str) -> NotionDatabaseResponse:
|
22
45
|
"""
|
23
46
|
Gets metadata for a Notion database by its ID.
|
notionary/file_upload/models.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import
|
1
|
+
from typing import Literal, Optional
|
2
2
|
|
3
3
|
from pydantic import BaseModel
|
4
4
|
|
@@ -28,7 +28,7 @@ class FileUploadListResponse(BaseModel):
|
|
28
28
|
"""
|
29
29
|
|
30
30
|
object: Literal["list"]
|
31
|
-
results:
|
31
|
+
results: list[FileUploadResponse]
|
32
32
|
next_cursor: Optional[str] = None
|
33
33
|
has_more: bool
|
34
34
|
type: Literal["file_upload"]
|
@@ -51,6 +51,7 @@ from notionary.blocks.toggleable_heading import (
|
|
51
51
|
ToggleableHeadingMarkdownBlockParams,
|
52
52
|
ToggleableHeadingMarkdownNode,
|
53
53
|
)
|
54
|
+
from notionary.blocks.types import BlockType, MarkdownBlockType
|
54
55
|
from notionary.blocks.video import VideoMarkdownBlockParams, VideoMarkdownNode
|
55
56
|
from notionary.markdown.markdown_document_model import (
|
56
57
|
MarkdownBlock,
|
@@ -67,32 +68,38 @@ class MarkdownBuilder:
|
|
67
68
|
def __init__(self) -> None:
|
68
69
|
self.children: list[MarkdownNode] = []
|
69
70
|
|
70
|
-
# Explicit mapping instead of dynamic getattr - type-safe and clear
|
71
71
|
self._block_processors: dict[str, Callable[[Any], None]] = {
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
72
|
+
MarkdownBlockType.HEADING_1: self._add_heading,
|
73
|
+
MarkdownBlockType.HEADING_2: self._add_heading,
|
74
|
+
MarkdownBlockType.HEADING_3: self._add_heading,
|
75
|
+
MarkdownBlockType.PARAGRAPH: self._add_paragraph,
|
76
|
+
MarkdownBlockType.QUOTE: self._add_quote,
|
77
|
+
MarkdownBlockType.BULLETED_LIST_ITEM: self._add_bulleted_list,
|
78
|
+
MarkdownBlockType.NUMBERED_LIST_ITEM: self._add_numbered_list,
|
79
|
+
MarkdownBlockType.TO_DO: self._add_todo,
|
80
|
+
MarkdownBlockType.CALLOUT: self._add_callout,
|
81
|
+
MarkdownBlockType.CODE: self._add_code,
|
82
|
+
MarkdownBlockType.IMAGE: self._add_image,
|
83
|
+
MarkdownBlockType.VIDEO: self._add_video,
|
84
|
+
MarkdownBlockType.AUDIO: self._add_audio,
|
85
|
+
MarkdownBlockType.FILE: self._add_file,
|
86
|
+
MarkdownBlockType.PDF: self._add_pdf,
|
87
|
+
MarkdownBlockType.BOOKMARK: self._add_bookmark,
|
88
|
+
MarkdownBlockType.EMBED: self._add_embed,
|
89
|
+
MarkdownBlockType.TABLE: self._add_table,
|
90
|
+
MarkdownBlockType.DIVIDER: self._add_divider,
|
91
|
+
MarkdownBlockType.EQUATION: self._add_equation,
|
92
|
+
MarkdownBlockType.TABLE_OF_CONTENTS: self._add_table_of_contents,
|
93
|
+
MarkdownBlockType.TOGGLE: self._add_toggle,
|
94
|
+
MarkdownBlockType.COLUMN_LIST: self._add_columns,
|
95
|
+
MarkdownBlockType.BREADCRUMB: self._add_breadcrumb,
|
96
|
+
MarkdownBlockType.HEADING: self._add_heading,
|
97
|
+
MarkdownBlockType.BULLETED_LIST: self._add_bulleted_list,
|
98
|
+
MarkdownBlockType.NUMBERED_LIST: self._add_numbered_list,
|
99
|
+
MarkdownBlockType.TODO: self._add_todo,
|
100
|
+
MarkdownBlockType.TOGGLEABLE_HEADING: self._add_toggleable_heading,
|
101
|
+
MarkdownBlockType.COLUMNS: self._add_columns,
|
102
|
+
MarkdownBlockType.SPACE: self._add_space,
|
96
103
|
}
|
97
104
|
|
98
105
|
@classmethod
|
@@ -353,7 +360,7 @@ class MarkdownBuilder:
|
|
353
360
|
return self
|
354
361
|
|
355
362
|
def bookmark(
|
356
|
-
self, url: str, title: Optional[str] = None,
|
363
|
+
self, url: str, title: Optional[str] = None, caption: Optional[str] = None
|
357
364
|
) -> Self:
|
358
365
|
"""
|
359
366
|
Add a bookmark.
|
@@ -364,7 +371,7 @@ class MarkdownBuilder:
|
|
364
371
|
description: Optional bookmark description text
|
365
372
|
"""
|
366
373
|
self.children.append(
|
367
|
-
BookmarkMarkdownNode(url=url, title=title,
|
374
|
+
BookmarkMarkdownNode(url=url, title=title, caption=caption)
|
368
375
|
)
|
369
376
|
return self
|
370
377
|
|
notionary/page/client.py
CHANGED
@@ -20,20 +20,40 @@ class NotionPageClient(BaseNotionClient):
|
|
20
20
|
|
21
21
|
async def create_page(
|
22
22
|
self,
|
23
|
+
*,
|
23
24
|
parent_database_id: Optional[str] = None,
|
24
|
-
|
25
|
+
parent_page_id: Optional[str] = None,
|
26
|
+
title: str,
|
25
27
|
) -> NotionPageResponse:
|
26
28
|
"""
|
27
|
-
Creates a new page in a
|
29
|
+
Creates a new page either in a database or as a child of another page.
|
30
|
+
Exactly one of parent_database_id or parent_page_id must be provided.
|
31
|
+
Only 'title' is supported here (no icon/cover/children).
|
28
32
|
"""
|
29
|
-
|
30
|
-
|
31
|
-
"
|
33
|
+
# Exakt einen Parent zulassen
|
34
|
+
if (parent_database_id is None) == (parent_page_id is None):
|
35
|
+
raise ValueError("Specify exactly one parent: database OR page")
|
36
|
+
|
37
|
+
# Parent bauen
|
38
|
+
parent = (
|
39
|
+
{"database_id": parent_database_id}
|
40
|
+
if parent_database_id
|
41
|
+
else {"page_id": parent_page_id}
|
42
|
+
)
|
43
|
+
|
44
|
+
properties: dict[str, Any] = {
|
45
|
+
"title": {
|
46
|
+
"title": [
|
47
|
+
{"type": "text", "text": {"content": title}}
|
48
|
+
]
|
49
|
+
}
|
32
50
|
}
|
33
51
|
|
34
|
-
|
52
|
+
payload = {"parent": parent, "properties": properties}
|
53
|
+
response = await self.post("pages", payload)
|
35
54
|
return NotionPageResponse.model_validate(response)
|
36
55
|
|
56
|
+
|
37
57
|
async def patch_page(
|
38
58
|
self, page_id: str, data: Optional[dict[str, Any]] = None
|
39
59
|
) -> NotionPageResponse:
|
notionary/page/notion_page.py
CHANGED
@@ -5,12 +5,15 @@ import random
|
|
5
5
|
from typing import TYPE_CHECKING, Any, Callable, Optional, Union
|
6
6
|
|
7
7
|
from notionary.blocks.client import NotionBlockClient
|
8
|
+
from notionary.blocks.syntax_prompt_builder import SyntaxPromptBuilder
|
8
9
|
from notionary.blocks.models import DatabaseParent
|
9
10
|
from notionary.blocks.registry.block_registry import BlockRegistry
|
10
11
|
from notionary.blocks.registry.block_registry_builder import BlockRegistryBuilder
|
12
|
+
from notionary.database.client import NotionDatabaseClient
|
11
13
|
from notionary.markdown.markdown_builder import MarkdownBuilder
|
12
14
|
from notionary.page.client import NotionPageClient
|
13
15
|
from notionary.page.models import NotionPageResponse
|
16
|
+
from notionary.page.page_content_deleting_service import PageContentDeletingService
|
14
17
|
from notionary.page.page_content_writer import PageContentWriter
|
15
18
|
from notionary.page.property_formatter import NotionPropertyFormatter
|
16
19
|
from notionary.page.reader.page_content_retriever import PageContentRetriever
|
@@ -21,7 +24,6 @@ from notionary.util.fuzzy import find_best_match
|
|
21
24
|
if TYPE_CHECKING:
|
22
25
|
from notionary import NotionDatabase
|
23
26
|
|
24
|
-
|
25
27
|
class NotionPage(LoggingMixin):
|
26
28
|
"""
|
27
29
|
Managing content and metadata of a Notion page.
|
@@ -55,7 +57,7 @@ class NotionPage(LoggingMixin):
|
|
55
57
|
self._client = NotionPageClient(token=token)
|
56
58
|
self._block_client = NotionBlockClient(token=token)
|
57
59
|
self._page_data = None
|
58
|
-
|
60
|
+
|
59
61
|
self.block_element_registry = BlockRegistry.create_registry()
|
60
62
|
|
61
63
|
self._page_content_writer = PageContentWriter(
|
@@ -63,6 +65,11 @@ class NotionPage(LoggingMixin):
|
|
63
65
|
block_registry=self.block_element_registry,
|
64
66
|
)
|
65
67
|
|
68
|
+
self._page_content_deleting_service = PageContentDeletingService(
|
69
|
+
page_id=self._page_id,
|
70
|
+
block_registry=self.block_element_registry,
|
71
|
+
)
|
72
|
+
|
66
73
|
self._page_content_retriever = PageContentRetriever(
|
67
74
|
block_registry=self.block_element_registry,
|
68
75
|
)
|
@@ -202,6 +209,10 @@ class NotionPage(LoggingMixin):
|
|
202
209
|
"""
|
203
210
|
return self.block_element_registry.builder
|
204
211
|
|
212
|
+
def get_prompt_information(self) -> str:
|
213
|
+
markdown_syntax_builder = SyntaxPromptBuilder()
|
214
|
+
return markdown_syntax_builder.build_concise_reference()
|
215
|
+
|
205
216
|
async def set_title(self, title: str) -> str:
|
206
217
|
"""
|
207
218
|
Set the title of the page.
|
@@ -264,7 +275,7 @@ class NotionPage(LoggingMixin):
|
|
264
275
|
Returns:
|
265
276
|
bool: True if successful, False otherwise
|
266
277
|
"""
|
267
|
-
clear_result = await self.
|
278
|
+
clear_result = await self._page_content_deleting_service.clear_page_content()
|
268
279
|
if not clear_result:
|
269
280
|
self.logger.error("Failed to clear page content before replacement")
|
270
281
|
|
@@ -279,7 +290,7 @@ class NotionPage(LoggingMixin):
|
|
279
290
|
"""
|
280
291
|
Clear all content from the page.
|
281
292
|
"""
|
282
|
-
return await self.
|
293
|
+
return await self._page_content_deleting_service.clear_page_content()
|
283
294
|
|
284
295
|
async def get_text_content(self) -> str:
|
285
296
|
"""
|
@@ -309,7 +320,27 @@ class NotionPage(LoggingMixin):
|
|
309
320
|
|
310
321
|
self.logger.error(f"Error updating page emoji: {str(e)}")
|
311
322
|
return None
|
312
|
-
|
323
|
+
|
324
|
+
async def create_child_database(self, title: str) -> NotionDatabase:
|
325
|
+
from notionary import NotionDatabase
|
326
|
+
database_client = NotionDatabaseClient(token=self._client.token)
|
327
|
+
|
328
|
+
create_database_response = await database_client.create_database(
|
329
|
+
title=title,
|
330
|
+
parent_page_id=self._page_id,
|
331
|
+
)
|
332
|
+
|
333
|
+
return await NotionDatabase.from_database_id(id=create_database_response.id, token=self._client.token)
|
334
|
+
|
335
|
+
async def create_child_page(self, title: str) -> NotionPage:
|
336
|
+
from notionary import NotionPage
|
337
|
+
child_page_response = await self._client.create_page(
|
338
|
+
parent_page_id=self._page_id,
|
339
|
+
title=title,
|
340
|
+
)
|
341
|
+
|
342
|
+
return await NotionPage.from_page_id(page_id=child_page_response.id, token=self._client.token)
|
343
|
+
|
313
344
|
async def set_external_icon(self, url: str) -> Optional[str]:
|
314
345
|
"""
|
315
346
|
Sets the page icon to an external image.
|
@@ -605,4 +636,4 @@ class NotionPage(LoggingMixin):
|
|
605
636
|
"""Extract parent database ID from page response."""
|
606
637
|
parent = page_response.parent
|
607
638
|
if isinstance(parent, DatabaseParent):
|
608
|
-
return parent.database_id
|
639
|
+
return parent.database_id
|
@@ -0,0 +1,117 @@
|
|
1
|
+
from typing import Optional
|
2
|
+
|
3
|
+
from notionary.blocks.client import NotionBlockClient
|
4
|
+
from notionary.blocks.models import Block
|
5
|
+
from notionary.blocks.registry.block_registry import BlockRegistry
|
6
|
+
from notionary.page.reader.page_content_retriever import PageContentRetriever
|
7
|
+
from notionary.util import LoggingMixin
|
8
|
+
|
9
|
+
|
10
|
+
class PageContentDeletingService(LoggingMixin):
|
11
|
+
"""Service responsible for deleting page content and blocks."""
|
12
|
+
|
13
|
+
def __init__(self, page_id: str, block_registry: BlockRegistry):
|
14
|
+
self.page_id = page_id
|
15
|
+
self.block_registry = block_registry
|
16
|
+
self._block_client = NotionBlockClient()
|
17
|
+
self._content_retriever = PageContentRetriever(block_registry=block_registry)
|
18
|
+
|
19
|
+
async def clear_page_content(self) -> Optional[str]:
|
20
|
+
"""Clear all content of the page and return deleted content as markdown."""
|
21
|
+
try:
|
22
|
+
children_response = await self._block_client.get_block_children(
|
23
|
+
block_id=self.page_id
|
24
|
+
)
|
25
|
+
|
26
|
+
if not children_response or not children_response.results:
|
27
|
+
return None
|
28
|
+
|
29
|
+
# Use PageContentRetriever for sophisticated markdown conversion
|
30
|
+
deleted_content = self._content_retriever._convert_blocks_to_markdown(
|
31
|
+
children_response.results, indent_level=0
|
32
|
+
)
|
33
|
+
|
34
|
+
# Delete blocks
|
35
|
+
success = True
|
36
|
+
for block in children_response.results:
|
37
|
+
block_success = await self._delete_block_with_children(block)
|
38
|
+
if not block_success:
|
39
|
+
success = False
|
40
|
+
|
41
|
+
if not success:
|
42
|
+
self.logger.warning("Some blocks could not be deleted")
|
43
|
+
|
44
|
+
return deleted_content if deleted_content else None
|
45
|
+
|
46
|
+
except Exception:
|
47
|
+
self.logger.error("Error clearing page content", exc_info=True)
|
48
|
+
return None
|
49
|
+
|
50
|
+
async def _delete_block_with_children(self, block: Block) -> bool:
|
51
|
+
"""Delete a block and all its children recursively."""
|
52
|
+
if not block.id:
|
53
|
+
self.logger.error("Block has no valid ID")
|
54
|
+
return False
|
55
|
+
|
56
|
+
self.logger.debug("Deleting block: %s (type: %s)", block.id, block.type)
|
57
|
+
|
58
|
+
try:
|
59
|
+
if block.has_children and not await self._delete_block_children(block):
|
60
|
+
return False
|
61
|
+
|
62
|
+
return await self._delete_single_block(block)
|
63
|
+
|
64
|
+
except Exception as e:
|
65
|
+
self.logger.error("Failed to delete block %s: %s", block.id, str(e))
|
66
|
+
return False
|
67
|
+
|
68
|
+
async def _delete_block_children(self, block: Block) -> bool:
|
69
|
+
"""Delete all children of a block."""
|
70
|
+
self.logger.debug("Block %s has children, deleting children first", block.id)
|
71
|
+
|
72
|
+
try:
|
73
|
+
children_blocks = await self._block_client.get_all_block_children(block.id)
|
74
|
+
|
75
|
+
if not children_blocks:
|
76
|
+
self.logger.debug("No children found for block: %s", block.id)
|
77
|
+
return True
|
78
|
+
|
79
|
+
self.logger.debug(
|
80
|
+
"Found %d children to delete for block: %s",
|
81
|
+
len(children_blocks),
|
82
|
+
block.id,
|
83
|
+
)
|
84
|
+
|
85
|
+
# Delete all children recursively
|
86
|
+
for child_block in children_blocks:
|
87
|
+
if not await self._delete_block_with_children(child_block):
|
88
|
+
self.logger.error(
|
89
|
+
"Failed to delete child block: %s", child_block.id
|
90
|
+
)
|
91
|
+
return False
|
92
|
+
|
93
|
+
self.logger.debug(
|
94
|
+
"Successfully deleted all children of block: %s", block.id
|
95
|
+
)
|
96
|
+
return True
|
97
|
+
|
98
|
+
except Exception as e:
|
99
|
+
self.logger.error(
|
100
|
+
"Failed to delete children of block %s: %s", block.id, str(e)
|
101
|
+
)
|
102
|
+
return False
|
103
|
+
|
104
|
+
async def _delete_single_block(self, block: Block) -> bool:
|
105
|
+
"""Delete a single block."""
|
106
|
+
deleted_block: Optional[Block] = await self._block_client.delete_block(block.id)
|
107
|
+
|
108
|
+
if deleted_block is None:
|
109
|
+
self.logger.error("Failed to delete block: %s", block.id)
|
110
|
+
return False
|
111
|
+
|
112
|
+
if deleted_block.archived or deleted_block.in_trash:
|
113
|
+
self.logger.debug("Successfully deleted/archived block: %s", block.id)
|
114
|
+
return True
|
115
|
+
else:
|
116
|
+
self.logger.warning("Block %s was not properly archived/deleted", block.id)
|
117
|
+
return False
|