notionary 0.2.17__tar.gz → 0.2.19__tar.gz

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 (161) hide show
  1. {notionary-0.2.17 → notionary-0.2.19}/PKG-INFO +1 -1
  2. {notionary-0.2.17 → notionary-0.2.19}/notionary/__init__.py +3 -2
  3. notionary-0.2.19/notionary/blocks/__init__.py +92 -0
  4. notionary-0.2.19/notionary/blocks/audio/__init__.py +7 -0
  5. notionary-0.2.19/notionary/blocks/audio/audio_element.py +152 -0
  6. notionary-0.2.19/notionary/blocks/audio/audio_markdown_node.py +29 -0
  7. notionary-0.2.19/notionary/blocks/audio/audio_models.py +59 -0
  8. notionary-0.2.19/notionary/blocks/bookmark/__init__.py +7 -0
  9. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/bookmark}/bookmark_element.py +20 -65
  10. notionary-0.2.19/notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
  11. notionary-0.2.19/notionary/blocks/bulleted_list/__init__.py +7 -0
  12. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/bulleted_list}/bulleted_list_element.py +7 -3
  13. notionary-0.2.19/notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
  14. notionary-0.2.19/notionary/blocks/callout/__init__.py +7 -0
  15. notionary-0.2.19/notionary/blocks/callout/callout_element.py +132 -0
  16. notionary-0.2.19/notionary/blocks/callout/callout_markdown_node.py +31 -0
  17. notionary-0.2.19/notionary/blocks/code/__init__.py +7 -0
  18. notionary-0.2.17/notionary/blocks/code_block_element.py → notionary-0.2.19/notionary/blocks/code/code_element.py +72 -40
  19. notionary-0.2.19/notionary/blocks/code/code_markdown_node.py +43 -0
  20. notionary-0.2.19/notionary/blocks/column/__init__.py +5 -0
  21. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/column}/column_element.py +24 -55
  22. notionary-0.2.19/notionary/blocks/divider/__init__.py +7 -0
  23. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/divider}/divider_element.py +11 -3
  24. notionary-0.2.19/notionary/blocks/divider/divider_markdown_node.py +24 -0
  25. notionary-0.2.19/notionary/blocks/divider/divider_models.py +0 -0
  26. notionary-0.2.19/notionary/blocks/document/__init__.py +7 -0
  27. notionary-0.2.19/notionary/blocks/document/document_element.py +102 -0
  28. notionary-0.2.19/notionary/blocks/document/document_markdown_node.py +31 -0
  29. notionary-0.2.19/notionary/blocks/document/document_models.py +0 -0
  30. notionary-0.2.19/notionary/blocks/embed/__init__.py +7 -0
  31. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/embed}/embed_element.py +50 -32
  32. notionary-0.2.19/notionary/blocks/embed/embed_markdown_node.py +30 -0
  33. notionary-0.2.19/notionary/blocks/embed/embed_models.py +0 -0
  34. notionary-0.2.19/notionary/blocks/heading/__init__.py +7 -0
  35. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/heading}/heading_element.py +25 -17
  36. notionary-0.2.19/notionary/blocks/heading/heading_markdown_node.py +29 -0
  37. notionary-0.2.19/notionary/blocks/heading/heading_models.py +0 -0
  38. notionary-0.2.19/notionary/blocks/image/__init__.py +7 -0
  39. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/image}/image_element.py +62 -42
  40. notionary-0.2.19/notionary/blocks/image/image_markdown_node.py +33 -0
  41. notionary-0.2.19/notionary/blocks/image/image_models.py +0 -0
  42. notionary-0.2.19/notionary/blocks/markdown_builder.py +356 -0
  43. notionary-0.2.19/notionary/blocks/markdown_node.py +29 -0
  44. notionary-0.2.19/notionary/blocks/mention/__init__.py +7 -0
  45. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/mention}/mention_element.py +6 -2
  46. notionary-0.2.19/notionary/blocks/mention/mention_markdown_node.py +38 -0
  47. notionary-0.2.19/notionary/blocks/mention/mention_models.py +0 -0
  48. notionary-0.2.19/notionary/blocks/numbered_list/__init__.py +7 -0
  49. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/numbered_list}/numbered_list_element.py +10 -6
  50. notionary-0.2.19/notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
  51. notionary-0.2.19/notionary/blocks/numbered_list/numbered_list_models.py +0 -0
  52. notionary-0.2.19/notionary/blocks/paragraph/__init__.py +7 -0
  53. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/paragraph}/paragraph_element.py +7 -3
  54. notionary-0.2.19/notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
  55. notionary-0.2.19/notionary/blocks/paragraph/paragraph_models.py +0 -0
  56. notionary-0.2.19/notionary/blocks/quote/__init__.py +7 -0
  57. notionary-0.2.19/notionary/blocks/quote/quote_element.py +92 -0
  58. notionary-0.2.19/notionary/blocks/quote/quote_markdown_node.py +23 -0
  59. notionary-0.2.19/notionary/blocks/quote/quote_models.py +0 -0
  60. {notionary-0.2.17 → notionary-0.2.19}/notionary/blocks/registry/block_registry.py +17 -3
  61. notionary-0.2.19/notionary/blocks/registry/block_registry_builder.py +208 -0
  62. notionary-0.2.19/notionary/blocks/shared/__init__.py +0 -0
  63. notionary-0.2.19/notionary/blocks/shared/block_client.py +256 -0
  64. notionary-0.2.19/notionary/blocks/shared/models.py +713 -0
  65. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/shared}/notion_block_element.py +8 -5
  66. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/shared}/text_inline_formatter.py +14 -14
  67. notionary-0.2.19/notionary/blocks/shared/text_inline_formatter_new.py +139 -0
  68. notionary-0.2.19/notionary/blocks/table/__init__.py +7 -0
  69. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/table}/table_element.py +23 -11
  70. notionary-0.2.19/notionary/blocks/table/table_markdown_node.py +40 -0
  71. notionary-0.2.19/notionary/blocks/table/table_models.py +0 -0
  72. notionary-0.2.19/notionary/blocks/todo/__init__.py +7 -0
  73. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/todo}/todo_element.py +8 -4
  74. notionary-0.2.19/notionary/blocks/todo/todo_markdown_node.py +31 -0
  75. notionary-0.2.19/notionary/blocks/todo/todo_models.py +0 -0
  76. notionary-0.2.19/notionary/blocks/toggle/__init__.py +4 -0
  77. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/toggle}/toggle_element.py +7 -3
  78. notionary-0.2.19/notionary/blocks/toggle/toggle_markdown_node.py +35 -0
  79. notionary-0.2.19/notionary/blocks/toggle/toggle_models.py +0 -0
  80. notionary-0.2.19/notionary/blocks/toggleable_heading/__init__.py +9 -0
  81. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/toggleable_heading}/toggleable_heading_element.py +8 -4
  82. notionary-0.2.19/notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
  83. notionary-0.2.19/notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
  84. notionary-0.2.19/notionary/blocks/video/__init__.py +7 -0
  85. {notionary-0.2.17/notionary/blocks → notionary-0.2.19/notionary/blocks/video}/video_element.py +82 -57
  86. notionary-0.2.19/notionary/blocks/video/video_markdown_node.py +30 -0
  87. notionary-0.2.19/notionary/database/factory.py +0 -0
  88. notionary-0.2.19/notionary/elements/__init__.py +0 -0
  89. {notionary-0.2.17 → notionary-0.2.19}/notionary/file_upload/notion_file_upload.py +1 -1
  90. notionary-0.2.19/notionary/models/search_response.py +0 -0
  91. notionary-0.2.19/notionary/page/__init__.py +0 -0
  92. notionary-0.2.19/notionary/page/content/markdown_whitespace_processor.py +80 -0
  93. notionary-0.2.19/notionary/page/content/notion_text_length_utils.py +87 -0
  94. {notionary-0.2.17 → notionary-0.2.19}/notionary/page/content/page_content_retriever.py +18 -10
  95. notionary-0.2.19/notionary/page/content/page_content_writer.py +151 -0
  96. notionary-0.2.19/notionary/page/formatting/line_processor.py +153 -0
  97. notionary-0.2.19/notionary/page/formatting/markdown_to_notion_converter.py +153 -0
  98. {notionary-0.2.17 → notionary-0.2.19}/notionary/page/notion_page.py +9 -11
  99. {notionary-0.2.17 → notionary-0.2.19}/notionary/page/notion_to_markdown_converter.py +9 -13
  100. notionary-0.2.19/notionary/page/properites/property_value_extractor.py +0 -0
  101. notionary-0.2.19/notionary/util/factory_decorator.py +0 -0
  102. {notionary-0.2.17 → notionary-0.2.19}/notionary/workspace.py +0 -1
  103. {notionary-0.2.17 → notionary-0.2.19}/pyproject.toml +2 -1
  104. notionary-0.2.17/notionary/blocks/__init__.py +0 -63
  105. notionary-0.2.17/notionary/blocks/audio_element.py +0 -144
  106. notionary-0.2.17/notionary/blocks/callout_element.py +0 -122
  107. notionary-0.2.17/notionary/blocks/document_element.py +0 -194
  108. notionary-0.2.17/notionary/blocks/notion_block_client.py +0 -26
  109. notionary-0.2.17/notionary/blocks/qoute_element.py +0 -169
  110. notionary-0.2.17/notionary/blocks/registry/block_registry_builder.py +0 -296
  111. notionary-0.2.17/notionary/page/content/notion_page_content_chunker.py +0 -84
  112. notionary-0.2.17/notionary/page/content/page_content_writer.py +0 -202
  113. notionary-0.2.17/notionary/page/formatting/markdown_to_notion_converter.py +0 -474
  114. notionary-0.2.17/notionary/page/formatting/spacer_rules.py +0 -483
  115. {notionary-0.2.17 → notionary-0.2.19}/LICENSE +0 -0
  116. {notionary-0.2.17 → notionary-0.2.19}/README.md +0 -0
  117. {notionary-0.2.17 → notionary-0.2.19}/notionary/base_notion_client.py +0 -0
  118. /notionary-0.2.17/notionary/database/factory.py → /notionary-0.2.19/notionary/blocks/bookmark/bookmark_models.py +0 -0
  119. /notionary-0.2.17/notionary/elements/__init__.py → /notionary-0.2.19/notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
  120. /notionary-0.2.17/notionary/models/search_response.py → /notionary-0.2.19/notionary/blocks/callout/callout_models.py +0 -0
  121. /notionary-0.2.17/notionary/page/__init__.py → /notionary-0.2.19/notionary/blocks/code/code_models.py +0 -0
  122. /notionary-0.2.17/notionary/page/properites/property_value_extractor.py → /notionary-0.2.19/notionary/blocks/column/column_models.py +0 -0
  123. {notionary-0.2.17 → notionary-0.2.19}/notionary/blocks/prompts/element_prompt_builder.py +0 -0
  124. {notionary-0.2.17 → notionary-0.2.19}/notionary/blocks/prompts/element_prompt_content.py +0 -0
  125. {notionary-0.2.17 → notionary-0.2.19}/notionary/database/__init__.py +0 -0
  126. {notionary-0.2.17 → notionary-0.2.19}/notionary/database/client.py +0 -0
  127. {notionary-0.2.17 → notionary-0.2.19}/notionary/database/database.py +0 -0
  128. {notionary-0.2.17 → notionary-0.2.19}/notionary/database/database_filter_builder.py +0 -0
  129. {notionary-0.2.17 → notionary-0.2.19}/notionary/database/database_provider.py +0 -0
  130. {notionary-0.2.17 → notionary-0.2.19}/notionary/database/exceptions.py +0 -0
  131. {notionary-0.2.17 → notionary-0.2.19}/notionary/database/models/page_result.py +0 -0
  132. {notionary-0.2.17 → notionary-0.2.19}/notionary/database/notion_database.py +0 -0
  133. {notionary-0.2.17 → notionary-0.2.19}/notionary/file_upload/__init__.py +0 -0
  134. {notionary-0.2.17 → notionary-0.2.19}/notionary/file_upload/client.py +0 -0
  135. {notionary-0.2.17 → notionary-0.2.19}/notionary/file_upload/models.py +0 -0
  136. {notionary-0.2.17 → notionary-0.2.19}/notionary/models/notion_block_response.py +0 -0
  137. {notionary-0.2.17 → notionary-0.2.19}/notionary/models/notion_database_response.py +0 -0
  138. {notionary-0.2.17 → notionary-0.2.19}/notionary/models/notion_page_response.py +0 -0
  139. {notionary-0.2.17 → notionary-0.2.19}/notionary/page/client.py +0 -0
  140. {notionary-0.2.17 → notionary-0.2.19}/notionary/page/markdown_syntax_prompt_generator.py +0 -0
  141. {notionary-0.2.17 → notionary-0.2.19}/notionary/page/property_formatter.py +0 -0
  142. {notionary-0.2.17 → notionary-0.2.19}/notionary/page/search_filter_builder.py +0 -0
  143. {notionary-0.2.17 → notionary-0.2.19}/notionary/page/utils.py +0 -0
  144. {notionary-0.2.17 → notionary-0.2.19}/notionary/telemetry/__init__.py +0 -0
  145. {notionary-0.2.17 → notionary-0.2.19}/notionary/telemetry/service.py +0 -0
  146. {notionary-0.2.17 → notionary-0.2.19}/notionary/telemetry/views.py +0 -0
  147. {notionary-0.2.17 → notionary-0.2.19}/notionary/user/__init__.py +0 -0
  148. {notionary-0.2.17 → notionary-0.2.19}/notionary/user/base_notion_user.py +0 -0
  149. {notionary-0.2.17 → notionary-0.2.19}/notionary/user/client.py +0 -0
  150. {notionary-0.2.17 → notionary-0.2.19}/notionary/user/models.py +0 -0
  151. {notionary-0.2.17 → notionary-0.2.19}/notionary/user/notion_bot_user.py +0 -0
  152. {notionary-0.2.17 → notionary-0.2.19}/notionary/user/notion_user.py +0 -0
  153. {notionary-0.2.17 → notionary-0.2.19}/notionary/user/notion_user_manager.py +0 -0
  154. {notionary-0.2.17 → notionary-0.2.19}/notionary/user/notion_user_provider.py +0 -0
  155. {notionary-0.2.17 → notionary-0.2.19}/notionary/util/__init__.py +0 -0
  156. {notionary-0.2.17 → notionary-0.2.19}/notionary/util/factory_only.py +0 -0
  157. {notionary-0.2.17 → notionary-0.2.19}/notionary/util/fuzzy.py +0 -0
  158. {notionary-0.2.17 → notionary-0.2.19}/notionary/util/logging_mixin.py +0 -0
  159. {notionary-0.2.17 → notionary-0.2.19}/notionary/util/page_id_utils.py +0 -0
  160. {notionary-0.2.17 → notionary-0.2.19}/notionary/util/singleton.py +0 -0
  161. {notionary-0.2.17 → notionary-0.2.19}/notionary/util/singleton_metaclass.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: notionary
