notionary 0.2.23__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/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/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/client.py +1 -1
- 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/markdown_whitespace_processor.py +129 -0
- notionary/page/notion_page.py +35 -40
- notionary/page/page_content_deleting_service.py +1 -1
- notionary/page/page_content_writer.py +32 -129
- notionary/page/page_context.py +0 -5
- 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 +1 -3
- {notionary-0.2.23.dist-info → notionary-0.2.24.dist-info}/METADATA +16 -1
- {notionary-0.2.23.dist-info → notionary-0.2.24.dist-info}/RECORD +91 -93
- 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.23.dist-info → notionary-0.2.24.dist-info}/LICENSE +0 -0
- {notionary-0.2.23.dist-info → notionary-0.2.24.dist-info}/WHEEL +0 -0
@@ -10,7 +10,7 @@ from notionary.blocks.table_of_contents.table_of_contents_models import (
|
|
10
10
|
CreateTableOfContentsBlock,
|
11
11
|
TableOfContentsBlock,
|
12
12
|
)
|
13
|
-
from notionary.blocks.types import BlockType
|
13
|
+
from notionary.blocks.types import BlockType, BlockColor
|
14
14
|
|
15
15
|
|
16
16
|
class TableOfContentsElement(BaseBlockElement):
|
@@ -18,7 +18,7 @@ class TableOfContentsElement(BaseBlockElement):
|
|
18
18
|
Handles conversion between Markdown [toc] syntax and Notion table_of_contents blocks.
|
19
19
|
|
20
20
|
Markdown syntax:
|
21
|
-
- [toc] → default color
|
21
|
+
- [toc] → default color (enum default)
|
22
22
|
- [toc](blue) → custom color
|
23
23
|
- [toc](blue_background) → custom background color
|
24
24
|
"""
|
@@ -34,35 +34,47 @@ class TableOfContentsElement(BaseBlockElement):
|
|
34
34
|
if not (input_match := cls.PATTERN.match(text.strip())):
|
35
35
|
return None
|
36
36
|
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
37
|
+
color_str = input_match.group("color")
|
38
|
+
if color_str:
|
39
|
+
# Validate against the enum; fallback to default if unknown
|
40
|
+
try:
|
41
|
+
color = BlockColor(color_str.lower())
|
42
|
+
toc_payload = TableOfContentsBlock(color=color)
|
43
|
+
except ValueError:
|
44
|
+
# Unknown color → omit to use enum default
|
45
|
+
toc_payload = TableOfContentsBlock()
|
46
|
+
else:
|
47
|
+
# No color provided → omit to let enum default apply
|
48
|
+
toc_payload = TableOfContentsBlock()
|
49
|
+
|
50
|
+
return CreateTableOfContentsBlock(table_of_contents=toc_payload)
|
41
51
|
|
42
52
|
@classmethod
|
43
53
|
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
44
|
-
#
|
54
|
+
# Correct guard: if not a TOC or missing payload → no match
|
45
55
|
if block.type != BlockType.TABLE_OF_CONTENTS or not block.table_of_contents:
|
46
56
|
return None
|
47
57
|
|
48
|
-
color = block.table_of_contents.color
|
49
|
-
|
50
|
-
if color ==
|
58
|
+
color = block.table_of_contents.color
|
59
|
+
# If None or default → plain [toc]
|
60
|
+
if color is None or color == BlockColor.DEFAULT:
|
51
61
|
return "[toc]"
|
52
|
-
return f"[toc]({color})"
|
62
|
+
return f"[toc]({color.value})"
|
53
63
|
|
54
|
-
@classmethod
|
55
64
|
@classmethod
|
56
65
|
def get_system_prompt_information(cls) -> Optional[BlockElementMarkdownInformation]:
|
57
|
-
"""
|
66
|
+
"""System prompt info for table of contents blocks."""
|
58
67
|
return BlockElementMarkdownInformation(
|
59
68
|
block_type=cls.__name__,
|
60
|
-
description="Table of contents blocks automatically generate navigation for page headings",
|
69
|
+
description="Table of contents blocks automatically generate navigation for page headings.",
|
61
70
|
syntax_examples=[
|
62
71
|
"[toc]",
|
63
72
|
"[toc](blue)",
|
64
73
|
"[toc](blue_background)",
|
65
74
|
"[toc](gray_background)",
|
66
75
|
],
|
67
|
-
usage_guidelines=
|
76
|
+
usage_guidelines=(
|
77
|
+
"Use to auto-generate a clickable table of contents from page headings. "
|
78
|
+
"The color parameter is optional; if omitted, the default enum color is used."
|
79
|
+
),
|
68
80
|
)
|
@@ -1,18 +1,11 @@
|
|
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 TableOfContentsMarkdownBlockParams(BaseModel):
|
11
|
-
color: Optional[str] = "default"
|
3
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
12
4
|
|
13
5
|
|
14
6
|
class TableOfContentsMarkdownNode(MarkdownNode):
|
15
7
|
"""
|
8
|
+
Enhanced Table of Contents node with Pydantic integration.
|
16
9
|
Programmatic interface for creating Markdown table of contents blocks.
|
17
10
|
Example:
|
18
11
|
[toc]
|
@@ -20,14 +13,7 @@ class TableOfContentsMarkdownNode(MarkdownNode):
|
|
20
13
|
[toc](blue_background)
|
21
14
|
"""
|
22
15
|
|
23
|
-
|
24
|
-
self.color = color or "default"
|
25
|
-
|
26
|
-
@classmethod
|
27
|
-
def from_params(
|
28
|
-
cls, params: TableOfContentsMarkdownBlockParams
|
29
|
-
) -> TableOfContentsMarkdownNode:
|
30
|
-
return cls(color=params.color)
|
16
|
+
color: Optional[str] = "default"
|
31
17
|
|
32
18
|
def to_markdown(self) -> str:
|
33
19
|
if self.color == "default":
|
@@ -1,4 +1,4 @@
|
|
1
|
-
from typing import Literal
|
1
|
+
from typing import Literal, Optional
|
2
2
|
|
3
3
|
from pydantic import BaseModel
|
4
4
|
|
@@ -8,7 +8,7 @@ from notionary.blocks.types import BlockColor
|
|
8
8
|
class TableOfContentsBlock(BaseModel):
|
9
9
|
"""Inneres Payload-Objekt: { table_of_contents: { color: ... } }"""
|
10
10
|
|
11
|
-
color: BlockColor = BlockColor.DEFAULT
|
11
|
+
color: Optional[BlockColor] = BlockColor.DEFAULT
|
12
12
|
|
13
13
|
|
14
14
|
class CreateTableOfContentsBlock(BaseModel):
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from notionary.blocks.todo.todo_element import TodoElement
|
2
2
|
from notionary.blocks.todo.todo_markdown_node import (
|
3
|
-
TodoMarkdownBlockParams,
|
4
3
|
TodoMarkdownNode,
|
5
4
|
)
|
6
5
|
from notionary.blocks.todo.todo_models import CreateToDoBlock, ToDoBlock
|
@@ -10,5 +9,4 @@ __all__ = [
|
|
10
9
|
"ToDoBlock",
|
11
10
|
"CreateToDoBlock",
|
12
11
|
"TodoMarkdownNode",
|
13
|
-
"TodoMarkdownBlockParams",
|
14
12
|
]
|
@@ -1,32 +1,21 @@
|
|
1
|
-
from
|
2
|
-
|
3
|
-
from pydantic import BaseModel
|
4
|
-
|
5
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
6
|
-
|
7
|
-
|
8
|
-
class TodoMarkdownBlockParams(BaseModel):
|
9
|
-
text: str
|
10
|
-
checked: bool = False
|
11
|
-
marker: str = "-"
|
1
|
+
from pydantic import Field
|
2
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
12
3
|
|
13
4
|
|
14
5
|
class TodoMarkdownNode(MarkdownNode):
|
15
6
|
"""
|
7
|
+
Enhanced Todo node with Pydantic integration.
|
16
8
|
Programmatic interface for creating Markdown todo items (checkboxes).
|
17
9
|
Supports checked and unchecked states.
|
18
10
|
Example: - [ ] Task, - [x] Done
|
19
11
|
"""
|
20
12
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
self.marker = marker if marker in {"-", "*", "+"} else "-"
|
25
|
-
|
26
|
-
@classmethod
|
27
|
-
def from_params(cls, params: TodoMarkdownBlockParams) -> TodoMarkdownNode:
|
28
|
-
return cls(text=params.text, checked=params.checked, marker=params.marker)
|
13
|
+
text: str
|
14
|
+
checked: bool = False
|
15
|
+
marker: str = Field(default="-")
|
29
16
|
|
30
17
|
def to_markdown(self) -> str:
|
18
|
+
# Validate marker in to_markdown to ensure it's valid
|
19
|
+
valid_marker = self.marker if self.marker in {"-", "*", "+"} else "-"
|
31
20
|
checkbox = "[x]" if self.checked else "[ ]"
|
32
|
-
return f"{
|
21
|
+
return f"{valid_marker} {checkbox} {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,6 @@ class ToDoBlock(BaseModel):
|
|
11
11
|
rich_text: list[RichTextObject]
|
12
12
|
checked: bool = False
|
13
13
|
color: BlockColor = BlockColor.DEFAULT
|
14
|
-
children: list[Block] = Field(default_factory=list)
|
15
14
|
|
16
15
|
|
17
16
|
class CreateToDoBlock(BaseModel):
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from notionary.blocks.toggle.toggle_element import ToggleElement
|
2
2
|
from notionary.blocks.toggle.toggle_markdown_node import (
|
3
|
-
ToggleMarkdownBlockParams,
|
4
3
|
ToggleMarkdownNode,
|
5
4
|
)
|
6
5
|
from notionary.blocks.toggle.toggle_models import CreateToggleBlock, ToggleBlock
|
@@ -10,5 +9,4 @@ __all__ = [
|
|
10
9
|
"ToggleBlock",
|
11
10
|
"CreateToggleBlock",
|
12
11
|
"ToggleMarkdownNode",
|
13
|
-
"ToggleMarkdownBlockParams",
|
14
12
|
]
|
@@ -1,18 +1,9 @@
|
|
1
|
-
from
|
2
|
-
|
3
|
-
from pydantic import BaseModel
|
4
|
-
|
5
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
6
|
-
|
7
|
-
|
8
|
-
class ToggleMarkdownBlockParams(BaseModel):
|
9
|
-
title: str
|
10
|
-
children: list[MarkdownNode]
|
11
|
-
model_config = {"arbitrary_types_allowed": True}
|
1
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
12
2
|
|
13
3
|
|
14
4
|
class ToggleMarkdownNode(MarkdownNode):
|
15
5
|
"""
|
6
|
+
Enhanced Toggle node with Pydantic integration.
|
16
7
|
Clean programmatic interface for creating Notion-style Markdown toggle blocks
|
17
8
|
with the simplified +++ "Title" syntax.
|
18
9
|
|
@@ -23,16 +14,11 @@ class ToggleMarkdownNode(MarkdownNode):
|
|
23
14
|
+++
|
24
15
|
"""
|
25
16
|
|
26
|
-
|
27
|
-
|
28
|
-
self.children = children
|
29
|
-
|
30
|
-
@classmethod
|
31
|
-
def from_params(cls, params: ToggleMarkdownBlockParams) -> ToggleMarkdownNode:
|
32
|
-
return cls(title=params.title, children=params.children)
|
17
|
+
title: str
|
18
|
+
children: list[MarkdownNode] = []
|
33
19
|
|
34
20
|
def to_markdown(self) -> str:
|
35
|
-
result = f"+++{self.title}"
|
21
|
+
result = f"+++ {self.title}"
|
36
22
|
|
37
23
|
if not self.children:
|
38
24
|
result += "\n+++"
|
@@ -2,12 +2,10 @@ from notionary.blocks.toggleable_heading.toggleable_heading_element import (
|
|
2
2
|
ToggleableHeadingElement,
|
3
3
|
)
|
4
4
|
from notionary.blocks.toggleable_heading.toggleable_heading_markdown_node import (
|
5
|
-
ToggleableHeadingMarkdownBlockParams,
|
6
5
|
ToggleableHeadingMarkdownNode,
|
7
6
|
)
|
8
7
|
|
9
8
|
__all__ = [
|
10
9
|
"ToggleableHeadingElement",
|
11
10
|
"ToggleableHeadingMarkdownNode",
|
12
|
-
"ToggleableHeadingMarkdownBlockParams",
|
13
11
|
]
|
@@ -1,19 +1,11 @@
|
|
1
|
-
from
|
1
|
+
from pydantic import Field
|
2
2
|
|
3
|
-
from
|
4
|
-
|
5
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
6
|
-
|
7
|
-
|
8
|
-
class ToggleableHeadingMarkdownBlockParams(BaseModel):
|
9
|
-
text: str
|
10
|
-
level: int
|
11
|
-
children: list[MarkdownNode]
|
12
|
-
model_config = {"arbitrary_types_allowed": True}
|
3
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
13
4
|
|
14
5
|
|
15
6
|
class ToggleableHeadingMarkdownNode(MarkdownNode):
|
16
7
|
"""
|
8
|
+
Enhanced Toggleable Heading node with Pydantic integration.
|
17
9
|
Clean programmatic interface for creating collapsible Markdown headings (toggleable headings)
|
18
10
|
with pipe-prefixed nested content using MarkdownNode children.
|
19
11
|
|
@@ -23,18 +15,9 @@ class ToggleableHeadingMarkdownNode(MarkdownNode):
|
|
23
15
|
+++
|
24
16
|
"""
|
25
17
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
self.text = text
|
30
|
-
self.level = level
|
31
|
-
self.children = children
|
32
|
-
|
33
|
-
@classmethod
|
34
|
-
def from_params(
|
35
|
-
cls, params: ToggleableHeadingMarkdownBlockParams
|
36
|
-
) -> ToggleableHeadingMarkdownNode:
|
37
|
-
return cls(text=params.text, level=params.level, children=params.children)
|
18
|
+
text: str
|
19
|
+
level: int = Field(ge=1, le=3)
|
20
|
+
children: list[MarkdownNode] = []
|
38
21
|
|
39
22
|
def to_markdown(self) -> str:
|
40
23
|
prefix = "+++" + ("#" * self.level)
|
@@ -1,7 +1,6 @@
|
|
1
1
|
from notionary.blocks.video.video_element import VideoElement
|
2
2
|
from notionary.blocks.video.video_element_models import CreateVideoBlock
|
3
3
|
from notionary.blocks.video.video_markdown_node import (
|
4
|
-
VideoMarkdownBlockParams,
|
5
4
|
VideoMarkdownNode,
|
6
5
|
)
|
7
6
|
|
@@ -9,5 +8,4 @@ __all__ = [
|
|
9
8
|
"VideoElement",
|
10
9
|
"CreateVideoBlock",
|
11
10
|
"VideoMarkdownNode",
|
12
|
-
"VideoMarkdownBlockParams",
|
13
11
|
]
|
@@ -1,66 +1,126 @@
|
|
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
|
7
|
-
from notionary.blocks.file.file_element_models import
|
8
|
+
from notionary.blocks.file.file_element_models import (
|
9
|
+
ExternalFile,
|
10
|
+
FileBlock,
|
11
|
+
FileType,
|
12
|
+
FileUploadFile,
|
13
|
+
)
|
8
14
|
from notionary.blocks.mixins.captions import CaptionMixin
|
15
|
+
from notionary.blocks.mixins.file_upload.file_upload_mixin import FileUploadMixin
|
9
16
|
from notionary.blocks.syntax_prompt_builder import BlockElementMarkdownInformation
|
10
17
|
from notionary.blocks.models import Block, BlockCreateResult
|
11
18
|
from notionary.blocks.types import BlockType
|
12
19
|
from notionary.blocks.video.video_element_models import CreateVideoBlock
|
13
20
|
|
14
21
|
|
15
|
-
class VideoElement(BaseBlockElement, CaptionMixin):
|
16
|
-
"""
|
22
|
+
class VideoElement(BaseBlockElement, CaptionMixin, FileUploadMixin):
|
23
|
+
r"""
|
17
24
|
Handles conversion between Markdown video embeds and Notion video blocks.
|
18
25
|
|
19
|
-
|
20
|
-
- [video](https://example.com/video.mp4) - URL only
|
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
|
+
Supports external URLs (YouTube, Vimeo, direct links) and local video file uploads.
|
23
27
|
|
24
|
-
|
28
|
+
Markdown video syntax:
|
29
|
+
- [video](https://example.com/video.mp4) - External URL
|
30
|
+
- [video](./local/movie.mp4) - Local video file (will be uploaded)
|
31
|
+
- [video](C:\Videos\tutorial.mov) - Absolute local path (will be uploaded)
|
32
|
+
- [video](https://youtube.com/watch?v=abc123)(caption:Demo Video) - URL with caption
|
33
|
+
- (caption:Tutorial video)[video](./local.mp4) - Caption before URL
|
25
34
|
"""
|
26
35
|
|
27
|
-
#
|
28
|
-
VIDEO_PATTERN = re.compile(r"\[video\]\((
|
36
|
+
# Pattern matches both URLs and file paths
|
37
|
+
VIDEO_PATTERN = re.compile(r"\[video\]\(([^)]+)\)")
|
29
38
|
|
30
39
|
YOUTUBE_PATTERNS = [
|
31
40
|
re.compile(r"(?:https?://)?(?:www\.)?youtube\.com/watch\?v=([\w-]{11})"),
|
32
41
|
re.compile(r"(?:https?://)?(?:www\.)?youtu\.be/([\w-]{11})"),
|
33
42
|
]
|
34
43
|
|
44
|
+
SUPPORTED_EXTENSIONS = {
|
45
|
+
".mp4",
|
46
|
+
".avi",
|
47
|
+
".mov",
|
48
|
+
".wmv",
|
49
|
+
".flv",
|
50
|
+
".webm",
|
51
|
+
".mkv",
|
52
|
+
".m4v",
|
53
|
+
".3gp",
|
54
|
+
}
|
55
|
+
|
35
56
|
@classmethod
|
36
57
|
def match_notion(cls, block: Block) -> bool:
|
37
58
|
return block.type == BlockType.VIDEO and block.video
|
38
59
|
|
39
60
|
@classmethod
|
40
|
-
async def markdown_to_notion(cls, text: str) -> BlockCreateResult:
|
61
|
+
async def markdown_to_notion(cls, text: str) -> Optional[BlockCreateResult]:
|
41
62
|
"""Convert markdown video syntax to a Notion VideoBlock."""
|
42
|
-
#
|
43
|
-
|
44
|
-
if not
|
63
|
+
# Extract the path/URL
|
64
|
+
path = cls._extract_video_path(text.strip())
|
65
|
+
if not path:
|
45
66
|
return None
|
46
67
|
|
47
|
-
|
68
|
+
# Check if it's a local file path
|
69
|
+
if cls._is_local_file_path(path):
|
70
|
+
# Verify file exists and has supported extension
|
71
|
+
video_path = Path(path)
|
72
|
+
if not video_path.exists():
|
73
|
+
cls.logger.warning(f"Video file not found: {path}")
|
74
|
+
return None
|
48
75
|
|
49
|
-
|
50
|
-
|
51
|
-
|
76
|
+
if video_path.suffix.lower() not in cls.SUPPORTED_EXTENSIONS:
|
77
|
+
cls.logger.warning(f"Unsupported video format: {video_path.suffix}")
|
78
|
+
return None
|
52
79
|
|
53
|
-
|
54
|
-
caption_text = cls.extract_caption(text.strip())
|
55
|
-
caption_rich_text = cls.build_caption_rich_text(caption_text or "")
|
80
|
+
cls.logger.info(f"Uploading local video file: {path}")
|
56
81
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
82
|
+
# Upload the local video file
|
83
|
+
file_upload_id = await cls._upload_local_file(path, "video")
|
84
|
+
if not file_upload_id:
|
85
|
+
cls.logger.error(f"Failed to upload video file: {path}")
|
86
|
+
return None
|
87
|
+
|
88
|
+
cls.logger.info(
|
89
|
+
f"Successfully uploaded video file with ID: {file_upload_id}"
|
90
|
+
)
|
91
|
+
|
92
|
+
# Use mixin to extract caption (if present anywhere in text)
|
93
|
+
caption_text = cls.extract_caption(text.strip())
|
94
|
+
caption_rich_text = cls.build_caption_rich_text(caption_text or "")
|
95
|
+
|
96
|
+
video_block = FileBlock(
|
97
|
+
type=FileType.FILE_UPLOAD,
|
98
|
+
file_upload=FileUploadFile(id=file_upload_id),
|
99
|
+
caption=caption_rich_text,
|
100
|
+
)
|
101
|
+
|
102
|
+
return CreateVideoBlock(video=video_block)
|
103
|
+
|
104
|
+
else:
|
105
|
+
# Handle external URL (YouTube, Vimeo, direct links)
|
106
|
+
url = path
|
62
107
|
|
63
|
-
|
108
|
+
# Check for YouTube and normalize URL
|
109
|
+
vid_id = cls._get_youtube_id(url)
|
110
|
+
if vid_id:
|
111
|
+
url = f"https://www.youtube.com/watch?v={vid_id}"
|
112
|
+
|
113
|
+
# Use mixin to extract caption (if present anywhere in text)
|
114
|
+
caption_text = cls.extract_caption(text.strip())
|
115
|
+
caption_rich_text = cls.build_caption_rich_text(caption_text or "")
|
116
|
+
|
117
|
+
video_block = FileBlock(
|
118
|
+
type=FileType.EXTERNAL,
|
119
|
+
external=ExternalFile(url=url),
|
120
|
+
caption=caption_rich_text,
|
121
|
+
)
|
122
|
+
|
123
|
+
return CreateVideoBlock(video=video_block)
|
64
124
|
|
65
125
|
@classmethod
|
66
126
|
async def notion_to_markdown(cls, block: Block) -> Optional[str]:
|
@@ -68,14 +128,16 @@ class VideoElement(BaseBlockElement, CaptionMixin):
|
|
68
128
|
return None
|
69
129
|
|
70
130
|
fo = block.video
|
131
|
+
url = None
|
71
132
|
|
72
|
-
#
|
133
|
+
# Handle both external URLs and uploaded files
|
73
134
|
if fo.type == FileType.EXTERNAL and fo.external:
|
74
135
|
url = fo.external.url
|
75
136
|
elif fo.type == FileType.FILE and fo.file:
|
76
137
|
url = fo.file.url
|
77
|
-
|
78
|
-
|
138
|
+
|
139
|
+
if not url:
|
140
|
+
return None
|
79
141
|
|
80
142
|
result = f"[video]({url})"
|
81
143
|
|
@@ -99,13 +161,27 @@ class VideoElement(BaseBlockElement, CaptionMixin):
|
|
99
161
|
"""Get system prompt information for video blocks."""
|
100
162
|
return BlockElementMarkdownInformation(
|
101
163
|
block_type=cls.__name__,
|
102
|
-
description="Video blocks embed videos from external URLs
|
164
|
+
description="Video blocks embed videos from external URLs (YouTube, Vimeo, direct links) or upload local video files with optional captions",
|
103
165
|
syntax_examples=[
|
104
166
|
"[video](https://youtube.com/watch?v=abc123)",
|
105
167
|
"[video](https://vimeo.com/123456789)",
|
168
|
+
"[video](./local/tutorial.mp4)",
|
169
|
+
"[video](C:\\Videos\\presentation.mov)",
|
106
170
|
"[video](https://example.com/video.mp4)(caption:Demo Video)",
|
107
|
-
"(caption:Tutorial)[video](
|
108
|
-
"[video](
|
171
|
+
"(caption:Tutorial)[video](./demo.mp4)",
|
172
|
+
"[video](./training.mp4)(caption:**Important** tutorial)",
|
109
173
|
],
|
110
|
-
usage_guidelines="Use for embedding videos from supported platforms or
|
174
|
+
usage_guidelines="Use for embedding videos from supported platforms or local video files. Supports YouTube, Vimeo, direct video URLs, and local file uploads. Supports common video formats (mp4, avi, mov, wmv, flv, webm, mkv, m4v, 3gp). Caption supports rich text formatting and describes the video content.",
|
111
175
|
)
|
176
|
+
|
177
|
+
@classmethod
|
178
|
+
def _extract_video_path(cls, text: str) -> Optional[str]:
|
179
|
+
"""Extract video path/URL from text, handling caption patterns."""
|
180
|
+
clean_text = cls.remove_caption(text)
|
181
|
+
|
182
|
+
# Now extract the path/URL from clean text
|
183
|
+
match = cls.VIDEO_PATTERN.search(clean_text)
|
184
|
+
if match:
|
185
|
+
return match.group(1).strip()
|
186
|
+
|
187
|
+
return None
|
@@ -2,29 +2,18 @@ from __future__ import annotations
|
|
2
2
|
|
3
3
|
from typing import Optional
|
4
4
|
|
5
|
-
from
|
6
|
-
|
7
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
5
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
8
6
|
from notionary.blocks.mixins.captions import CaptionMarkdownNodeMixin
|
9
7
|
|
10
8
|
|
11
|
-
class VideoMarkdownBlockParams(BaseModel):
|
12
|
-
url: str
|
13
|
-
caption: Optional[str] = None
|
14
|
-
|
15
|
-
|
16
9
|
class VideoMarkdownNode(MarkdownNode, CaptionMarkdownNodeMixin):
|
17
10
|
"""
|
11
|
+
Enhanced Video node with Pydantic integration.
|
18
12
|
Programmatic interface for creating Notion-style video blocks.
|
19
13
|
"""
|
20
14
|
|
21
|
-
|
22
|
-
|
23
|
-
self.caption = caption
|
24
|
-
|
25
|
-
@classmethod
|
26
|
-
def from_params(cls, params: VideoMarkdownBlockParams) -> VideoMarkdownNode:
|
27
|
-
return cls(url=params.url, caption=params.caption)
|
15
|
+
url: str
|
16
|
+
caption: Optional[str] = None
|
28
17
|
|
29
18
|
def to_markdown(self) -> str:
|
30
19
|
"""Return the Markdown representation.
|
notionary/comments/client.py
CHANGED
notionary/file_upload/client.py
CHANGED
@@ -11,6 +11,7 @@ from notionary.file_upload.models import (
|
|
11
11
|
FileUploadCreateRequest,
|
12
12
|
FileUploadListResponse,
|
13
13
|
FileUploadResponse,
|
14
|
+
UploadMode,
|
14
15
|
)
|
15
16
|
|
16
17
|
|
@@ -25,7 +26,7 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
25
26
|
filename: str,
|
26
27
|
content_type: Optional[str] = None,
|
27
28
|
content_length: Optional[int] = None,
|
28
|
-
mode:
|
29
|
+
mode: UploadMode = UploadMode.SINGLE_PART,
|
29
30
|
) -> Optional[FileUploadResponse]:
|
30
31
|
"""
|
31
32
|
Create a new file upload.
|
@@ -34,7 +35,7 @@ class NotionFileUploadClient(BaseNotionClient):
|
|
34
35
|
filename: Name of the file (max 900 bytes)
|
35
36
|
content_type: MIME type of the file
|
36
37
|
content_length: Size of the file in bytes
|
37
|
-
mode: Upload mode (
|
38
|
+
mode: Upload mode (UploadMode.SINGLE_PART or UploadMode.MULTI_PART)
|
38
39
|
|
39
40
|
Returns:
|
40
41
|
FileUploadResponse or None if failed
|
notionary/file_upload/models.py
CHANGED
@@ -1,8 +1,17 @@
|
|
1
|
+
from enum import Enum
|
2
|
+
|
1
3
|
from typing import Literal, Optional
|
2
4
|
|
3
5
|
from pydantic import BaseModel
|
4
6
|
|
5
7
|
|
8
|
+
class UploadMode(str, Enum):
|
9
|
+
"""Enum for file upload modes."""
|
10
|
+
|
11
|
+
SINGLE_PART = "single_part"
|
12
|
+
MULTI_PART = "multi_part"
|
13
|
+
|
14
|
+
|
6
15
|
class FileUploadResponse(BaseModel):
|
7
16
|
"""
|
8
17
|
Represents a Notion file upload object as returned by the File Upload API.
|
@@ -44,7 +53,7 @@ class FileUploadCreateRequest(BaseModel):
|
|
44
53
|
filename: str
|
45
54
|
content_type: Optional[str] = None
|
46
55
|
content_length: Optional[int] = None
|
47
|
-
mode:
|
56
|
+
mode: UploadMode = UploadMode.SINGLE_PART
|
48
57
|
|
49
58
|
def model_dump(self, **kwargs):
|
50
59
|
"""Override to exclude None values"""
|