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
@@ -8,107 +8,47 @@ Maps 1:1 to the available blocks with clear, expressive method names.
|
|
8
8
|
|
9
9
|
from __future__ import annotations
|
10
10
|
|
11
|
-
from typing import
|
11
|
+
from typing import Callable, Optional, Self
|
12
12
|
|
13
|
-
from notionary.blocks.
|
14
|
-
from notionary.blocks.bookmark import BookmarkMarkdownBlockParams, BookmarkMarkdownNode
|
13
|
+
from notionary.blocks.bookmark import BookmarkMarkdownNode
|
15
14
|
from notionary.blocks.breadcrumbs import BreadcrumbMarkdownNode
|
16
|
-
from notionary.blocks.bulleted_list import
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
from notionary.blocks.
|
21
|
-
from notionary.blocks.
|
22
|
-
from notionary.blocks.
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
from notionary.blocks.
|
28
|
-
from notionary.blocks.
|
29
|
-
from notionary.blocks.
|
30
|
-
from notionary.blocks.
|
31
|
-
from notionary.blocks.
|
32
|
-
from notionary.blocks.
|
33
|
-
from notionary.blocks.
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
ParagraphMarkdownNode,
|
40
|
-
)
|
41
|
-
from notionary.blocks.pdf import PdfMarkdownNode, PdfMarkdownNodeParams
|
42
|
-
from notionary.blocks.quote import QuoteMarkdownBlockParams, QuoteMarkdownNode
|
43
|
-
from notionary.blocks.table import TableMarkdownBlockParams, TableMarkdownNode
|
44
|
-
from notionary.blocks.table_of_contents import (
|
45
|
-
TableOfContentsMarkdownBlockParams,
|
46
|
-
TableOfContentsMarkdownNode,
|
47
|
-
)
|
48
|
-
from notionary.blocks.todo import TodoMarkdownBlockParams, TodoMarkdownNode
|
49
|
-
from notionary.blocks.toggle import ToggleMarkdownBlockParams, ToggleMarkdownNode
|
50
|
-
from notionary.blocks.toggleable_heading import (
|
51
|
-
ToggleableHeadingMarkdownBlockParams,
|
52
|
-
ToggleableHeadingMarkdownNode,
|
53
|
-
)
|
54
|
-
from notionary.blocks.types import BlockType, MarkdownBlockType
|
55
|
-
from notionary.blocks.video import VideoMarkdownBlockParams, VideoMarkdownNode
|
56
|
-
from notionary.markdown.markdown_document_model import (
|
57
|
-
MarkdownBlock,
|
58
|
-
MarkdownDocumentModel,
|
59
|
-
)
|
60
|
-
from notionary.markdown.markdown_node import MarkdownNode
|
15
|
+
from notionary.blocks.bulleted_list import BulletedListMarkdownNode
|
16
|
+
from notionary.blocks.callout import CalloutMarkdownNode
|
17
|
+
from notionary.blocks.code import CodeLanguage, CodeMarkdownNode
|
18
|
+
from notionary.blocks.column import ColumnListMarkdownNode, ColumnMarkdownNode
|
19
|
+
from notionary.blocks.divider import DividerMarkdownNode
|
20
|
+
from notionary.blocks.embed import EmbedMarkdownNode
|
21
|
+
from notionary.blocks.equation import EquationMarkdownNode
|
22
|
+
from notionary.blocks.file import FileMarkdownNode
|
23
|
+
from notionary.blocks.heading import HeadingMarkdownNode
|
24
|
+
from notionary.blocks.image_block import ImageMarkdownNode
|
25
|
+
from notionary.blocks.numbered_list import NumberedListMarkdownNode
|
26
|
+
from notionary.blocks.paragraph import ParagraphMarkdownNode
|
27
|
+
from notionary.blocks.pdf import PdfMarkdownNode
|
28
|
+
from notionary.blocks.quote import QuoteMarkdownNode
|
29
|
+
from notionary.blocks.table import TableMarkdownNode
|
30
|
+
from notionary.blocks.table_of_contents import TableOfContentsMarkdownNode
|
31
|
+
from notionary.blocks.todo import TodoMarkdownNode
|
32
|
+
from notionary.blocks.toggle import ToggleMarkdownNode
|
33
|
+
from notionary.blocks.toggleable_heading import ToggleableHeadingMarkdownNode
|
34
|
+
from notionary.blocks.video import VideoMarkdownNode
|
35
|
+
from notionary.blocks.audio import AudioMarkdownNode
|
36
|
+
|
37
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
61
38
|
|
62
39
|
|
63
40
|
class MarkdownBuilder:
|
64
41
|
"""
|
65
42
|
Fluent interface builder for creating Notion content with clean, direct methods.
|
43
|
+
|
44
|
+
Focuses on the developer API for programmatic content creation.
|
45
|
+
Model processing is handled by MarkdownModelProcessor.
|
66
46
|
"""
|
67
47
|
|
68
48
|
def __init__(self) -> None:
|
49
|
+
"""Initialize builder with empty children list."""
|
69
50
|
self.children: list[MarkdownNode] = []
|
70
51
|
|
71
|
-
self._block_processors: dict[str, Callable[[Any], None]] = {
|
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,
|
103
|
-
}
|
104
|
-
|
105
|
-
@classmethod
|
106
|
-
def from_model(cls, model: MarkdownDocumentModel) -> Self:
|
107
|
-
"""Create MarkdownBuilder from a Pydantic model."""
|
108
|
-
builder = cls()
|
109
|
-
builder._process_blocks(model.blocks)
|
110
|
-
return builder
|
111
|
-
|
112
52
|
def h1(self, text: str) -> Self:
|
113
53
|
"""
|
114
54
|
Add an H1 heading.
|
@@ -583,147 +523,3 @@ class MarkdownBuilder:
|
|
583
523
|
return "\n\n".join(
|
584
524
|
child.to_markdown() for child in self.children if child is not None
|
585
525
|
)
|
586
|
-
|
587
|
-
def _add_heading(self, params: HeadingMarkdownBlockParams) -> None:
|
588
|
-
"""Add a heading block."""
|
589
|
-
self.children.append(HeadingMarkdownNode.from_params(params))
|
590
|
-
|
591
|
-
def _add_paragraph(self, params: ParagraphMarkdownBlockParams) -> None:
|
592
|
-
"""Add a paragraph block."""
|
593
|
-
self.children.append(ParagraphMarkdownNode.from_params(params))
|
594
|
-
|
595
|
-
def _add_quote(self, params: QuoteMarkdownBlockParams) -> None:
|
596
|
-
"""Add a quote block."""
|
597
|
-
self.children.append(QuoteMarkdownNode.from_params(params))
|
598
|
-
|
599
|
-
def _add_bulleted_list(self, params: BulletedListMarkdownBlockParams) -> None:
|
600
|
-
"""Add a bulleted list block."""
|
601
|
-
self.children.append(BulletedListMarkdownNode.from_params(params))
|
602
|
-
|
603
|
-
def _add_numbered_list(self, params: NumberedListMarkdownBlockParams) -> None:
|
604
|
-
"""Add a numbered list block."""
|
605
|
-
self.children.append(NumberedListMarkdownNode.from_params(params))
|
606
|
-
|
607
|
-
def _add_todo(self, params: TodoMarkdownBlockParams) -> None:
|
608
|
-
"""Add a todo block."""
|
609
|
-
self.children.append(TodoMarkdownNode.from_params(params))
|
610
|
-
|
611
|
-
def _add_callout(self, params: CalloutMarkdownBlockParams) -> None:
|
612
|
-
"""Add a callout block."""
|
613
|
-
self.children.append(CalloutMarkdownNode.from_params(params))
|
614
|
-
|
615
|
-
def _add_code(self, params: CodeBlock) -> None:
|
616
|
-
"""Add a code block."""
|
617
|
-
self.children.append(CodeMarkdownNode.from_params(params))
|
618
|
-
|
619
|
-
def _add_image(self, params: ImageMarkdownBlockParams) -> None:
|
620
|
-
"""Add an image block."""
|
621
|
-
self.children.append(ImageMarkdownNode.from_params(params))
|
622
|
-
|
623
|
-
def _add_video(self, params: VideoMarkdownBlockParams) -> None:
|
624
|
-
"""Add a video block."""
|
625
|
-
self.children.append(VideoMarkdownNode.from_params(params))
|
626
|
-
|
627
|
-
def _add_audio(self, params: AudioMarkdownBlockParams) -> None:
|
628
|
-
"""Add an audio block."""
|
629
|
-
self.children.append(AudioMarkdownNode.from_params(params))
|
630
|
-
|
631
|
-
def _add_file(self, params: FileMarkdownNodeParams) -> None:
|
632
|
-
"""Add a file block."""
|
633
|
-
self.children.append(FileMarkdownNode.from_params(params))
|
634
|
-
|
635
|
-
def _add_pdf(self, params: PdfMarkdownNodeParams) -> None:
|
636
|
-
"""Add a PDF block."""
|
637
|
-
self.children.append(PdfMarkdownNode.from_params(params))
|
638
|
-
|
639
|
-
def _add_bookmark(self, params: BookmarkMarkdownBlockParams) -> None:
|
640
|
-
"""Add a bookmark block."""
|
641
|
-
self.children.append(BookmarkMarkdownNode.from_params(params))
|
642
|
-
|
643
|
-
def _add_embed(self, params: EmbedMarkdownBlockParams) -> None:
|
644
|
-
"""Add an embed block."""
|
645
|
-
self.children.append(EmbedMarkdownNode.from_params(params))
|
646
|
-
|
647
|
-
def _add_table(self, params: TableMarkdownBlockParams) -> None:
|
648
|
-
"""Add a table block."""
|
649
|
-
self.children.append(TableMarkdownNode.from_params(params))
|
650
|
-
|
651
|
-
def _add_divider(self, params: DividerMarkdownBlockParams) -> None:
|
652
|
-
"""Add a divider block."""
|
653
|
-
self.children.append(DividerMarkdownNode.from_params(params))
|
654
|
-
|
655
|
-
def _add_equation(self, params: EquationMarkdownBlockParams) -> None:
|
656
|
-
"""Add an equation block."""
|
657
|
-
self.children.append(EquationMarkdownNode.from_params(params))
|
658
|
-
|
659
|
-
def _add_table_of_contents(
|
660
|
-
self, params: TableOfContentsMarkdownBlockParams
|
661
|
-
) -> None:
|
662
|
-
"""Add a table of contents block."""
|
663
|
-
self.children.append(TableOfContentsMarkdownNode.from_params(params))
|
664
|
-
|
665
|
-
def _add_toggle(self, params: ToggleMarkdownBlockParams) -> None:
|
666
|
-
"""Add a toggle block."""
|
667
|
-
child_builder = MarkdownBuilder()
|
668
|
-
child_builder._process_blocks(params.children)
|
669
|
-
self.children.append(
|
670
|
-
ToggleMarkdownNode(title=params.title, children=child_builder.children)
|
671
|
-
)
|
672
|
-
|
673
|
-
def _add_toggleable_heading(
|
674
|
-
self, params: ToggleableHeadingMarkdownBlockParams
|
675
|
-
) -> None:
|
676
|
-
"""Add a toggleable heading block."""
|
677
|
-
# Create nested builder for children
|
678
|
-
child_builder = MarkdownBuilder()
|
679
|
-
child_builder._process_blocks(params.children)
|
680
|
-
self.children.append(
|
681
|
-
ToggleableHeadingMarkdownNode(
|
682
|
-
text=params.text, level=params.level, children=child_builder.children
|
683
|
-
)
|
684
|
-
)
|
685
|
-
|
686
|
-
def _add_columns(self, params: ColumnListMarkdownBlockParams) -> None:
|
687
|
-
"""Add a columns block."""
|
688
|
-
column_nodes = []
|
689
|
-
|
690
|
-
for i, column_blocks in enumerate(params.columns):
|
691
|
-
width_ratio = (
|
692
|
-
params.width_ratios[i]
|
693
|
-
if params.width_ratios and i < len(params.width_ratios)
|
694
|
-
else None
|
695
|
-
)
|
696
|
-
|
697
|
-
col_builder = MarkdownBuilder()
|
698
|
-
col_builder._process_blocks(column_blocks)
|
699
|
-
|
700
|
-
# Erstelle ColumnMarkdownNode
|
701
|
-
column_nodes.append(
|
702
|
-
ColumnMarkdownNode(
|
703
|
-
children=col_builder.children, width_ratio=width_ratio
|
704
|
-
)
|
705
|
-
)
|
706
|
-
|
707
|
-
self.children.append(ColumnListMarkdownNode(columns=column_nodes))
|
708
|
-
|
709
|
-
def _add_breadcrumb(self, params) -> None:
|
710
|
-
"""Add a breadcrumb block."""
|
711
|
-
self.children.append(BreadcrumbMarkdownNode())
|
712
|
-
|
713
|
-
def _add_space(self, params) -> None:
|
714
|
-
"""Add a space block."""
|
715
|
-
self.children.append(ParagraphMarkdownNode(text=""))
|
716
|
-
|
717
|
-
def _process_blocks(self, blocks: list[MarkdownBlock]) -> None:
|
718
|
-
"""Process blocks using explicit mapping - type-safe and clear."""
|
719
|
-
for block in blocks:
|
720
|
-
processor = self._block_processors.get(block.type)
|
721
|
-
if processor:
|
722
|
-
processor(block.params)
|
723
|
-
else:
|
724
|
-
# More explicit error handling
|
725
|
-
available_types = ", ".join(sorted(self._block_processors.keys()))
|
726
|
-
raise ValueError(
|
727
|
-
f"Unsupported block type '{block.type}'. "
|
728
|
-
f"Available types: {available_types}"
|
729
|
-
)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
from pydantic import BaseModel
|
3
|
+
|
4
|
+
|
5
|
+
class MarkdownNode(BaseModel, ABC):
|
6
|
+
"""
|
7
|
+
Enhanced base class for all Markdown nodes with Pydantic integration.
|
8
|
+
|
9
|
+
This class serves dual purposes:
|
10
|
+
1. Runtime representation for markdown generation
|
11
|
+
2. Serializable model for structured output (LLM/API)
|
12
|
+
|
13
|
+
The 'type' field acts as a discriminator for Union types and processing.
|
14
|
+
"""
|
15
|
+
|
16
|
+
@abstractmethod
|
17
|
+
def to_markdown(self) -> str:
|
18
|
+
"""
|
19
|
+
Returns the Markdown representation of the block.
|
20
|
+
Must be implemented by subclasses.
|
21
|
+
"""
|
22
|
+
pass
|
23
|
+
|
24
|
+
def __str__(self):
|
25
|
+
return self.to_markdown()
|
@@ -0,0 +1,320 @@
|
|
1
|
+
from urllib.parse import urlparse
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Optional
|
4
|
+
from notionary.file_upload import NotionFileUploadClient
|
5
|
+
from notionary.file_upload.models import UploadMode
|
6
|
+
from notionary.page.page_context import get_page_context
|
7
|
+
from notionary.util.logging_mixin import LoggingMixin
|
8
|
+
|
9
|
+
|
10
|
+
# TOOD: Hier überlegen wirklich nur was common ist hier in den mixin ansonstne dediziert handeln.
|
11
|
+
class FileUploadMixin(LoggingMixin):
|
12
|
+
"""
|
13
|
+
Mixin to add file upload functionality to all media block elements.
|
14
|
+
|
15
|
+
Supports uploading local files for:
|
16
|
+
- file blocks
|
17
|
+
- image blocks
|
18
|
+
- pdf blocks
|
19
|
+
- audio blocks
|
20
|
+
- video blocks
|
21
|
+
"""
|
22
|
+
|
23
|
+
@classmethod
|
24
|
+
def _get_file_upload_client(cls) -> NotionFileUploadClient:
|
25
|
+
"""Get the file upload client from the current page context."""
|
26
|
+
context = get_page_context()
|
27
|
+
return context.file_upload_client
|
28
|
+
|
29
|
+
@classmethod
|
30
|
+
def _is_local_file_path(cls, path: str) -> bool:
|
31
|
+
"""Determine if the path is a local file rather than a URL."""
|
32
|
+
if path.startswith(("http://", "https://", "ftp://")):
|
33
|
+
return False
|
34
|
+
|
35
|
+
return (
|
36
|
+
"/" in path
|
37
|
+
or "\\" in path
|
38
|
+
or path.startswith("./")
|
39
|
+
or path.startswith("../")
|
40
|
+
or ":" in path[:3]
|
41
|
+
) # Windows drive letters like C:
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def _should_upload_file(cls, path: str, expected_category: str = "file") -> bool:
|
45
|
+
"""
|
46
|
+
Determine if a path should be uploaded vs used as external URL.
|
47
|
+
|
48
|
+
Args:
|
49
|
+
path: File path or URL
|
50
|
+
expected_category: Expected file category
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
True if file should be uploaded
|
54
|
+
"""
|
55
|
+
if not cls._is_local_file_path(path):
|
56
|
+
return False
|
57
|
+
|
58
|
+
file_path = Path(path)
|
59
|
+
if not file_path.exists():
|
60
|
+
return False
|
61
|
+
|
62
|
+
return True
|
63
|
+
|
64
|
+
@classmethod
|
65
|
+
def _get_content_type(cls, file_path: Path) -> str:
|
66
|
+
"""Get MIME type based on file extension."""
|
67
|
+
extension_map = {
|
68
|
+
# Documents
|
69
|
+
".pdf": "application/pdf",
|
70
|
+
".doc": "application/msword",
|
71
|
+
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
72
|
+
".xls": "application/vnd.ms-excel",
|
73
|
+
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
74
|
+
".ppt": "application/vnd.ms-powerpoint",
|
75
|
+
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
76
|
+
".txt": "text/plain",
|
77
|
+
".csv": "text/csv",
|
78
|
+
".json": "application/json",
|
79
|
+
".xml": "application/xml",
|
80
|
+
".zip": "application/zip",
|
81
|
+
".rar": "application/vnd.rar",
|
82
|
+
".7z": "application/x-7z-compressed",
|
83
|
+
# Images
|
84
|
+
".png": "image/png",
|
85
|
+
".jpg": "image/jpeg",
|
86
|
+
".jpeg": "image/jpeg",
|
87
|
+
".gif": "image/gif",
|
88
|
+
".webp": "image/webp",
|
89
|
+
".bmp": "image/bmp",
|
90
|
+
".tiff": "image/tiff",
|
91
|
+
".tif": "image/tiff",
|
92
|
+
".svg": "image/svg+xml",
|
93
|
+
".ico": "image/x-icon",
|
94
|
+
".heic": "image/heic",
|
95
|
+
".heif": "image/heif",
|
96
|
+
# Audio
|
97
|
+
".mp3": "audio/mpeg",
|
98
|
+
".wav": "audio/wav",
|
99
|
+
".ogg": "audio/ogg",
|
100
|
+
".m4a": "audio/mp4",
|
101
|
+
".aac": "audio/aac",
|
102
|
+
".flac": "audio/flac",
|
103
|
+
".wma": "audio/x-ms-wma",
|
104
|
+
".opus": "audio/opus",
|
105
|
+
# Video
|
106
|
+
".mp4": "video/mp4",
|
107
|
+
".avi": "video/x-msvideo",
|
108
|
+
".mov": "video/quicktime",
|
109
|
+
".wmv": "video/x-ms-wmv",
|
110
|
+
".flv": "video/x-flv",
|
111
|
+
".webm": "video/webm",
|
112
|
+
".mkv": "video/x-matroska",
|
113
|
+
".m4v": "video/mp4",
|
114
|
+
".3gp": "video/3gpp",
|
115
|
+
}
|
116
|
+
|
117
|
+
suffix = file_path.suffix.lower()
|
118
|
+
return extension_map.get(suffix, "application/octet-stream")
|
119
|
+
|
120
|
+
@classmethod
|
121
|
+
def _get_file_category(cls, file_path: Path) -> str:
|
122
|
+
"""
|
123
|
+
Determine the category of file based on extension.
|
124
|
+
|
125
|
+
Returns:
|
126
|
+
One of: 'image', 'audio', 'video', 'pdf', 'document', 'archive', 'other'
|
127
|
+
"""
|
128
|
+
suffix = file_path.suffix.lower()
|
129
|
+
|
130
|
+
# Define extension sets for each category
|
131
|
+
extension_categories = {
|
132
|
+
"image": {
|
133
|
+
".png",
|
134
|
+
".jpg",
|
135
|
+
".jpeg",
|
136
|
+
".gif",
|
137
|
+
".webp",
|
138
|
+
".bmp",
|
139
|
+
".tiff",
|
140
|
+
".tif",
|
141
|
+
".svg",
|
142
|
+
".ico",
|
143
|
+
".heic",
|
144
|
+
".heif",
|
145
|
+
},
|
146
|
+
"audio": {".mp3", ".wav", ".ogg", ".m4a", ".aac", ".flac", ".wma", ".opus"},
|
147
|
+
"video": {
|
148
|
+
".mp4",
|
149
|
+
".avi",
|
150
|
+
".mov",
|
151
|
+
".wmv",
|
152
|
+
".flv",
|
153
|
+
".webm",
|
154
|
+
".mkv",
|
155
|
+
".m4v",
|
156
|
+
".3gp",
|
157
|
+
},
|
158
|
+
"pdf": {".pdf"},
|
159
|
+
"document": {
|
160
|
+
".doc",
|
161
|
+
".docx",
|
162
|
+
".xls",
|
163
|
+
".xlsx",
|
164
|
+
".ppt",
|
165
|
+
".pptx",
|
166
|
+
".txt",
|
167
|
+
".csv",
|
168
|
+
".json",
|
169
|
+
".xml",
|
170
|
+
},
|
171
|
+
"archive": {".zip", ".rar", ".7z"},
|
172
|
+
}
|
173
|
+
|
174
|
+
# Find matching category
|
175
|
+
for category, extensions in extension_categories.items():
|
176
|
+
if suffix in extensions:
|
177
|
+
return category
|
178
|
+
|
179
|
+
return "other"
|
180
|
+
|
181
|
+
@classmethod
|
182
|
+
def _is_supported_file_type(cls, file_path: Path, expected_category: str) -> bool:
|
183
|
+
"""
|
184
|
+
Check if the file type matches the expected category.
|
185
|
+
|
186
|
+
Args:
|
187
|
+
file_path: Path to the file
|
188
|
+
expected_category: Expected category ('image', 'audio', 'video', 'pdf', 'file')
|
189
|
+
|
190
|
+
Returns:
|
191
|
+
True if file type matches expected category
|
192
|
+
"""
|
193
|
+
# 'file' category accepts any file type
|
194
|
+
if expected_category == "file":
|
195
|
+
return True
|
196
|
+
|
197
|
+
actual_category = cls._get_file_category(file_path)
|
198
|
+
return actual_category == expected_category
|
199
|
+
|
200
|
+
@classmethod
|
201
|
+
async def _upload_local_file(
|
202
|
+
cls, file_path_str: str, expected_category: str = "file"
|
203
|
+
) -> Optional[str]:
|
204
|
+
"""
|
205
|
+
Upload a local file and return the file upload ID.
|
206
|
+
|
207
|
+
Args:
|
208
|
+
file_path_str: String path to the local file
|
209
|
+
expected_category: Expected file category for validation
|
210
|
+
|
211
|
+
Returns:
|
212
|
+
File upload ID if successful, None otherwise
|
213
|
+
"""
|
214
|
+
try:
|
215
|
+
file_upload_client = cls._get_file_upload_client()
|
216
|
+
file_path = Path(file_path_str)
|
217
|
+
|
218
|
+
# Pre-upload validation
|
219
|
+
if not cls._validate_file_for_upload(file_path, expected_category):
|
220
|
+
return None
|
221
|
+
|
222
|
+
# Get file metadata
|
223
|
+
file_size = file_path.stat().st_size
|
224
|
+
content_type = cls._get_content_type(file_path)
|
225
|
+
|
226
|
+
cls.logger.info(
|
227
|
+
f"Uploading {expected_category} file: {file_path.name} "
|
228
|
+
f"({file_size} bytes, {content_type})"
|
229
|
+
)
|
230
|
+
|
231
|
+
# Create and execute upload
|
232
|
+
upload_id = await cls._execute_upload(
|
233
|
+
file_upload_client, file_path, content_type, file_size
|
234
|
+
)
|
235
|
+
|
236
|
+
if upload_id:
|
237
|
+
cls.logger.info(
|
238
|
+
f"File upload completed: {upload_id} ({file_path.name})"
|
239
|
+
)
|
240
|
+
|
241
|
+
return upload_id
|
242
|
+
|
243
|
+
except Exception as e:
|
244
|
+
cls.logger.error(
|
245
|
+
f"Error uploading {expected_category} file {file_path_str}: {e}"
|
246
|
+
)
|
247
|
+
cls.logger.debug("Upload error traceback:", exc_info=True)
|
248
|
+
return None
|
249
|
+
|
250
|
+
@classmethod
|
251
|
+
def _validate_file_for_upload(cls, file_path: Path, expected_category: str) -> bool:
|
252
|
+
"""Validate file exists and type matches expected category."""
|
253
|
+
# Check if file exists
|
254
|
+
if not file_path.exists():
|
255
|
+
cls.logger.error(f"File not found: {file_path}")
|
256
|
+
return False
|
257
|
+
|
258
|
+
# Validate file type if needed
|
259
|
+
if not cls._is_supported_file_type(file_path, expected_category):
|
260
|
+
actual_category = cls._get_file_category(file_path)
|
261
|
+
cls.logger.warning(
|
262
|
+
f"File type mismatch: expected {expected_category}, "
|
263
|
+
f"got {actual_category} for {file_path} - proceeding anyway"
|
264
|
+
)
|
265
|
+
|
266
|
+
return True
|
267
|
+
|
268
|
+
@classmethod
|
269
|
+
async def _execute_upload(
|
270
|
+
cls,
|
271
|
+
file_upload_client: NotionFileUploadClient,
|
272
|
+
file_path: Path,
|
273
|
+
content_type: str,
|
274
|
+
file_size: int,
|
275
|
+
) -> Optional[str]:
|
276
|
+
"""Execute the actual file upload process."""
|
277
|
+
# Step 1: Create file upload
|
278
|
+
upload_response = await file_upload_client.create_file_upload(
|
279
|
+
filename=file_path.name,
|
280
|
+
content_type=content_type,
|
281
|
+
content_length=file_size,
|
282
|
+
mode=UploadMode.SINGLE_PART,
|
283
|
+
)
|
284
|
+
|
285
|
+
if not upload_response:
|
286
|
+
cls.logger.error(f"Failed to create file upload for {file_path.name}")
|
287
|
+
return None
|
288
|
+
|
289
|
+
cls.logger.debug(f"Created file upload with ID: {upload_response.id}")
|
290
|
+
|
291
|
+
# Step 2: Send file content
|
292
|
+
success = await file_upload_client.send_file_from_path(
|
293
|
+
file_upload_id=upload_response.id, file_path=file_path
|
294
|
+
)
|
295
|
+
|
296
|
+
if not success:
|
297
|
+
cls.logger.error(f"Failed to send file content for {file_path.name}")
|
298
|
+
return None
|
299
|
+
|
300
|
+
cls.logger.debug(f"File content sent successfully for {file_path.name}")
|
301
|
+
return upload_response.id
|
302
|
+
|
303
|
+
@classmethod
|
304
|
+
def _get_upload_error_message(
|
305
|
+
cls, file_path_str: str, expected_category: str
|
306
|
+
) -> str:
|
307
|
+
"""Get a user-friendly error message for upload failures."""
|
308
|
+
file_path = Path(file_path_str)
|
309
|
+
|
310
|
+
if not file_path.exists():
|
311
|
+
return f"File not found: {file_path_str}"
|
312
|
+
|
313
|
+
actual_category = cls._get_file_category(file_path)
|
314
|
+
if actual_category != expected_category and expected_category != "file":
|
315
|
+
return (
|
316
|
+
f"Invalid file type for {expected_category} block: "
|
317
|
+
f"{file_path.suffix} (detected as {actual_category})"
|
318
|
+
)
|
319
|
+
|
320
|
+
return f"Failed to upload {expected_category} file: {file_path_str}"
|
@@ -1,6 +1,5 @@
|
|
1
1
|
from notionary.blocks.numbered_list.numbered_list_element import NumberedListElement
|
2
2
|
from notionary.blocks.numbered_list.numbered_list_markdown_node import (
|
3
|
-
NumberedListMarkdownBlockParams,
|
4
3
|
NumberedListMarkdownNode,
|
5
4
|
)
|
6
5
|
from notionary.blocks.numbered_list.numbered_list_models import (
|
@@ -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 NumberedListMarkdownBlockParams(BaseModel):
|
9
|
-
texts: list[str]
|
1
|
+
from notionary.blocks.markdown.markdown_node import MarkdownNode
|
10
2
|
|
11
3
|
|
12
4
|
class NumberedListMarkdownNode(MarkdownNode):
|
13
5
|
"""
|
6
|
+
Enhanced NumberedList node with Pydantic integration.
|
14
7
|
Programmatic interface for creating Markdown numbered list items.
|
15
8
|
Example:
|
16
9
|
1. First step
|
@@ -18,14 +11,7 @@ class NumberedListMarkdownNode(MarkdownNode):
|
|
18
11
|
3. Third step
|
19
12
|
"""
|
20
13
|
|
21
|
-
|
22
|
-
self.texts = texts
|
23
|
-
|
24
|
-
@classmethod
|
25
|
-
def from_params(
|
26
|
-
cls, params: NumberedListMarkdownBlockParams
|
27
|
-
) -> NumberedListMarkdownNode:
|
28
|
-
return cls(texts=params.texts)
|
14
|
+
texts: list[str]
|
29
15
|
|
30
16
|
def to_markdown(self) -> str:
|
31
17
|
return "\n".join(f"{i + 1}. {text}" for i, text in enumerate(self.texts))
|