notionary 0.1.9__py3-none-any.whl → 0.1.11__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.
@@ -0,0 +1,141 @@
1
+ import re
2
+ from typing import Dict, Any, Optional, List
3
+ from notionary.core.converters.elements.notion_block_element import NotionBlockElement
4
+
5
+
6
+ class AudioElement(NotionBlockElement):
7
+ """
8
+ Handles conversion between Markdown audio embeds and Notion audio blocks.
9
+
10
+ Markdown audio syntax (custom format since standard Markdown doesn't support audio):
11
+ - $[Caption](https://example.com/audio.mp3) - Basic audio with caption
12
+ - $[](https://example.com/audio.mp3) - Audio without caption
13
+ - $[Caption](https://storage.googleapis.com/audio_summaries/example.mp3) - CDN hosted audio
14
+
15
+ Supports various audio URLs including direct audio file links from CDNs and other sources.
16
+ """
17
+
18
+ # Regex pattern for audio syntax
19
+ PATTERN = re.compile(
20
+ r"^\$\[(.*?)\]" # $[Caption] part
21
+ + r'\((https?://[^\s"]+)' # (URL part
22
+ + r"\)$" # closing parenthesis
23
+ )
24
+
25
+ # Audio file extensions
26
+ AUDIO_EXTENSIONS = [".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac"]
27
+
28
+ @staticmethod
29
+ def match_markdown(text: str) -> bool:
30
+ """Check if text is a markdown audio embed."""
31
+ text = text.strip()
32
+ return text.startswith("$[") and bool(AudioElement.PATTERN.match(text))
33
+
34
+ @staticmethod
35
+ def match_notion(block: Dict[str, Any]) -> bool:
36
+ """Check if block is a Notion audio."""
37
+ return block.get("type") == "audio"
38
+
39
+ @staticmethod
40
+ def is_audio_url(url: str) -> bool:
41
+ """Check if URL points to an audio file."""
42
+ return any(url.lower().endswith(ext) for ext in AudioElement.AUDIO_EXTENSIONS) or \
43
+ "audio" in url.lower() or \
44
+ "storage.googleapis.com/audio_summaries" in url.lower()
45
+
46
+ @staticmethod
47
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
48
+ """Convert markdown audio embed to Notion audio block."""
49
+ audio_match = AudioElement.PATTERN.match(text.strip())
50
+ if not audio_match:
51
+ return None
52
+
53
+ caption = audio_match.group(1)
54
+ url = audio_match.group(2)
55
+
56
+ if not url:
57
+ return None
58
+
59
+ # Make sure this is an audio URL
60
+ if not AudioElement.is_audio_url(url):
61
+ # If not obviously an audio URL, we'll still accept it as the user might know better
62
+ pass
63
+
64
+ # Prepare the audio block
65
+ audio_block = {
66
+ "type": "audio",
67
+ "audio": {"type": "external", "external": {"url": url}},
68
+ }
69
+
70
+ # Add caption if provided
71
+ if caption:
72
+ audio_block["audio"]["caption"] = [
73
+ {"type": "text", "text": {"content": caption}}
74
+ ]
75
+
76
+ return audio_block
77
+
78
+ @staticmethod
79
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
80
+ """Convert Notion audio block to markdown audio embed."""
81
+ if block.get("type") != "audio":
82
+ return None
83
+
84
+ audio_data = block.get("audio", {})
85
+
86
+ # Handle both external and file (uploaded) audios
87
+ if audio_data.get("type") == "external":
88
+ url = audio_data.get("external", {}).get("url", "")
89
+ elif audio_data.get("type") == "file":
90
+ url = audio_data.get("file", {}).get("url", "")
91
+ else:
92
+ return None
93
+
94
+ if not url:
95
+ return None
96
+
97
+ # Extract caption if available
98
+ caption = ""
99
+ caption_rich_text = audio_data.get("caption", [])
100
+ if caption_rich_text:
101
+ caption = AudioElement._extract_text_content(caption_rich_text)
102
+
103
+ return f"$[{caption}]({url})"
104
+
105
+ @staticmethod
106
+ def is_multiline() -> bool:
107
+ """Audio embeds are single-line elements."""
108
+ return False
109
+
110
+ @staticmethod
111
+ def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
112
+ """Extract plain text content from Notion rich_text elements."""
113
+ result = ""
114
+ for text_obj in rich_text:
115
+ if text_obj.get("type") == "text":
116
+ result += text_obj.get("text", {}).get("content", "")
117
+ elif "plain_text" in text_obj:
118
+ result += text_obj.get("plain_text", "")
119
+ return result
120
+
121
+ @classmethod
122
+ def get_llm_prompt_content(cls) -> dict:
123
+ """Returns information for LLM prompts about this element."""
124
+ return {
125
+ "description": "Embeds audio content from external sources like CDNs or direct audio URLs.",
126
+ "when_to_use": "Use audio embeds when you want to include audio content directly in your document. Audio embeds are useful for podcasts, music, voice recordings, or any content that benefits from audio explanation.",
127
+ "syntax": [
128
+ "$[](https://example.com/audio.mp3) - Audio without caption",
129
+ "$[Caption text](https://example.com/audio.mp3) - Audio with caption",
130
+ ],
131
+ "supported_sources": [
132
+ "Direct links to audio files (.mp3, .wav, .ogg, etc.)",
133
+ "Google Cloud Storage links (storage.googleapis.com)",
134
+ "Other audio hosting platforms supported by Notion",
135
+ ],
136
+ "examples": [
137
+ "$[Podcast Episode](https://storage.googleapis.com/audio_summaries/ep_ai_summary_127d02ec-ca12-4312-a5ed-cb14b185480c.mp3)",
138
+ "$[Voice recording](https://example.com/audio/recording.mp3)",
139
+ "$[](https://storage.googleapis.com/audio_summaries/example.mp3)",
140
+ ],
141
+ }
@@ -0,0 +1,129 @@
1
+ import re
2
+ from typing import Dict, Any, Optional, List
3
+ from notionary.core.converters.elements.notion_block_element import NotionBlockElement
4
+
5
+
6
+ class EmbedElement(NotionBlockElement):
7
+ """
8
+ Handles conversion between Markdown embeds and Notion embed blocks.
9
+
10
+ Markdown embed syntax (custom format):
11
+ - <embed:Caption>(https://example.com) - Basic embed with caption
12
+ - <embed>(https://example.com) - Embed without caption
13
+
14
+ Supports various URL types including websites, PDFs, Google Maps, Google Drive,
15
+ Twitter/X posts, and other sources that Notion can embed.
16
+ """
17
+
18
+ PATTERN = re.compile(
19
+ r"^<embed(?:\:(.*?))?>(?:\s*)"
20
+ + r'\((https?://[^\s"]+)'
21
+ + r"\)$"
22
+ )
23
+
24
+ @staticmethod
25
+ def match_markdown(text: str) -> bool:
26
+ """Check if text is a markdown embed."""
27
+ text = text.strip()
28
+ return text.startswith("<embed") and bool(EmbedElement.PATTERN.match(text))
29
+
30
+ @staticmethod
31
+ def match_notion(block: Dict[str, Any]) -> bool:
32
+ """Check if block is a Notion embed."""
33
+ return block.get("type") == "embed"
34
+
35
+ @staticmethod
36
+ def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
37
+ """Convert markdown embed to Notion embed block."""
38
+ embed_match = EmbedElement.PATTERN.match(text.strip())
39
+ if not embed_match:
40
+ return None
41
+
42
+ caption = embed_match.group(1) or ""
43
+ url = embed_match.group(2)
44
+
45
+ if not url:
46
+ return None
47
+
48
+ # Prepare the embed block
49
+ embed_block = {
50
+ "type": "embed",
51
+ "embed": {"url": url},
52
+ }
53
+
54
+ # Add caption if provided
55
+ if caption:
56
+ embed_block["embed"]["caption"] = [
57
+ {"type": "text", "text": {"content": caption}}
58
+ ]
59
+
60
+ return embed_block
61
+
62
+ @staticmethod
63
+ def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
64
+ """Convert Notion embed block to markdown embed."""
65
+ if block.get("type") != "embed":
66
+ return None
67
+
68
+ embed_data = block.get("embed", {})
69
+ url = embed_data.get("url", "")
70
+
71
+ if not url:
72
+ return None
73
+
74
+ # Extract caption if available
75
+ caption = ""
76
+ caption_rich_text = embed_data.get("caption", [])
77
+ if caption_rich_text:
78
+ caption = EmbedElement._extract_text_content(caption_rich_text)
79
+
80
+ if caption:
81
+ return f"<embed:{caption}>({url})"
82
+ else:
83
+ return f"<embed>({url})"
84
+
85
+ @staticmethod
86
+ def is_multiline() -> bool:
87
+ """Embeds are single-line elements."""
88
+ return False
89
+
90
+ @staticmethod
91
+ def _extract_text_content(rich_text: List[Dict[str, Any]]) -> str:
92
+ """Extract plain text content from Notion rich_text elements."""
93
+ result = ""
94
+ for text_obj in rich_text:
95
+ if text_obj.get("type") == "text":
96
+ result += text_obj.get("text", {}).get("content", "")
97
+ elif "plain_text" in text_obj:
98
+ result += text_obj.get("plain_text", "")
99
+ return result
100
+
101
+ @classmethod
102
+ def get_llm_prompt_content(cls) -> dict:
103
+ """Returns information for LLM prompts about this element."""
104
+ return {
105
+ "description": "Embeds external content from websites, PDFs, Google Maps, and other sources directly in your document.",
106
+ "when_to_use": "Use embeds when you want to include external content that isn't just a video or image. Embeds are great for interactive content, reference materials, or live data sources.",
107
+ "syntax": [
108
+ "<embed>(https://example.com) - Embed without caption",
109
+ "<embed:Caption text>(https://example.com) - Embed with caption",
110
+ ],
111
+ "supported_sources": [
112
+ "Websites and web pages",
113
+ "PDFs and documents",
114
+ "Google Maps",
115
+ "Google Drive files",
116
+ "Twitter/X posts",
117
+ "GitHub repositories and code",
118
+ "Figma designs",
119
+ "Miro boards",
120
+ "Many other services supported by Notion's embed feature",
121
+ ],
122
+ "examples": [
123
+ "<embed:Course materials>(https://drive.google.com/file/d/123456/view)",
124
+ "<embed:Our office location>(https://www.google.com/maps?q=San+Francisco)",
125
+ "<embed:Latest announcement>(https://twitter.com/NotionHQ/status/1234567890)",
126
+ "<embed:Project documentation>(https://github.com/username/repo)",
127
+ "<embed>(https://example.com/important-reference.pdf)",
128
+ ],
129
+ }
@@ -95,6 +95,8 @@ class BlockElementRegistry:
95
95
  """
96
96
  # Create a copy of registered elements
97
97
  element_classes = self._elements.copy()
98
+
99
+ # TODO: Das hier besser formattieren und über debug level lösen . )
98
100
  print("Elements in registry:", element_classes)
99
101
 
100
102
  formatter_names = [e.__name__ for e in element_classes]
@@ -231,4 +233,4 @@ paragraphs, lists, quotes, etc.
231
233
  """
