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.
Files changed (99) hide show
  1. notionary/__init__.py +1 -1
  2. notionary/blocks/__init__.py +3 -1
  3. notionary/blocks/audio/__init__.py +0 -2
  4. notionary/blocks/audio/audio_element.py +92 -49
  5. notionary/blocks/audio/audio_markdown_node.py +4 -17
  6. notionary/blocks/bookmark/__init__.py +0 -2
  7. notionary/blocks/bookmark/bookmark_markdown_node.py +5 -21
  8. notionary/blocks/breadcrumbs/__init__.py +0 -2
  9. notionary/blocks/breadcrumbs/breadcrumb_markdown_node.py +2 -21
  10. notionary/blocks/bulleted_list/__init__.py +0 -2
  11. notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +3 -17
  12. notionary/blocks/bulleted_list/bulleted_list_models.py +0 -1
  13. notionary/blocks/callout/__init__.py +0 -2
  14. notionary/blocks/callout/callout_markdown_node.py +4 -18
  15. notionary/blocks/callout/callout_models.py +3 -4
  16. notionary/blocks/code/code_markdown_node.py +5 -19
  17. notionary/blocks/column/__init__.py +0 -4
  18. notionary/blocks/column/column_list_markdown_node.py +3 -19
  19. notionary/blocks/column/column_markdown_node.py +4 -21
  20. notionary/blocks/divider/__init__.py +0 -2
  21. notionary/blocks/divider/divider_markdown_node.py +2 -16
  22. notionary/blocks/embed/__init__.py +0 -2
  23. notionary/blocks/embed/embed_markdown_node.py +4 -17
  24. notionary/blocks/equation/__init__.py +0 -1
  25. notionary/blocks/equation/equation_element_markdown_node.py +3 -15
  26. notionary/blocks/file/__init__.py +0 -2
  27. notionary/blocks/file/file_element.py +67 -46
  28. notionary/blocks/file/file_element_markdown_node.py +4 -17
  29. notionary/blocks/heading/__init__.py +0 -2
  30. notionary/blocks/heading/heading_markdown_node.py +5 -19
  31. notionary/blocks/heading/heading_models.py +3 -3
  32. notionary/blocks/image_block/__init__.py +0 -2
  33. notionary/blocks/image_block/image_element.py +66 -25
  34. notionary/blocks/image_block/image_markdown_node.py +5 -20
  35. notionary/{markdown → blocks/markdown}/markdown_builder.py +29 -233
  36. notionary/blocks/markdown/markdown_node.py +25 -0
  37. notionary/blocks/mixins/file_upload/__init__.py +3 -0
  38. notionary/blocks/mixins/file_upload/file_upload_mixin.py +320 -0
  39. notionary/blocks/numbered_list/__init__.py +0 -1
  40. notionary/blocks/numbered_list/numbered_list_markdown_node.py +3 -17
  41. notionary/blocks/numbered_list/numbered_list_models.py +3 -3
  42. notionary/blocks/paragraph/__init__.py +0 -2
  43. notionary/blocks/paragraph/paragraph_markdown_node.py +3 -13
  44. notionary/blocks/pdf/__init__.py +0 -2
  45. notionary/blocks/pdf/pdf_element.py +81 -32
  46. notionary/blocks/pdf/pdf_markdown_node.py +5 -18
  47. notionary/blocks/quote/__init__.py +0 -2
  48. notionary/blocks/quote/quote_markdown_node.py +3 -13
  49. notionary/blocks/registry/__init__.py +1 -2
  50. notionary/blocks/registry/block_registry.py +116 -61
  51. notionary/blocks/table/__init__.py +0 -2
  52. notionary/blocks/table/table_markdown_node.py +17 -16
  53. notionary/blocks/table_of_contents/__init__.py +0 -2
  54. notionary/blocks/table_of_contents/table_of_contents_element.py +27 -15
  55. notionary/blocks/table_of_contents/table_of_contents_markdown_node.py +3 -17
  56. notionary/blocks/table_of_contents/table_of_contents_models.py +2 -2
  57. notionary/blocks/todo/__init__.py +0 -2
  58. notionary/blocks/todo/todo_markdown_node.py +9 -20
  59. notionary/blocks/todo/todo_models.py +2 -3
  60. notionary/blocks/toggle/__init__.py +0 -2
  61. notionary/blocks/toggle/toggle_markdown_node.py +5 -19
  62. notionary/blocks/toggleable_heading/__init__.py +0 -2
  63. notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +6 -23
  64. notionary/blocks/video/__init__.py +0 -2
  65. notionary/blocks/video/video_element.py +110 -34
  66. notionary/blocks/video/video_markdown_node.py +4 -15
  67. notionary/comments/client.py +1 -1
  68. notionary/file_upload/client.py +3 -2
  69. notionary/file_upload/models.py +10 -1
  70. notionary/file_upload/notion_file_upload.py +5 -5
  71. notionary/page/markdown_whitespace_processor.py +129 -0
  72. notionary/page/notion_page.py +35 -40
  73. notionary/page/page_content_deleting_service.py +1 -1
  74. notionary/page/page_content_writer.py +32 -129
  75. notionary/page/page_context.py +0 -5
  76. notionary/page/reader/handler/column_list_renderer.py +2 -2
  77. notionary/page/reader/handler/column_renderer.py +2 -2
  78. notionary/page/reader/handler/line_renderer.py +2 -2
  79. notionary/page/reader/handler/toggle_renderer.py +2 -2
  80. notionary/page/reader/handler/toggleable_heading_renderer.py +2 -2
  81. notionary/page/writer/handler/toggle_handler.py +8 -4
  82. notionary/page/writer/handler/toggleable_heading_handler.py +3 -2
  83. notionary/page/writer/markdown_to_notion_converter.py +74 -30
  84. notionary/schemas/__init__.py +3 -0
  85. notionary/schemas/base.py +73 -0
  86. notionary/shared/__init__.py +1 -3
  87. {notionary-0.2.23.dist-info → notionary-0.2.24.dist-info}/METADATA +16 -1
  88. {notionary-0.2.23.dist-info → notionary-0.2.24.dist-info}/RECORD +91 -93
  89. notionary/blocks/guards.py +0 -22
  90. notionary/blocks/registry/block_registry_builder.py +0 -264
  91. notionary/markdown/makdown_document_model.py +0 -0
  92. notionary/markdown/markdown_document_model.py +0 -228
  93. notionary/markdown/markdown_node.py +0 -30
  94. notionary/models/notion_database_response.py +0 -0
  95. notionary/page/writer/markdown_to_notion_formatting_post_processor.py +0 -73
  96. notionary/page/writer/markdown_to_notion_post_processor.py +0 -0
  97. /notionary/{markdown/___init__.py → blocks/markdown/markdown_document_model.py} +0 -0
  98. {notionary-0.2.23.dist-info → notionary-0.2.24.dist-info}/LICENSE +0 -0
  99. {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
- color = (input_match.group("color") or "default").lower()
38
- return CreateTableOfContentsBlock(
39
- table_of_contents=TableOfContentsBlock(color=color)
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
- # Fix: Use 'or' instead of 'and'
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.value
49
-
50
- if color == "default":
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
- """Get system prompt information for table of contents blocks."""
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="Use to automatically generate a clickable table of contents from page headings. Optional color parameter changes the appearance. Default color is gray.",
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 pydantic import BaseModel
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
- def __init__(self, color: Optional[str] = "default"):
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 __future__ import annotations
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
- def __init__(self, text: str, checked: bool = False, marker: str = "-"):
22
- self.text = text
23
- self.checked = checked
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"{self.marker} {checkbox} {self.text}"
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, Field
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 __future__ import annotations
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
- def __init__(self, title: str, children: list[MarkdownNode]):
27
- self.title = title
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 __future__ import annotations
1
+ from pydantic import Field
2
2
 
3
- from pydantic import BaseModel
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
- def __init__(self, text: str, level: int, children: list[MarkdownNode]):
27
- if not (1 <= level <= 3):
28
- raise ValueError("Only heading levels 1-3 are supported (H1, H2, H3)")
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 ExternalFile, FileBlock, FileType
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
- Markdown video syntax:
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
- Supports YouTube, Vimeo, and direct file URLs.
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
- # Flexible pattern that can handle caption in any position
28
- VIDEO_PATTERN = re.compile(r"\[video\]\((https?://[^\s\"]+)\)")
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
- # Use our own regex to find the video URL
43
- video_match = cls.VIDEO_PATTERN.search(text.strip())
44
- if not video_match:
63
+ # Extract the path/URL
64
+ path = cls._extract_video_path(text.strip())
65
+ if not path:
45
66
  return None
46
67
 
47
- url = video_match.group(1)
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
- vid_id = cls._get_youtube_id(url)
50
- if vid_id:
51
- url = f"https://www.youtube.com/watch?v={vid_id}"
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
- # 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 "")
80
+ cls.logger.info(f"Uploading local video file: {path}")
56
81
 
57
- video_block = FileBlock(
58
- type=FileType.EXTERNAL,
59
- external=ExternalFile(url=url),
60
- caption=caption_rich_text,
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
- return CreateVideoBlock(video=video_block)
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
- # URL ermitteln
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
- else:
78
- return None # (file_upload o.ä. hier nicht supported)
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 like YouTube, Vimeo, or direct video files",
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](https://youtu.be/abc123)",
108
- "[video](https://youtube.com/watch?v=xyz)(caption:**Important** tutorial)",
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 direct video file URLs. Supports YouTube, Vimeo, and direct video files. Caption supports rich text formatting and describes the video content.",
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 pydantic import BaseModel
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
- def __init__(self, url: str, caption: Optional[str] = None):
22
- self.url = url
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.
@@ -208,4 +208,4 @@ class CommentClient(BaseNotionClient):
208
208
  content=text,
209
209
  display_name=display_name,
210
210
  attachments=attachments,
211
- )
211
+ )
@@ -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: str = "single_part",
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 ("single_part" or "multi_part")
38
+ mode: Upload mode (UploadMode.SINGLE_PART or UploadMode.MULTI_PART)
38
39
 
39
40
  Returns:
40
41
  FileUploadResponse or None if failed
@@ -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: Literal["single_part", "multi_part"] = "single_part"
56
+ mode: UploadMode = UploadMode.SINGLE_PART
48
57
 
49
58
  def model_dump(self, **kwargs):
50
59
  """Override to exclude None values"""