3
- Version: 0.2.17
3
+ Version: 0.2.19
4
4
  Summary: Python library for programmatic Notion workspace management - databases, pages, and content with advanced Markdown support
5
5
  License: MIT
6
6
  Author: Mathis Arends
@@ -2,7 +2,8 @@ from .database import NotionDatabase, DatabaseFilterBuilder
2
2
  from .page.notion_page import NotionPage
3
3
  from .workspace import NotionWorkspace
4
4
  from .user import NotionUser, NotionUserManager, NotionBotUser
5
- from .file_upload import NotionFileUpload, NotionFileUploadClient
5
+ from .file_upload import NotionFileUpload
6
+ from .blocks.markdown_builder import MarkdownBuilder
6
7
 
7
8
  __all__ = [
8
9
  "NotionDatabase",
@@ -13,5 +14,5 @@ __all__ = [
13
14
  "NotionUserManager",
14
15
  "NotionBotUser",
15
16
  "NotionFileUpload",
16
- "NotionFileUploadClient",
17
+ "MarkdownBuilder",
17
18
  ]
@@ -0,0 +1,92 @@
1
+ # Order is important here, as some imports depend on others.
2
+ from .prompts.element_prompt_content import ElementPromptContent
3
+ from .prompts.element_prompt_builder import ElementPromptBuilder
4
+
5
+ from .shared.notion_block_element import (
6
+ NotionBlockElement,
7
+ NotionBlockResult,
8
+ NotionBlock,
9
+ )
10
+
11
+ from .audio import AudioElement, AudioMarkdownNode
12
+ from .bulleted_list import BulletedListElement, BulletedListMarkdownNode
13
+ from .callout import CalloutElement, CalloutMarkdownNode
14
+ from .code import CodeElement, CodeMarkdownNode
15
+ from .column.column_element import ColumnElement
16
+ from .divider import DividerElement, DividerMarkdownNode
17
+ from .embed import EmbedElement, EmbedMarkdownNode
18
+ from .heading import HeadingElement, HeadingMarkdownNode
19
+ from .image import ImageElement, ImageMarkdownNode
20
+ from .numbered_list import NumberedListElement, NumberedListMarkdownNode
21
+ from .paragraph import ParagraphElement, ParagraphMarkdownNode
22
+ from .table import TableElement, TableMarkdownNode
23
+ from .toggle import ToggleElement, ToggleMarkdownNode
24
+ from .todo import TodoElement, TodoMarkdownNode
25
+ from .video import VideoElement, VideoMarkdownNode
26
+ from .toggleable_heading import ToggleableHeadingElement, ToggleableHeadingMarkdownNode
27
+ from .bookmark import BookmarkElement, BookmarkMarkdownNode
28
+ from .divider import DividerElement, DividerMarkdownNode
29
+ from .heading import HeadingElement, HeadingMarkdownNode
30
+ from .mention import MentionElement, MentionMarkdownNode
31
+ from .quote import QuoteElement, QuoteMarkdownNode
32
+ from .document import DocumentElement, DocumentMarkdownNode
33
+ from .shared.text_inline_formatter import TextInlineFormatter
34
+
35
+ from .markdown_node import MarkdownNode
36
+
37
+ from .registry.block_registry import BlockRegistry
38
+ from .registry.block_registry_builder import BlockRegistryBuilder
39
+
40
+ from .shared.block_client import NotionBlockClient
41
+
42
+ __all__ = [
43
+ "MarkdownNode",
44
+ "ElementPromptContent",
45
+ "ElementPromptBuilder",
46
+ "NotionBlockElement",
47
+ "AudioElement",
48
+ "AudioMarkdownNode",
49
+ "BulletedListElement",
50
+ "BulletedListMarkdownNode",
51
+ "CalloutElement",
52
+ "CalloutMarkdownNode",
53
+ "CodeElement",
54
+ "CodeMarkdownNode",
55
+ "ColumnElement",
56
+ "DividerElement",
57
+ "DividerMarkdownNode",
58
+ "EmbedElement",
59
+ "EmbedMarkdownNode",
60
+ "HeadingElement",
61
+ "HeadingMarkdownNode",
62
+ "ImageElement",
63
+ "ImageMarkdownNode",
64
+ "NumberedListElement",
65
+ "NumberedListMarkdownNode",
66
+ "ParagraphElement",
67
+ "ParagraphMarkdownNode",
68
+ "TableElement",
69
+ "TableMarkdownNode",
70
+ "ToggleElement",
71
+ "ToggleMarkdownNode",
72
+ "TodoElement",
73
+ "TodoMarkdownNode",
74
+ "VideoElement",
75
+ "VideoMarkdownNode",
76
+ "ToggleableHeadingElement",
77
+ "ToggleableHeadingMarkdownNode",
78
+ "BookmarkElement",
79
+ "BookmarkMarkdownNode",
80
+ "MentionElement",
81
+ "MentionMarkdownNode",
82
+ "QuoteElement",
83
+ "QuoteMarkdownNode",
84
+ "DocumentElement",
85
+ "DocumentMarkdownNode",
86
+ "BlockRegistry",
87
+ "BlockRegistryBuilder",
88
+ "TextInlineFormatter",
89
+ "NotionBlockResult",
90
+ "NotionBlock",
91
+ "NotionBlockClient",
92
+ ]
@@ -0,0 +1,7 @@
1
+ from .audio_element import AudioElement
2
+ from .audio_markdown_node import AudioMarkdownNode
3
+
4
+ __all__ = [
5
+ "AudioElement",
6
+ "AudioMarkdownNode",
7
+ ]
@@ -0,0 +1,152 @@
1
+ import re
2
+ from typing import Any, Optional, List
3
+
4
+ from notionary.blocks import (
5
+ NotionBlockElement,
6
+ ElementPromptContent,
7
+ ElementPromptBuilder,
8
+ NotionBlockResult,
9
+ )
10
+ from notionary.blocks.shared.models import RichTextObject
11
+
12
+
13
+ class AudioElement(NotionBlockElement):
14
+ """
15
+ Handles conversion between Markdown audio embeds and Notion audio blocks.
16
+
17
+ Markdown audio syntax:
18
+ - [audio](https://example.com/audio.mp3) - Simple audio embed
19
+ - [audio](https://example.com/audio.mp3 "Caption text") - Audio with caption
20
+
21
+ Where:
22
+ - URL is the required audio file URL
23
+ - Caption is optional descriptive text (enclosed in quotes)
24
+ """
25
+
26
+ # Regex patterns
27
+ URL_PATTERN = r"(https?://[^\s\"]+)"
28
+ CAPTION_PATTERN = r'(?:\s+"([^"]+)")?'
29
+
30
+ PATTERN = re.compile(r"^\[audio\]\(" + URL_PATTERN + CAPTION_PATTERN + r"\)$")
31
+
32
+ # Supported audio extensions
33
+ SUPPORTED_EXTENSIONS = {".mp3", ".wav", ".ogg", ".oga", ".m4a"}
34
+
35
+ @classmethod
36
+ def match_markdown(cls, text: str) -> bool:
37
+ m = cls.PATTERN.match(text.strip())
38
+ if not m:
39
+ return False
40
+ url = m.group(1)
41
+ return cls._is_likely_audio_url(url)
42
+
43
+ @classmethod
44
+ def match_notion(cls, block: dict[str, Any]) -> bool:
45
+ """Check if block is a Notion audio block."""
46
+ return block.get("type") == "audio"
47
+
48
+ @classmethod
49
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
50
+ """Convert markdown audio embed to Notion audio block."""
51
+ audio_match = cls.PATTERN.match(text.strip())
52
+ if not audio_match:
53
+ return None
54
+
55
+ url = audio_match.group(1)
56
+ caption_text = audio_match.group(2)
57
+
58
+ if not url:
59
+ return None
60
+
61
+ # Validate URL if possible
62
+ if not cls._is_likely_audio_url(url):
63
+ # Still proceed - user might know better
64
+ pass
65
+
66
+ audio_data = {"type": "external", "external": {"url": url}}
67
+
68
+ # Add caption if provided
69
+ if caption_text:
70
+ caption_rich_text = RichTextObject.from_plain_text(caption_text)
71
+ audio_data["caption"] = [caption_rich_text.model_dump()]
72
+ else:
73
+ audio_data["caption"] = []
74
+
75
+ return {"type": "audio", "audio": audio_data}
76
+
77
+ @classmethod
78
+ def notion_to_markdown(cls, block: dict[str, Any]) -> Optional[str]:
79
+ """Convert Notion audio block to markdown audio embed."""
80
+ if block.get("type") != "audio":
81
+ return None
82
+
83
+ audio_data = block.get("audio", {})
84
+
85
+ # Get URL from external source
86
+ if audio_data.get("type") == "external":
87
+ url = audio_data.get("external", {}).get("url", "")
88
+ else:
89
+ # Handle file or file_upload types if needed
90
+ return None
91
+
92
+ if not url:
93
+ return None
94
+
95
+ # Extract caption
96
+ caption = audio_data.get("caption", [])
97
+ if caption:
98
+ caption_text = cls._extract_text_content(caption)
99
+ return f'[audio]({url} "{caption_text}")'
100
+
101
+ return f"[audio]({url})"
102
+
103
+ @classmethod
104
+ def is_multiline(cls) -> bool:
105
+ """Audio embeds are single-line elements."""
106
+ return False
107
+
108
+ @classmethod
109
+ def _is_likely_audio_url(cls, url: str) -> bool:
110
+ """Check if URL likely points to an audio file."""
111
+ return any(url.lower().endswith(ext) for ext in cls.SUPPORTED_EXTENSIONS)
112
+
113
+ @classmethod
114
+ def _extract_text_content(cls, rich_text: List[dict[str, Any]]) -> str:
115
+ """Extract plain text content from Notion rich_text elements."""
116
+ result = ""
117
+ for text_obj in rich_text:
118
+ if text_obj.get("type") == "text":
119
+ result += text_obj.get("text", {}).get("content", "")
120
+ elif "plain_text" in text_obj:
121
+ result += text_obj.get("plain_text", "")
122
+ return result
123
+
124
+ @classmethod
125
+ def get_llm_prompt_content(cls) -> ElementPromptContent:
126
+ """
127
+ Returns structured LLM prompt metadata for the audio element.
128
+ """
129
+ return (
130
+ ElementPromptBuilder()
131
+ .with_description(
132
+ "Embeds an audio file that can be played directly in the page."
133
+ )
134
+ .with_usage_guidelines(
135
+ "Use audio embeds when you want to include sound files, music, podcasts, "
136
+ "or voice recordings. Supports common audio formats like MP3, WAV, OGG, and M4A."
137
+ )
138
+ .with_syntax('![audio](https://example.com/audio.mp3 "Optional caption")')
139
+ .with_examples(
140
+ [
141
+ "[audio](https://example.com/song.mp3)",
142
+ '[audio](https://example.com/podcast.mp3 "Episode 1: Introduction")',
143
+ '[audio](https://example.com/sound.wav "Sound effect for presentation")',
144
+ '[audio](https://example.com/recording.m4a "Voice memo from meeting")',
145
+ ]
146
+ )
147
+ .with_avoidance_guidelines(
148
+ "Ensure the URL points to a valid audio file. "
149
+ "Some audio formats may not be supported by all browsers."
150
+ )
151
+ .build()
152
+ )
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.blocks.markdown_node import MarkdownNode
6
+
7
+
8
+ class AudioMarkdownBlockParams(BaseModel):
9
+ url: str
10
+ caption: Optional[str] = None
11
+
12
+
13
+ class AudioMarkdownNode(MarkdownNode):
14
+ """
15
+ Programmatic interface for creating Notion-style audio blocks.
16
+ """
17
+
18
+ def __init__(self, url: str, caption: Optional[str] = None):
19
+ self.url = url
20
+ self.caption = caption
21
+
22
+ @classmethod
23
+ def from_params(cls, params: AudioMarkdownBlockParams) -> AudioMarkdownNode:
24
+ return cls(url=params.url, caption=params.caption)
25
+
26
+ def to_markdown(self) -> str:
27
+ if self.caption:
28
+ return f'[audio]({self.url} "{self.caption}")'
29
+ return f"[audio]({self.url})"
@@ -0,0 +1,59 @@
1
+ from typing import Optional
2
+ from pydantic import BaseModel
3
+
4
+ from notionary.blocks.shared.models import RichTextObject
5
+
6
+
7
+ # TODO: Diesen Kram hier auch verwenden
8
+ class ExternalAudioSource(BaseModel):
9
+ """External audio source."""
10
+
11
+ url: str
12
+
13
+
14
+ class NotionAudioData(BaseModel):
15
+ """Audio block data."""
16
+
17
+ type: str = "external"
18
+ external: ExternalAudioSource
19
+ caption: list[dict] = []
20
+
21
+
22
+ class NotionAudioBlock(BaseModel):
23
+ """Audio block result."""
24
+
25
+ type: str = "audio"
26
+ audio: NotionAudioData
27
+
28
+
29
+ # Updated method with typed return
30
+ @classmethod
31
+ def markdown_to_notion(cls, text: str) -> Optional[NotionAudioBlock]:
32
+ """Convert markdown audio embed to Notion audio block."""
33
+ audio_match = cls.PATTERN.match(text.strip())
34
+ if not audio_match:
35
+ return None
36
+
37
+ url = audio_match.group(1)
38
+ caption_text = audio_match.group(2)
39
+
40
+ if not url:
41
+ return None
42
+
43
+ # Validate URL if possible
44
+ if not cls._is_likely_audio_url(url):
45
+ # Still proceed - user might know better
46
+ pass
47
+
48
+ # Build caption list
49
+ caption_list = []
50
+ if caption_text:
51
+ caption_rich_text = RichTextObject.from_plain_text(caption_text)
52
+ caption_list = [caption_rich_text.model_dump()]
53
+
54
+ # Create typed result
55
+ return NotionAudioBlock(
56
+ audio=NotionAudioData(
57
+ external=ExternalAudioSource(url=url), caption=caption_list
58
+ )
59
+ )
@@ -0,0 +1,7 @@
1
+ from .bookmark_element import BookmarkElement
2
+ from .bookmark_markdown_node import BookmarkMarkdownNode
3
+
4
+ __all__ = [
5
+ "BookmarkElement",
6
+ "BookmarkMarkdownNode",
7
+ ]
@@ -2,7 +2,12 @@ import re
2
2
  from typing import Dict, Any, Optional, List, Tuple
3
3
 
4
4
  from notionary.blocks import NotionBlockElement
5
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
5
+ from notionary.blocks import (
6
+ ElementPromptContent,
7
+ ElementPromptBuilder,
8
+ NotionBlockResult,
9
+ )
10
+ from notionary.blocks.shared.models import RichTextObject
6
11
 
7
12
 
8
13
  class BookmarkElement(NotionBlockElement):
@@ -33,7 +38,7 @@ class BookmarkElement(NotionBlockElement):
33
38
  def match_markdown(cls, text: str) -> bool:
34
39
  """Check if text is a markdown bookmark."""
35
40
  return text.strip().startswith("[bookmark]") and bool(
36
- BookmarkElement.PATTERN.match(text.strip())
41
+ cls.PATTERN.match(text.strip())
37
42
  )
38
43
 
39
44
  @classmethod
@@ -42,7 +47,7 @@ class BookmarkElement(NotionBlockElement):
42
47
  return block.get("type") in ["bookmark", "external-bookmark"]
43
48
 
44
49
  @classmethod
45
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
50
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
46
51
  """Convert markdown bookmark to Notion bookmark block."""
47
52
  bookmark_match = BookmarkElement.PATTERN.match(text.strip())
48
53
  if not bookmark_match:
@@ -54,71 +59,21 @@ class BookmarkElement(NotionBlockElement):
54
59
 
55
60
  bookmark_data = {"url": url}
56
61
 
57
- # Add caption if title or description is provided
58
- if title or description:
59
- caption = []
60
-
61
- if title:
62
- caption.append(
63
- {
64
- "type": "text",
65
- "text": {"content": title, "link": None},
66
- "annotations": {
67
- "bold": False,
68
- "italic": False,
69
- "strikethrough": False,
70
- "underline": False,
71
- "code": False,
72
- "color": "default",
73
- },
74
- "plain_text": title,
75
- "href": None,
76
- }
77
- )
78
-
79
- # Add a separator if both title and description are provided
80
- if description:
81
- caption.append(
82
- {
83
- "type": "text",
84
- "text": {"content": " - ", "link": None},
85
- "annotations": {
86
- "bold": False,
87
- "italic": False,
88
- "strikethrough": False,
89
- "underline": False,
90
- "code": False,
91
- "color": "default",
92
- },
93
- "plain_text": " - ",
94
- "href": None,
95
- }
96
- )
97
-
98
- if description:
99
- caption.append(
100
- {
101
- "type": "text",
102
- "text": {"content": description, "link": None},
103
- "annotations": {
104
- "bold": False,
105
- "italic": False,
106
- "strikethrough": False,
107
- "underline": False,
108
- "code": False,
109
- "color": "default",
110
- },
111
- "plain_text": description,
112
- "href": None,
113
- }
114
- )
115
-
116
- bookmark_data["caption"] = caption
62
+ # Build caption string
63
+ caption_parts = []
64
+ if title:
65
+ caption_parts.append(title)
66
+ if description:
67
+ caption_parts.append(description)
68
+
69
+ if caption_parts:
70
+ caption_text = " - ".join(caption_parts)
71
+ caption_rich_text = RichTextObject.from_plain_text(caption_text)
72
+ bookmark_data["caption"] = [caption_rich_text.model_dump()]
117
73
  else:
118
- # Empty caption list to match Notion's format for bookmarks without titles
119
74
  bookmark_data["caption"] = []
120
75
 
121
- return {"type": "bookmark", "bookmark": bookmark_data}
76
+ return [{"type": "bookmark", "bookmark": bookmark_data}]
122
77
 
123
78
  @classmethod
124
79
  def notion_to_markdown(cls, block: Dict[str, Any]) -> Optional[str]:
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+ from pydantic import BaseModel
4
+
5
+ from notionary.blocks.markdown_node import MarkdownNode
6
+
7
+
8
+ class BookmarkMarkdownBlockParams(BaseModel):
9
+ url: str
10
+ title: Optional[str] = None
11
+ description: Optional[str] = None
12
+
13
+
14
+ class BookmarkMarkdownNode(MarkdownNode):
15
+ """
16
+ Programmatic interface for creating Notion-style bookmark Markdown blocks.
17
+ """
18
+
19
+ def __init__(
20
+ self, url: str, title: Optional[str] = None, description: Optional[str] = None
21
+ ):
22
+ self.url = url
23
+ self.title = title
24
+ self.description = description
25
+
26
+ @classmethod
27
+ def from_params(cls, params: BookmarkMarkdownBlockParams) -> BookmarkMarkdownNode:
28
+ return cls(url=params.url, title=params.title, description=params.description)
29
+
30
+ def to_markdown(self) -> str:
31
+ """
32
+ Returns the Markdown representation, e.g.:
33
+ [bookmark](https://example.com "Title" "Description")
34
+ """
35
+ parts = [f"[bookmark]({self.url}"]
36
+ if self.title is not None:
37
+ parts.append(f'"{self.title}"')
38
+ if self.description is not None:
39
+ # Wenn title fehlt, aber description da ist, trotzdem Platzhalter für title:
40
+ if self.title is None:
41
+ parts.append('""')
42
+ parts.append(f'"{self.description}"')
43
+ return " ".join(parts) + ")"
@@ -0,0 +1,7 @@
1
+ from .bulleted_list_element import BulletedListElement
2
+ from .bulleted_list_markdown_node import BulletedListMarkdownNode
3
+
4
+ __all__ = [
5
+ "BulletedListElement",
6
+ "BulletedListMarkdownNode",
7
+ ]
@@ -1,16 +1,20 @@
1
1
  import re
2
2
  from typing import Dict, Any, Optional
3
3
  from notionary.blocks import NotionBlockElement
4
- from notionary.blocks import ElementPromptContent, ElementPromptBuilder
4
+ from notionary.blocks import (
5
+ ElementPromptContent,
6
+ ElementPromptBuilder,
7
+ NotionBlockResult,
8
+ )
5
9
 
6
- from notionary.blocks.text_inline_formatter import TextInlineFormatter
10
+ from notionary.blocks.shared.text_inline_formatter import TextInlineFormatter
7
11
 
8
12
 
9
13
  class BulletedListElement(NotionBlockElement):
10
14
  """Class for converting between Markdown bullet lists and Notion bulleted list items."""
11
15
 
12
16
  @classmethod
13
- def markdown_to_notion(cls, text: str) -> Optional[Dict[str, Any]]:
17
+ def markdown_to_notion(cls, text: str) -> NotionBlockResult:
14
18
  """Convert markdown bulleted list item to Notion block."""
15
19
  pattern = re.compile(
16
20
  r"^(\s*)[*\-+]\s+(?!\[[ x]\])(.+)$"
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel
4
+ from notionary.blocks.markdown_node import MarkdownNode
5
+
6
+
7
+ class BulletedListMarkdownBlockParams(BaseModel):
8
+ texts: list[str]
9
+
10
+
11
+ class BulletedListMarkdownNode(MarkdownNode):
12
+ """
13
+ Programmatic interface for creating Markdown bulleted list items.
14
+ Example:
15
+ - First item
16
+ - Second item
17
+ - Third item
18
+ """
19
+
20
+ def __init__(self, texts: list[str]):
21
+ self.texts = texts
22
+
23
+ @classmethod
24
+ def from_params(
25
+ cls, params: BulletedListMarkdownBlockParams
26
+ ) -> BulletedListMarkdownNode:
27
+ return cls(texts=params.texts)
28
+
29
+ def to_markdown(self) -> str:
30
+ result = []
31
+ for text in self.texts:
32
+ result.append(f"- {text}")
33
+ return "\n".join(result)
@@ -0,0 +1,7 @@
1
+ from .callout_element import CalloutElement
2
+ from .callout_markdown_node import CalloutMarkdownNode
3
+
4
+ __all__ = [
5
+ "CalloutElement",
6
+ "CalloutMarkdownNode",
7
+ ]