notionary 0.2.22__py3-none-any.whl → 0.2.24__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 +1 -1
- notionary/blocks/__init__.py +3 -1
- notionary/blocks/audio/__init__.py +0 -2
- notionary/blocks/audio/audio_element.py +92 -49
- notionary/blocks/audio/audio_markdown_node.py +4 -17
- notionary/blocks/bookmark/__init__.py +0 -2
- notionary/blocks/bookmark/bookmark_markdown_node.py +5 -21
- notionary/blocks/breadcrumbs/__init__.py +0 -2
- notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +2 -21
- notionary/blocks/bulleted_list/__init__.py +0 -2
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +3 -17
- notionary/blocks/bulleted_list/bulleted_list_models.py +0 -1
- notionary/blocks/callout/__init__.py +0 -2
- notionary/blocks/callout/callout_markdown_node.py +4 -18
- notionary/blocks/callout/callout_models.py +3 -4
- notionary/blocks/child_database/child_database_element.py +2 -4
- notionary/blocks/code/code_markdown_node.py +5 -19
- notionary/blocks/column/__init__.py +0 -4
- notionary/blocks/column/column_list_markdown_node.py +3 -19
- notionary/blocks/column/column_markdown_node.py +4 -21
- notionary/blocks/divider/__init__.py +0 -2
- notionary/blocks/divider/divider_markdown_node.py +2 -16
- notionary/blocks/embed/__init__.py +0 -2
- notionary/blocks/embed/embed_markdown_node.py +4 -17
- notionary/blocks/equation/__init__.py +0 -1
- notionary/blocks/equation/equation_element_markdown_node.py +3 -15
- notionary/blocks/file/__init__.py +0 -2
- notionary/blocks/file/file_element.py +67 -46
- notionary/blocks/file/file_element_markdown_node.py +4 -17
- notionary/blocks/heading/__init__.py +0 -2
- notionary/blocks/heading/heading_markdown_node.py +5 -19
- notionary/blocks/heading/heading_models.py +3 -3
- notionary/blocks/image_block/__init__.py +0 -2
- notionary/blocks/image_block/image_element.py +66 -25
- notionary/blocks/image_block/image_markdown_node.py +5 -20
- notionary/{markdown → blocks/markdown}/markdown_builder.py +29 -233
- notionary/blocks/markdown/markdown_node.py +25 -0
- notionary/blocks/mixins/file_upload/__init__.py +3 -0
- notionary/blocks/mixins/file_upload/file_upload_mixin.py +320 -0
- notionary/blocks/numbered_list/__init__.py +0 -1
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -17
- notionary/blocks/numbered_list/numbered_list_models.py +3 -3
- notionary/blocks/paragraph/__init__.py +0 -2
- notionary/blocks/paragraph/paragraph_markdown_node.py +3 -13
- notionary/blocks/pdf/__init__.py +0 -2
- notionary/blocks/pdf/pdf_element.py +81 -32
- notionary/blocks/pdf/pdf_markdown_node.py +5 -18
- notionary/blocks/quote/__init__.py +0 -2
- notionary/blocks/quote/quote_markdown_node.py +3 -13
- notionary/blocks/registry/__init__.py +1 -2
- notionary/blocks/registry/block_registry.py +116 -61
- notionary/blocks/rich_text/text_inline_formatter.py +1 -1
- notionary/blocks/table/__init__.py +0 -2
- notionary/blocks/table/table_markdown_node.py +17 -16
- notionary/blocks/table_of_contents/__init__.py +0 -2
- notionary/blocks/table_of_contents/table_of_contents_element.py +27 -15
- notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +3 -17
- notionary/blocks/table_of_contents/table_of_contents_models.py +2 -2
- notionary/blocks/todo/__init__.py +0 -2
- notionary/blocks/todo/todo_markdown_node.py +9 -20
- notionary/blocks/todo/todo_models.py +2 -3
- notionary/blocks/toggle/__init__.py +0 -2
- notionary/blocks/toggle/toggle_markdown_node.py +5 -19
- notionary/blocks/toggleable_heading/__init__.py +0 -2
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +6 -23
- notionary/blocks/video/__init__.py +0 -2
- notionary/blocks/video/video_element.py +110 -34
- notionary/blocks/video/video_markdown_node.py +4 -15
- notionary/comments/__init__.py +26 -0
- notionary/comments/client.py +211 -0
- notionary/comments/models.py +129 -0
- notionary/file_upload/client.py +3 -2
- notionary/file_upload/models.py +10 -1
- notionary/file_upload/notion_file_upload.py +5 -5
- notionary/page/client.py +1 -6
- notionary/page/markdown_whitespace_processor.py +129 -0
- notionary/page/notion_page.py +87 -48
- notionary/page/page_content_deleting_service.py +1 -1
- notionary/page/page_content_writer.py +32 -129
- notionary/page/page_context.py +0 -6
- notionary/page/reader/handler/column_list_renderer.py +2 -2
- notionary/page/reader/handler/column_renderer.py +2 -2
- notionary/page/reader/handler/line_renderer.py +2 -2
- notionary/page/reader/handler/toggle_renderer.py +2 -2
- notionary/page/reader/handler/toggleable_heading_renderer.py +2 -2
- notionary/page/writer/handler/toggle_handler.py +8 -4
- notionary/page/writer/handler/toggleable_heading_handler.py +3 -2
- notionary/page/writer/markdown_to_notion_converter.py +74 -30
- notionary/schemas/__init__.py +3 -0
- notionary/schemas/base.py +73 -0
- notionary/shared/__init__.py +3 -0
- notionary/{blocks/rich_text → shared}/name_to_id_resolver.py +0 -2
- {notionary-0.2.22.dist-info → notionary-0.2.24.dist-info}/METADATA +15 -2
- {notionary-0.2.22.dist-info → notionary-0.2.24.dist-info}/RECORD +97 -95
- notionary/blocks/guards.py +0 -22
- notionary/blocks/registry/block_registry_builder.py +0 -264
- notionary/markdown/makdown_document_model.py +0 -0
- notionary/markdown/markdown_document_model.py +0 -228
- notionary/markdown/markdown_node.py +0 -30
- notionary/models/notion_database_response.py +0 -0
- notionary/page/writer/markdown_to_notion_formatting_post_processor.py +0 -73
- notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
- /notionary/{markdown/___init__.py → blocks/markdown/markdown_document_model.py} +0 -0
- {notionary-0.2.22.dist-info → notionary-0.2.24.dist-info}/LICENSE +0 -0
- {notionary-0.2.22.dist-info → notionary-0.2.24.dist-info}/WHEEL +0 -0
@@ -1,25 +1,11 @@
|
|
1
|
-
from
|
2
|
-
|
3
|
-
from pydantic import BaseModel
|
4
|
-
|
5
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
6
|
-
|
7
|
-
|
8
|
-
class DividerMarkdownBlockParams(BaseModel):
|
9
|
-
pass
|
1
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
10
2
|
|
11
3
|
|
12
4
|
class DividerMarkdownNode(MarkdownNode):
|
13
5
|
"""
|
6
|
+
Enhanced Divider node with Pydantic integration.
|
14
7
|
Programmatic interface for creating Markdown divider lines (---).
|
15
8
|
"""
|
16
9
|
|
17
|
-
def __init__(self):
|
18
|
-
pass # Keine Attribute notwendig
|
19
|
-
|
20
|
-
@classmethod
|
21
|
-
def from_params(cls, params: DividerMarkdownBlockParams) -> DividerMarkdownNode:
|
22
|
-
return cls()
|
23
|
-
|
24
10
|
def to_markdown(self) -> str:
|
25
11
|
return "---"
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from notionary.blocks.embed.embed_element import EmbedElement
|
2
2
|
from notionary.blocks.embed.embed_markdown_node import (
|
3
|
-
EmbedMarkdownBlockParams,
|
4
3
|
EmbedMarkdownNode,
|
5
4
|
)
|
6
5
|
from notionary.blocks.embed.embed_models import CreateEmbedBlock, EmbedBlock
|
@@ -10,5 +9,4 @@ __all__ = [
|
|
10
9
|
"EmbedBlock",
|
11
10
|
"CreateEmbedBlock",
|
12
11
|
"EmbedMarkdownNode",
|
13
|
-
"EmbedMarkdownBlockParams",
|
14
12
|
]
|
@@ -1,30 +1,17 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
from typing import Optional
|
4
2
|
|
5
|
-
from
|
6
|
-
|
7
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
8
|
-
|
9
|
-
|
10
|
-
class EmbedMarkdownBlockParams(BaseModel):
|
11
|
-
url: str
|
12
|
-
caption: Optional[str] = None
|
3
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
13
4
|
|
14
5
|
|
15
6
|
class EmbedMarkdownNode(MarkdownNode):
|
16
7
|
"""
|
8
|
+
Enhanced Embed node with Pydantic integration.
|
17
9
|
Programmatic interface for creating Notion-style Markdown embed blocks.
|
18
10
|
Example: [embed](https://example.com "Optional caption")
|
19
11
|
"""
|
20
12
|
|
21
|
-
|
22
|
-
|
23
|
-
self.caption = caption
|
24
|
-
|
25
|
-
@classmethod
|
26
|
-
def from_params(cls, params: EmbedMarkdownBlockParams) -> EmbedMarkdownNode:
|
27
|
-
return cls(url=params.url, caption=params.caption)
|
13
|
+
url: str
|
14
|
+
caption: Optional[str] = None
|
28
15
|
|
29
16
|
def to_markdown(self) -> str:
|
30
17
|
if self.caption:
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from notionary.blocks.equation.equation_element import EquationElement
|
2
2
|
from notionary.blocks.equation.equation_element_markdown_node import (
|
3
|
-
EquationMarkdownBlockParams,
|
4
3
|
EquationMarkdownNode,
|
5
4
|
)
|
6
5
|
from notionary.blocks.equation.equation_models import CreateEquationBlock, EquationBlock
|
@@ -1,16 +1,9 @@
|
|
1
|
-
from
|
2
|
-
|
3
|
-
from pydantic import BaseModel
|
4
|
-
|
5
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
6
|
-
|
7
|
-
|
8
|
-
class EquationMarkdownBlockParams(BaseModel):
|
9
|
-
expression: str
|
1
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
10
2
|
|
11
3
|
|
12
4
|
class EquationMarkdownNode(MarkdownNode):
|
13
5
|
"""
|
6
|
+
Enhanced Equation node with Pydantic integration.
|
14
7
|
Programmatic interface for creating Markdown equation blocks.
|
15
8
|
Uses standard Markdown equation syntax with double dollar signs.
|
16
9
|
|
@@ -20,12 +13,7 @@ class EquationMarkdownNode(MarkdownNode):
|
|
20
13
|
$$\\int_0^\\infty e^{-x} dx = 1$$
|
21
14
|
"""
|
22
15
|
|
23
|
-
|
24
|
-
self.expression = expression
|
25
|
-
|
26
|
-
@classmethod
|
27
|
-
def from_params(cls, params: EquationMarkdownBlockParams) -> EquationMarkdownNode:
|
28
|
-
return cls(expression=params.expression)
|
16
|
+
expression: str
|
29
17
|
|
30
18
|
def to_markdown(self) -> str:
|
31
19
|
expr = self.expression.strip()
|
@@ -1,7 +1,6 @@
|
|
1
1
|
from notionary.blocks.file.file_element import FileElement
|
2
2
|
from notionary.blocks.file.file_element_markdown_node import (
|
3
3
|
FileMarkdownNode,
|
4
|
-
FileMarkdownNodeParams,
|
5
4
|
)
|
6
5
|
from notionary.blocks.file.file_element_models import (
|
7
6
|
CreateFileBlock,
|
@@ -21,5 +20,4 @@ __all__ = [
|
|
21
20
|
"FileBlock",
|
22
21
|
"CreateFileBlock",
|
23
22
|
"FileMarkdownNode",
|
24
|
-
"FileMarkdownNodeParams",
|
25
23
|
]
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import re
|
4
|
+
from pathlib import Path
|
4
5
|
from typing import Optional
|
5
6
|
|
6
7
|
from notionary.blocks.base_block_element import BaseBlockElement
|
@@ -9,63 +10,73 @@ from notionary.blocks.file.file_element_models import (
|
|
9
10
|
ExternalFile,
|
10
11
|
FileBlock,
|
11
12
|
FileType,
|
13
|
+
FileUploadFile,
|
12
14
|
)
|
13
15
|
from notionary.blocks.mixins.captions import CaptionMixin
|
16
|
+
from notionary.blocks.mixins.file_upload.file_upload_mixin import FileUploadMixin
|
14
17
|
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
15
18
|
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
16
19
|
|
17
20
|
|
18
|
-
class FileElement(BaseBlockElement, CaptionMixin):
|
19
|
-
"""
|
21
|
+
class FileElement(BaseBlockElement, CaptionMixin, FileUploadMixin):
|
22
|
+
r"""
|
20
23
|
Handles conversion between Markdown file embeds and Notion file blocks.
|
21
24
|
|
22
|
-
|
23
|
-
- [file](https://example.com/document.pdf) - URL only
|
24
|
-
- [file](https://example.com/document.pdf)(caption:Annual Report) - URL with caption
|
25
|
-
- (caption:Important document)[file](https://example.com/doc.pdf) - caption before URL
|
25
|
+
Supports both external URLs and local file uploads.
|
26
26
|
|
27
|
-
|
27
|
+
Markdown file syntax:
|
28
|
+
- [file](https://example.com/document.pdf) - External URL
|
29
|
+
- [file](./local/document.pdf) - Local file (will be uploaded)
|
30
|
+
- [file](C:\Documents\report.pdf) - Absolute local path (will be uploaded)
|
31
|
+
- [file](https://example.com/document.pdf)(caption:Annual Report) - With caption
|
32
|
+
- (caption:Important document)[file](./doc.pdf) - Caption before URL
|
28
33
|
"""
|
29
34
|
|
30
|
-
|
31
|
-
FILE_PATTERN = re.compile(r"\[file\]\((https?://[^\s\"]+)\)")
|
32
|
-
|
33
|
-
@classmethod
|
34
|
-
def _extract_file_url(cls, text: str) -> Optional[str]:
|
35
|
-
"""Extract file URL from text, handling caption patterns."""
|
36
|
-
# First remove any captions to get clean text for URL extraction
|
37
|
-
clean_text = cls.remove_caption(text)
|
38
|
-
|
39
|
-
# Now extract the URL from clean text
|
40
|
-
match = cls.FILE_PATTERN.search(clean_text)
|
41
|
-
if match:
|
42
|
-
return match.group(1)
|
43
|
-
|
44
|
-
return None
|
35
|
+
FILE_PATTERN = re.compile(r"\[file\]\(([^)]+)\)")
|
45
36
|
|
46
37
|
@classmethod
|
47
38
|
def match_notion(cls, block: Block) -> bool:
|
48
|
-
|
49
|
-
return block.type == BlockType.FILE and block.file
|
39
|
+
return bool(block.type == BlockType.FILE and block.file)
|
50
40
|
|
51
41
|
@classmethod
|
52
|
-
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
42
|
+
async def markdown_to_notion(cls, text: str) -> Optional[BlockCreateResult]:
|
53
43
|
"""Convert markdown file link to Notion FileBlock."""
|
54
|
-
|
55
|
-
|
56
|
-
if not url:
|
44
|
+
file_path = cls._extract_file_path(text.strip())
|
45
|
+
if not file_path:
|
57
46
|
return None
|
58
47
|
|
59
|
-
|
48
|
+
cls.logger.info(f"Processing file: {file_path}")
|
49
|
+
|
50
|
+
# Extract caption
|
60
51
|
caption_text = cls.extract_caption(text.strip())
|
61
52
|
caption_rich_text = cls.build_caption_rich_text(caption_text or "")
|
62
53
|
|
63
|
-
#
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
54
|
+
# Determine if it's a local file or external URL
|
55
|
+
if cls._is_local_file_path(file_path):
|
56
|
+
cls.logger.debug(f"Detected local file: {file_path}")
|
57
|
+
|
58
|
+
# Upload the local file using mixin method
|
59
|
+
file_upload_id = await cls._upload_local_file(file_path, "file")
|
60
|
+
if not file_upload_id:
|
61
|
+
cls.logger.error(f"Failed to upload file: {file_path}")
|
62
|
+
return None
|
63
|
+
|
64
|
+
# Create FILE_UPLOAD block
|
65
|
+
file_block = FileBlock(
|
66
|
+
type=FileType.FILE_UPLOAD,
|
67
|
+
file_upload=FileUploadFile(id=file_upload_id),
|
68
|
+
caption=caption_rich_text,
|
69
|
+
name=Path(file_path).name,
|
70
|
+
)
|
71
|
+
|
72
|
+
else:
|
73
|
+
cls.logger.debug(f"Using external URL: {file_path}")
|
74
|
+
|
75
|
+
file_block = FileBlock(
|
76
|
+
type=FileType.EXTERNAL,
|
77
|
+
external=ExternalFile(url=file_path),
|
78
|
+
caption=caption_rich_text,
|
79
|
+
)
|
69
80
|
|
70
81
|
return CreateFileBlock(file=file_block)
|
71
82
|
|
@@ -76,18 +87,15 @@ class FileElement(BaseBlockElement, CaptionMixin):
|
|
76
87
|
|
77
88
|
fb: FileBlock = block.file
|
78
89
|
|
79
|
-
# Determine
|
90
|
+
# Determine the source for markdown
|
80
91
|
if fb.type == FileType.EXTERNAL and fb.external:
|
81
|
-
|
92
|
+
source = fb.external.url
|
82
93
|
elif fb.type == FileType.FILE and fb.file:
|
83
|
-
|
84
|
-
elif fb.type == FileType.FILE_UPLOAD:
|
85
|
-
# Uploaded file has no stable URL for Markdown
|
86
|
-
return None
|
94
|
+
source = fb.file.url
|
87
95
|
else:
|
88
96
|
return None
|
89
97
|
|
90
|
-
result = f"[file]({
|
98
|
+
result = f"[file]({source})"
|
91
99
|
|
92
100
|
# Add caption if present
|
93
101
|
caption_markdown = await cls.format_caption_for_markdown(fb.caption or [])
|
@@ -101,12 +109,25 @@ class FileElement(BaseBlockElement, CaptionMixin):
|
|
101
109
|
"""Get system prompt information for file blocks."""
|
102
110
|
return BlockElementMarkdownInformation(
|
103
111
|
block_type=cls.__name__,
|
104
|
-
description="File blocks embed
|
112
|
+
description="File blocks embed files from external URLs or upload local files with optional captions",
|
105
113
|
syntax_examples=[
|
106
114
|
"[file](https://example.com/document.pdf)",
|
115
|
+
"[file](./local/document.pdf)",
|
116
|
+
"[file](C:\\Documents\\report.xlsx)",
|
107
117
|
"[file](https://example.com/document.pdf)(caption:Annual Report)",
|
108
|
-
"(caption:Q1 Data)[file](
|
109
|
-
"[file](
|
118
|
+
"(caption:Q1 Data)[file](./spreadsheet.xlsx)",
|
119
|
+
"[file](./manual.docx)(caption:**User** manual)",
|
110
120
|
],
|
111
|
-
usage_guidelines="Use for
|
121
|
+
usage_guidelines="Use for both external URLs and local files. Local files will be automatically uploaded to Notion. Supports various file formats including PDFs, documents, spreadsheets, images. Caption supports rich text formatting and should describe the file content or purpose.",
|
112
122
|
)
|
123
|
+
|
124
|
+
@classmethod
|
125
|
+
def _extract_file_path(cls, text: str) -> Optional[str]:
|
126
|
+
"""Extract file path/URL from text, handling caption patterns."""
|
127
|
+
clean_text = cls.remove_caption(text)
|
128
|
+
|
129
|
+
match = cls.FILE_PATTERN.search(clean_text)
|
130
|
+
if match:
|
131
|
+
return match.group(1).strip()
|
132
|
+
|
133
|
+
return None
|
@@ -1,30 +1,17 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
from typing import Optional
|
4
2
|
|
5
|
-
from
|
6
|
-
|
7
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
3
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
8
4
|
from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
|
9
5
|
|
10
6
|
|
11
|
-
class FileMarkdownNodeParams(BaseModel):
|
12
|
-
url: str
|
13
|
-
caption: Optional[str] = None
|
14
|
-
|
15
|
-
|
16
7
|
class FileMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
17
8
|
"""
|
9
|
+
Enhanced File node with Pydantic integration.
|
18
10
|
Programmatic interface for creating Notion-style Markdown file embeds.
|
19
11
|
"""
|
20
12
|
|
21
|
-
|
22
|
-
|
23
|
-
self.caption = caption or ""
|
24
|
-
|
25
|
-
@classmethod
|
26
|
-
def from_params(cls, params: FileMarkdownNodeParams) -> FileMarkdownNode:
|
27
|
-
return cls(url=params.url, caption=params.caption)
|
13
|
+
url: str
|
14
|
+
caption: Optional[str] = None
|
28
15
|
|
29
16
|
def to_markdown(self) -> str:
|
30
17
|
"""Return the Markdown representation.
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from notionary.blocks.heading.heading_element import HeadingElement
|
2
2
|
from notionary.blocks.heading.heading_markdown_node import (
|
3
|
-
HeadingMarkdownBlockParams,
|
4
3
|
HeadingMarkdownNode,
|
5
4
|
)
|
6
5
|
from notionary.blocks.heading.heading_models import (
|
@@ -17,5 +16,4 @@ __all__ = [
|
|
17
16
|
"CreateHeading2Block",
|
18
17
|
"CreateHeading3Block",
|
19
18
|
"HeadingMarkdownNode",
|
20
|
-
"HeadingMarkdownBlockParams",
|
21
19
|
]
|
@@ -1,30 +1,16 @@
|
|
1
|
-
from
|
2
|
-
|
3
|
-
from pydantic import BaseModel
|
4
|
-
|
5
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
6
|
-
|
7
|
-
|
8
|
-
class HeadingMarkdownBlockParams(BaseModel):
|
9
|
-
text: str
|
10
|
-
level: int = 1
|
1
|
+
from pydantic import Field
|
2
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
11
3
|
|
12
4
|
|
13
5
|
class HeadingMarkdownNode(MarkdownNode):
|
14
6
|
"""
|
7
|
+
Enhanced Heading node with Pydantic integration.
|
15
8
|
Programmatic interface for creating Markdown headings (H1-H3).
|
16
9
|
Example: # Heading 1, ## Heading 2, ### Heading 3
|
17
10
|
"""
|
18
11
|
|
19
|
-
|
20
|
-
|
21
|
-
raise ValueError("Only heading levels 1-3 are supported (H1, H2, H3)")
|
22
|
-
self.text = text
|
23
|
-
self.level = level
|
24
|
-
|
25
|
-
@classmethod
|
26
|
-
def from_params(cls, params: HeadingMarkdownBlockParams) -> HeadingMarkdownNode:
|
27
|
-
return cls(text=params.text, level=params.level)
|
12
|
+
text: str
|
13
|
+
level: int = Field(default=1, ge=1, le=3)
|
28
14
|
|
29
15
|
def to_markdown(self) -> str:
|
30
16
|
return f"{'#' * self.level} {self.text}"
|
@@ -1,6 +1,6 @@
|
|
1
|
-
from typing import Literal
|
1
|
+
from typing import Literal, Optional
|
2
2
|
|
3
|
-
from pydantic import BaseModel
|
3
|
+
from pydantic import BaseModel
|
4
4
|
|
5
5
|
from notionary.blocks.models import Block
|
6
6
|
from notionary.blocks.rich_text.rich_text_models import RichTextObject
|
@@ -11,7 +11,7 @@ class HeadingBlock(BaseModel):
|
|
11
11
|
rich_text: list[RichTextObject]
|
12
12
|
color: BlockColor = BlockColor.DEFAULT
|
13
13
|
is_toggleable: bool = False
|
14
|
-
children: list[Block] =
|
14
|
+
children: Optional[list[Block]] = None
|
15
15
|
|
16
16
|
|
17
17
|
class CreateHeading1Block(BaseModel):
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from notionary.blocks.image_block.image_element import ImageElement
|
2
2
|
from notionary.blocks.image_block.image_markdown_node import (
|
3
|
-
ImageMarkdownBlockParams,
|
4
3
|
ImageMarkdownNode,
|
5
4
|
)
|
6
5
|
from notionary.blocks.image_block.image_models import CreateImageBlock
|
@@ -9,5 +8,4 @@ __all__ = [
|
|
9
8
|
"ImageElement",
|
10
9
|
"CreateImageBlock",
|
11
10
|
"ImageMarkdownNode",
|
12
|
-
"ImageMarkdownBlockParams",
|
13
11
|
]
|
@@ -4,49 +4,76 @@ import re
|
|
4
4
|
from typing import Optional
|
5
5
|
|
6
6
|
from notionary.blocks.base_block_element import BaseBlockElement
|
7
|
-
from notionary.blocks.file.file_element_models import
|
7
|
+
from notionary.blocks.file.file_element_models import (
|
8
|
+
ExternalFile,
|
9
|
+
FileType,
|
10
|
+
FileUploadFile,
|
11
|
+
)
|
8
12
|
from notionary.blocks.image_block.image_models import CreateImageBlock, FileBlock
|
9
13
|
from notionary.blocks.mixins.captions import CaptionMixin
|
14
|
+
from notionary.blocks.mixins.file_upload.file_upload_mixin import FileUploadMixin
|
10
15
|
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
11
16
|
from notionary.blocks.models import Block, BlockCreateResult, BlockType
|
12
17
|
|
13
18
|
|
14
|
-
class ImageElement(BaseBlockElement, CaptionMixin):
|
15
|
-
"""
|
19
|
+
class ImageElement(BaseBlockElement, CaptionMixin, FileUploadMixin):
|
20
|
+
r"""
|
16
21
|
Handles conversion between Markdown images and Notion image blocks.
|
17
22
|
|
23
|
+
Supports both external URLs and local image file uploads.
|
24
|
+
|
18
25
|
Markdown image syntax:
|
19
|
-
- [image](https://example.com/image.jpg) - URL
|
26
|
+
- [image](https://example.com/image.jpg) - External URL
|
27
|
+
- [image](./local/photo.png) - Local image file (will be uploaded)
|
28
|
+
- [image](C:\Pictures\avatar.jpg) - Absolute local path (will be uploaded)
|
20
29
|
- [image](https://example.com/image.jpg)(caption:This is a caption) - URL with caption
|
21
|
-
- (caption:Profile picture)[image](
|
30
|
+
- (caption:Profile picture)[image](./avatar.jpg) - Caption before URL
|
22
31
|
"""
|
23
32
|
|
24
|
-
#
|
25
|
-
IMAGE_PATTERN = re.compile(r"\[image\]\((
|
33
|
+
# Pattern matches both URLs and file paths
|
34
|
+
IMAGE_PATTERN = re.compile(r"\[image\]\(([^)]+)\)")
|
26
35
|
|
27
36
|
@classmethod
|
28
37
|
def match_notion(cls, block: Block) -> bool:
|
29
38
|
return block.type == BlockType.IMAGE and block.image
|
30
39
|
|
31
40
|
@classmethod
|
32
|
-
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
41
|
+
async def markdown_to_notion(cls, text: str) -> Optional[BlockCreateResult]:
|
33
42
|
"""Convert markdown image syntax to Notion ImageBlock."""
|
34
|
-
|
35
|
-
|
36
|
-
# Use our own regex to find the image URL
|
37
|
-
image_match = cls.IMAGE_PATTERN.search(clean_text)
|
38
|
-
if not image_match:
|
43
|
+
image_path = cls._extract_image_path(text.strip())
|
44
|
+
if not image_path:
|
39
45
|
return None
|
40
46
|
|
41
|
-
|
47
|
+
cls.logger.info(f"Processing image: {image_path}")
|
42
48
|
|
49
|
+
# Extract caption
|
43
50
|
caption_text = cls.extract_caption(text.strip())
|
44
51
|
caption_rich_text = cls.build_caption_rich_text(caption_text or "")
|
45
52
|
|
46
|
-
#
|
47
|
-
|
48
|
-
|
49
|
-
|
53
|
+
# Determine if it's a local file or external URL
|
54
|
+
if cls._is_local_file_path(image_path):
|
55
|
+
cls.logger.debug(f"Detected local image file: {image_path}")
|
56
|
+
|
57
|
+
# Upload the local image file using mixin method
|
58
|
+
file_upload_id = await cls._upload_local_file(image_path, "image")
|
59
|
+
if not file_upload_id:
|
60
|
+
cls.logger.error(f"Failed to upload image: {image_path}")
|
61
|
+
return None
|
62
|
+
|
63
|
+
image_block = FileBlock(
|
64
|
+
type=FileType.FILE_UPLOAD,
|
65
|
+
file_upload=FileUploadFile(id=file_upload_id),
|
66
|
+
caption=caption_rich_text,
|
67
|
+
)
|
68
|
+
|
69
|
+
else:
|
70
|
+
cls.logger.debug(f"Using external image URL: {image_path}")
|
71
|
+
|
72
|
+
image_block = FileBlock(
|
73
|
+
type=FileType.EXTERNAL,
|
74
|
+
external=ExternalFile(url=image_path),
|
75
|
+
caption=caption_rich_text,
|
76
|
+
)
|
50
77
|
|
51
78
|
return CreateImageBlock(image=image_block)
|
52
79
|
|
@@ -57,14 +84,15 @@ class ImageElement(BaseBlockElement, CaptionMixin):
|
|
57
84
|
|
58
85
|
fo = block.image
|
59
86
|
|
87
|
+
# Determine the source for markdown
|
60
88
|
if fo.type == FileType.EXTERNAL and fo.external:
|
61
|
-
|
89
|
+
source = fo.external.url
|
62
90
|
elif fo.type == FileType.FILE and fo.file:
|
63
|
-
|
91
|
+
source = fo.file.url
|
64
92
|
else:
|
65
93
|
return None
|
66
94
|
|
67
|
-
result = f"[image]({
|
95
|
+
result = f"[image]({source})"
|
68
96
|
|
69
97
|
# Add caption if present
|
70
98
|
caption_markdown = await cls.format_caption_for_markdown(fo.caption or [])
|
@@ -78,12 +106,25 @@ class ImageElement(BaseBlockElement, CaptionMixin):
|
|
78
106
|
"""Get system prompt information for image blocks."""
|
79
107
|
return BlockElementMarkdownInformation(
|
80
108
|
block_type=cls.__name__,
|
81
|
-
description="Image blocks display images from external URLs with optional captions",
|
109
|
+
description="Image blocks display images from external URLs or upload local image files with optional captions",
|
82
110
|
syntax_examples=[
|
83
111
|
"[image](https://example.com/photo.jpg)",
|
112
|
+
"[image](./local/screenshot.png)",
|
113
|
+
"[image](C:\\Pictures\\avatar.jpg)",
|
84
114
|
"[image](https://example.com/diagram.png)(caption:Architecture Diagram)",
|
85
|
-
"(caption:Sales Chart)[image](
|
86
|
-
"[image](
|
115
|
+
"(caption:Sales Chart)[image](./chart.svg)",
|
116
|
+
"[image](./screenshot.png)(caption:Dashboard **overview**)",
|
87
117
|
],
|
88
|
-
usage_guidelines="Use for displaying images from external URLs. Supports common image formats (jpg, png, gif, svg, webp). Caption supports rich text formatting and describes the image content.",
|
118
|
+
usage_guidelines="Use for displaying images from external URLs or local files. Local image files will be automatically uploaded to Notion. Supports common image formats (jpg, png, gif, svg, webp, bmp, tiff, heic). Caption supports rich text formatting and describes the image content.",
|
89
119
|
)
|
120
|
+
|
121
|
+
@classmethod
|
122
|
+
def _extract_image_path(cls, text: str) -> Optional[str]:
|
123
|
+
"""Extract image path/URL from text, handling caption patterns."""
|
124
|
+
clean_text = cls.remove_caption(text)
|
125
|
+
|
126
|
+
match = cls.IMAGE_PATTERN.search(clean_text)
|
127
|
+
if match:
|
128
|
+
return match.group(1).strip()
|
129
|
+
|
130
|
+
return None
|
@@ -1,33 +1,18 @@
|
|
1
|
-
from __future__ import annotations
|
2
|
-
|
3
1
|
from typing import Optional
|
4
2
|
|
5
|
-
from
|
6
|
-
|
7
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
3
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
8
4
|
from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
|
9
5
|
|
10
6
|
|
11
|
-
class ImageMarkdownBlockParams(BaseModel):
|
12
|
-
url: str
|
13
|
-
caption: Optional[str] = None
|
14
|
-
|
15
|
-
|
16
7
|
class ImageMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
17
8
|
"""
|
9
|
+
Enhanced Image node with Pydantic integration.
|
18
10
|
Programmatic interface for creating Notion-style image blocks.
|
19
11
|
"""
|
20
12
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
self.url = url
|
25
|
-
self.caption = caption
|
26
|
-
# Note: 'alt' is kept for API compatibility but not used in Notion syntax
|
27
|
-
|
28
|
-
@classmethod
|
29
|
-
def from_params(cls, params: ImageMarkdownBlockParams) -> ImageMarkdownNode:
|
30
|
-
return cls(url=params.url, caption=params.caption)
|
13
|
+
url: str
|
14
|
+
caption: Optional[str] = None
|
15
|
+
alt: Optional[str] = None
|
31
16
|
|
32
17
|
def to_markdown(self) -> str:
|
33
18
|
"""Return the Markdown representation.
|