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
@@ -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 Any, Callable, Optional, Self
11
+ from typing import Callable, Optional, Self
12
12
 
13
- from notionary.blocks.audio import AudioMarkdownBlockParams, AudioMarkdownNode
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
- BulletedListMarkdownBlockParams,
18
- BulletedListMarkdownNode,
19
- )
20
- from notionary.blocks.callout import CalloutMarkdownBlockParams, CalloutMarkdownNode
21
- from notionary.blocks.code import CodeBlock, CodeLanguage, CodeMarkdownNode
22
- from notionary.blocks.column import (
23
- ColumnListMarkdownBlockParams,
24
- ColumnListMarkdownNode,
25
- ColumnMarkdownNode,
26
- )
27
- from notionary.blocks.divider import DividerMarkdownBlockParams, DividerMarkdownNode
28
- from notionary.blocks.embed import EmbedMarkdownBlockParams, EmbedMarkdownNode
29
- from notionary.blocks.equation import EquationMarkdownBlockParams, EquationMarkdownNode
30
- from notionary.blocks.file import FileMarkdownNode, FileMarkdownNodeParams
31
- from notionary.blocks.heading import HeadingMarkdownBlockParams, HeadingMarkdownNode
32
- from notionary.blocks.image_block import ImageMarkdownBlockParams, ImageMarkdownNode
33
- from notionary.blocks.numbered_list import (
34
- NumberedListMarkdownBlockParams,
35
- NumberedListMarkdownNode,
36
- )
37
- from notionary.blocks.paragraph import (
38
- ParagraphMarkdownBlockParams,
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,3 @@
1
+ from .file_upload_mixin import FileUploadMixin
2
+
3
+ __all__ = ["FileUploadMixin"]
@@ -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 __future__ import annotations
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
- def __init__(self, texts: list[str]):
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))