232
234
  element_docs = cls.generate_element_docs(element_classes)
233
235
 
234
- return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
236
+ return cls.SYSTEM_PROMPT_TEMPLATE.format(element_docs=element_docs)
@@ -1,6 +1,8 @@
1
1
  from typing import List, Type
2
2
  from collections import OrderedDict
3
3
 
4
+ from notionary.core.converters.elements.audio_element import AudioElement
5
+ from notionary.core.converters.elements.embed_element import EmbedElement
4
6
  from notionary.core.converters.elements.notion_block_element import NotionBlockElement
5
7
  from notionary.core.converters.registry.block_element_registry import (
6
8
  BlockElementRegistry,
@@ -90,6 +92,8 @@ class BlockElementRegistryBuilder:
90
92
  .add_element(BookmarkElement)
91
93
  .add_element(ImageElement)
92
94
  .add_element(VideoElement)
95
+ .add_element(EmbedElement)
96
+ .add_element(AudioElement)
93
97
  .add_element(ParagraphElement)
94
98
  ) # Add paragraph last as fallback
95
99
 
@@ -306,6 +306,17 @@ async def main():
306
306
  print("\nDemonstration abgeschlossen.")
307
307
 
308
308
 
309
+ async def demo2():
310
+ url = "https://www.notion.so/Jarvis-Clipboard-1a3389d57bd380d7a507e67d1b25822c"
311
+
312
+ page_manager = NotionPageManager(url=url)
313
+
314
+ markdown = """
315
+ $[Podcast Zusammenfassung](https://storage.googleapis.com/audio_summaries/ep_ai_summary_127d02ec-ca12-4312-a5ed-cb14b185480c.mp3)
316
+ """
317
+
318
+ await page_manager.append_markdown(markdown=markdown)
319
+
309
320
  if __name__ == "__main__":
