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