310
- asyncio.run(main())
311
- print("\nDemonstration completed.")
321
+ asyncio.run(demo2())
322
+ print("\nDemonstration completed.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionary
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: A toolkit to convert between Markdown and Notion blocks
5
5
  Home-page: https://github.com/mathisarends/notionary
6
6
  Author: Mathis Arends
@@ -3,11 +3,13 @@ notionary/core/notion_client.py,sha256=9o9-Ki1homkSbM1C51nsaAzVPMt2d4r8cPzoX3NK_
3
3
  notionary/core/converters/__init__.py,sha256=GOUehJbe4BKHtec1MqL1YGu3AX8zFtkwSZfhYkY5-P0,1798
4
4
  notionary/core/converters/markdown_to_notion_converter.py,sha256=PGtg4v5lUvkXXl1Y8E6a3Mf8hEfxfhBrslPs_H_Lq_E,16564
5
5
  notionary/core/converters/notion_to_markdown_converter.py,sha256=c8GyWX8-UrNfRDk7OOBKbSEb5qOwljUCwI6g5risO2c,1287
6
+ notionary/core/converters/elements/audio_element.py,sha256=qTYxpErHw327JdESr4Biv-H89iZRXzBcllamrFu8P_k,5587
6
7
  notionary/core/converters/elements/bookmark_element.py,sha256=bpHobkGnyBGDAJK5vY9R3Ntl4GiRSF-EyyA31aq2O3E,8593
7
8
  notionary/core/converters/elements/callout_element.py,sha256=rkDoXikjIl-zU3GLawSXgRunBJGLnEvin9zIlCgW4TY,5964
8
9
  notionary/core/converters/elements/code_block_element.py,sha256=G1iGMsGSK5KPSk-tA8TsPs9XNU9ydjYfOVnjIvdZG74,5189
9
10
  notionary/core/converters/elements/column_element.py,sha256=ZwQsLBEownVJnzyv-GfNjvzJhAfKz9ncggqZUmZHF5A,10722
10
11
  notionary/core/converters/elements/divider_element.py,sha256=Ul0wXHY96qWL72iAvttRQMOoAGuASgwFwPraGnpUkX0,2792
12
+ notionary/core/converters/elements/embed_element.py,sha256=vEqY7VBOAvtI4uwys6_E1FT9j4bkHJM21OYaT51ahe8,4800
11
13
  notionary/core/converters/elements/heading_element.py,sha256=BCBcpEO_UX92nzCclVHAjlOLFJ5zu9wDlAGbphesaOQ,2788
12
14
  notionary/core/converters/elements/image_element.py,sha256=uU3bY26LvJwD_CAXN11tqYt5Ed84gjUeHWnJmxvH07Y,4861
13
15
  notionary/core/converters/elements/list_element.py,sha256=lM9nVGVG3VtmjMkqxrBj71wiJITuRypwxORu4zghqAM,4878
@@ -19,14 +21,14 @@ notionary/core/converters/elements/text_inline_formatter.py,sha256=FE_Sq2cozpu5R
19
21
  notionary/core/converters/elements/todo_lists.py,sha256=wgY6YejURBQ5ESdVLZVIy9QKchS-x8odrmS8X4cC5Kc,4265
20
22
  notionary/core/converters/elements/toggle_element.py,sha256=Xv4MuuOyoamvT3IEJX4mynvLEycgtZ9LWt6Nm764KXE,6980
21
23
  notionary/core/converters/elements/video_element.py,sha256=xrBLY3e_SgKNamItZkfPNMbNEh37Ftp4jWIV6nwV-ds,6047
22
- notionary/core/converters/registry/block_element_registry.py,sha256=0dpRFMM67UVmXRMP4-Ubc_uCZCVVmRKgxPpueCeknjw,8619
23
- notionary/core/converters/registry/block_element_registry_builder.py,sha256=yTV1GPKAN7o65r9jHIqPeBCw0ICdaUZnRC5JKNwpRq4,9227
24
+ notionary/core/converters/registry/block_element_registry.py,sha256=SRIsPr6xb6xX7GHyrYYNBbt9-l6os29rQndmoXnr5W0,8707
25
+ notionary/core/converters/registry/block_element_registry_builder.py,sha256=D4GmIAdMoP7K1P78qlgN-GoDwtB_4dZTws049gtXQ5c,9457
24
26
  notionary/core/database/database_info_service.py,sha256=58k7om0UXP8w0jCJHewccG5UbOvELMBAbQvXOm7F1OM,1341
25
27
  notionary/core/database/notion_database_manager.py,sha256=cEQHf8bTcgA3hLMgsbMGSXhCmccmxWLQ6oOJiINR3ac,8257
26
28
  notionary/core/database/notion_database_manager_factory.py,sha256=SoWUiM5zdajmR1ppYHTdPgHrdZbwuTMdoXW3_tBffyU,6831
27
29
  notionary/core/database/notion_database_schema.py,sha256=WUIjG7I5kusk4GOOdmVSHIKc2Z8SeOgJ1FuGpTn4avQ,3304
28
30
  notionary/core/database/models/page_result.py,sha256=Vmm5_oYpYAkIIJVoTd1ZZGloeC3cmFLMYP255mAmtaw,233
29
- notionary/core/page/notion_page_manager.py,sha256=tKYCtrqOOGKQR_qcFUX9WpZWZGhG5wPwmZa19XtIxns,13249
31
+ notionary/core/page/notion_page_manager.py,sha256=O5GBa2KzjAiqH_NDg4JpJbkNcfh3AD5uzTkhqHMr8XE,13635
30
32
  notionary/core/page/content/page_content_manager.py,sha256=GjgxVwi5NOMsvGwBNj-woq940ZupzALCQegDyIcsgMg,3358
31
33
  notionary/core/page/metadata/metadata_editor.py,sha256=U3Ff9GRk28dqT9M1xsl6Q3Cj47-hB1n2pNJzeDXy4ks,4938
32
34
  notionary/core/page/metadata/notion_icon_manager.py,sha256=v9pUG61TOT8x9UzDqBtQW6S5XQzWostq7IwrURnWvF4,1499
@@ -45,8 +47,8 @@ notionary/exceptions/page_creation_exception.py,sha256=4v7IuZD6GsQLrqhDLriGjuG3M
45
47
  notionary/util/logging_mixin.py,sha256=fKsx9t90bwvL74ZX3dU-sXdC4TZCQyO6qU9I8txkw_U,1369
46
48
  notionary/util/page_id_utils.py,sha256=9XexVGy8jY5iOlueS1MXFWHtRRmZ8js-EO3hT0_wg2E,1381
47
49
  notionary/util/singleton_decorator.py,sha256=GTNMfIlVNRUVMw_c88xqd12-DcqZJjmyidN54yqiNVw,472
48
- notionary-0.1.9.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
49
- notionary-0.1.9.dist-info/METADATA,sha256=q3KrPXR-bccNYMdF8HT7EeEvyiyBy9GqhESh4-oMcz8,6153
50
- notionary-0.1.9.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
51
- notionary-0.1.9.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
52
- notionary-0.1.9.dist-info/RECORD,,
50
+ notionary-0.1.11.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
51
+ notionary-0.1.11.dist-info/METADATA,sha256=3mCAD1Q1IlhUkPaf8NJAfDENEeZlZWjAmp1tN3e8F28,6154
52
+ notionary-0.1.11.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
53
+ notionary-0.1.11.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
54
+ notionary-0.1.11.dist-info/RECORD,,