notionary 0.2.16__py3-none-any.whl → 0.2.18__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notionary/__init__.py +10 -5
- notionary/base_notion_client.py +18 -7
- notionary/blocks/__init__.py +55 -24
- notionary/blocks/audio/__init__.py +7 -0
- notionary/blocks/audio/audio_element.py +152 -0
- notionary/blocks/audio/audio_markdown_node.py +29 -0
- notionary/blocks/audio/audio_models.py +59 -0
- notionary/blocks/bookmark/__init__.py +7 -0
- notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
- notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
- notionary/blocks/bulleted_list/__init__.py +7 -0
- notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
- notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
- notionary/blocks/callout/__init__.py +7 -0
- notionary/blocks/callout/callout_element.py +132 -0
- notionary/blocks/callout/callout_markdown_node.py +31 -0
- notionary/blocks/callout/callout_models.py +0 -0
- notionary/blocks/code/__init__.py +7 -0
- notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
- notionary/blocks/code/code_markdown_node.py +43 -0
- notionary/blocks/code/code_models.py +0 -0
- notionary/blocks/column/__init__.py +5 -0
- notionary/blocks/{column_element.py → column/column_element.py} +24 -55
- notionary/blocks/column/column_models.py +0 -0
- notionary/blocks/divider/__init__.py +7 -0
- notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
- notionary/blocks/divider/divider_markdown_node.py +24 -0
- notionary/blocks/divider/divider_models.py +0 -0
- notionary/blocks/document/__init__.py +7 -0
- notionary/blocks/document/document_element.py +102 -0
- notionary/blocks/document/document_markdown_node.py +31 -0
- notionary/blocks/document/document_models.py +0 -0
- notionary/blocks/embed/__init__.py +7 -0
- notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
- notionary/blocks/embed/embed_markdown_node.py +30 -0
- notionary/blocks/embed/embed_models.py +0 -0
- notionary/blocks/heading/__init__.py +7 -0
- notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
- notionary/blocks/heading/heading_markdown_node.py +29 -0
- notionary/blocks/heading/heading_models.py +0 -0
- notionary/blocks/image/__init__.py +7 -0
- notionary/blocks/{image_element.py → image/image_element.py} +62 -42
- notionary/blocks/image/image_markdown_node.py +33 -0
- notionary/blocks/image/image_models.py +0 -0
- notionary/blocks/markdown_builder.py +356 -0
- notionary/blocks/markdown_node.py +29 -0
- notionary/blocks/mention/__init__.py +7 -0
- notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
- notionary/blocks/mention/mention_markdown_node.py +38 -0
- notionary/blocks/mention/mention_models.py +0 -0
- notionary/blocks/numbered_list/__init__.py +7 -0
- notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
- notionary/blocks/numbered_list/numbered_list_models.py +0 -0
- notionary/blocks/paragraph/__init__.py +7 -0
- notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
- notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
- notionary/blocks/paragraph/paragraph_models.py +0 -0
- notionary/blocks/quote/__init__.py +7 -0
- notionary/blocks/quote/quote_element.py +92 -0
- notionary/blocks/quote/quote_markdown_node.py +23 -0
- notionary/blocks/quote/quote_models.py +0 -0
- notionary/blocks/registry/block_registry.py +17 -3
- notionary/blocks/registry/block_registry_builder.py +90 -178
- notionary/blocks/shared/__init__.py +0 -0
- notionary/blocks/shared/block_client.py +256 -0
- notionary/blocks/shared/models.py +710 -0
- notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
- notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
- notionary/blocks/shared/text_inline_formatter_new.py +139 -0
- notionary/blocks/table/__init__.py +7 -0
- notionary/blocks/{table_element.py → table/table_element.py} +23 -11
- notionary/blocks/table/table_markdown_node.py +40 -0
- notionary/blocks/table/table_models.py +0 -0
- notionary/blocks/todo/__init__.py +7 -0
- notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
- notionary/blocks/todo/todo_markdown_node.py +31 -0
- notionary/blocks/todo/todo_models.py +0 -0
- notionary/blocks/toggle/__init__.py +4 -0
- notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
- notionary/blocks/toggle/toggle_markdown_node.py +35 -0
- notionary/blocks/toggle/toggle_models.py +0 -0
- notionary/blocks/toggleable_heading/__init__.py +9 -0
- notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
- notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
- notionary/blocks/video/__init__.py +7 -0
- notionary/blocks/{video_element.py → video/video_element.py} +82 -57
- notionary/blocks/video/video_markdown_node.py +30 -0
- notionary/database/__init__.py +4 -0
- notionary/database/database.py +481 -0
- notionary/database/{filter_builder.py → database_filter_builder.py} +27 -29
- notionary/database/{notion_database_provider.py → database_provider.py} +4 -4
- notionary/database/notion_database.py +45 -18
- notionary/file_upload/__init__.py +7 -0
- notionary/file_upload/client.py +254 -0
- notionary/file_upload/models.py +60 -0
- notionary/file_upload/notion_file_upload.py +387 -0
- notionary/page/content/markdown_whitespace_processor.py +80 -0
- notionary/page/content/notion_text_length_utils.py +87 -0
- notionary/page/content/page_content_retriever.py +2 -2
- notionary/page/content/page_content_writer.py +97 -148
- notionary/page/formatting/line_processor.py +153 -0
- notionary/page/formatting/markdown_to_notion_converter.py +103 -424
- notionary/page/notion_page.py +13 -14
- notionary/page/notion_to_markdown_converter.py +9 -13
- notionary/telemetry/views.py +15 -6
- notionary/user/__init__.py +11 -0
- notionary/user/base_notion_user.py +52 -0
- notionary/user/client.py +129 -0
- notionary/user/models.py +83 -0
- notionary/user/notion_bot_user.py +227 -0
- notionary/user/notion_user.py +256 -0
- notionary/user/notion_user_manager.py +173 -0
- notionary/user/notion_user_provider.py +1 -0
- notionary/util/__init__.py +3 -5
- notionary/util/factory_decorator.py +0 -33
- notionary/util/factory_only.py +37 -0
- notionary/util/fuzzy.py +74 -0
- notionary/util/logging_mixin.py +12 -12
- notionary/workspace.py +38 -3
- {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/METADATA +2 -1
- notionary-0.2.18.dist-info/RECORD +149 -0
- notionary/blocks/audio_element.py +0 -144
- notionary/blocks/callout_element.py +0 -122
- notionary/blocks/notion_block_client.py +0 -26
- notionary/blocks/qoute_element.py +0 -169
- notionary/page/content/notion_page_content_chunker.py +0 -84
- notionary/page/formatting/spacer_rules.py +0 -483
- notionary/util/fuzzy_matcher.py +0 -82
- notionary-0.2.16.dist-info/RECORD +0 -71
- /notionary/{elements/__init__.py → blocks/bookmark/bookmark_models.py} +0 -0
- /notionary/database/{database_exceptions.py → exceptions.py} +0 -0
- /notionary/util/{singleton_decorator.py → singleton.py} +0 -0
- {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/LICENSE +0 -0
- {notionary-0.2.16.dist-info → notionary-0.2.18.dist-info}/WHEEL +0 -0
notionary/__init__.py
CHANGED
@@ -1,13 +1,18 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
from .database.notion_database import NotionDatabase
|
4
|
-
|
1
|
+
from .database import NotionDatabase, DatabaseFilterBuilder
|
5
2
|
from .page.notion_page import NotionPage
|
6
3
|
from .workspace import NotionWorkspace
|
7
|
-
|
4
|
+
from .user import NotionUser, NotionUserManager, NotionBotUser
|
5
|
+
from .file_upload import NotionFileUpload
|
6
|
+
from .blocks.markdown_builder import MarkdownBuilder
|
8
7
|
|
9
8
|
__all__ = [
|
10
9
|
"NotionDatabase",
|
10
|
+
"DatabaseFilterBuilder",
|
11
11
|
"NotionPage",
|
12
12
|
"NotionWorkspace",
|
13
|
+
"NotionUser",
|
14
|
+
"NotionUserManager",
|
15
|
+
"NotionBotUser",
|
16
|
+
"NotionFileUpload",
|
17
|
+
"MarkdownBuilder",
|
13
18
|
]
|
notionary/base_notion_client.py
CHANGED
@@ -93,14 +93,17 @@ class BaseNotionClient(LoggingMixin, ABC):
|
|
93
93
|
self._is_initialized = False
|
94
94
|
self.logger.debug("NotionClient closed")
|
95
95
|
|
96
|
-
async def get(
|
96
|
+
async def get(
|
97
|
+
self, endpoint: str, params: Optional[Dict[str, Any]] = None
|
98
|
+
) -> Optional[Dict[str, Any]]:
|
97
99
|
"""
|
98
100
|
Sends a GET request to the specified Notion API endpoint.
|
99
101
|
|
100
102
|
Args:
|
101
103
|
endpoint: The API endpoint (without base URL)
|
104
|
+
params: Query parameters to include in the request
|
102
105
|
"""
|
103
|
-
return await self._make_request(HttpMethod.GET, endpoint)
|
106
|
+
return await self._make_request(HttpMethod.GET, endpoint, params=params)
|
104
107
|
|
105
108
|
async def post(
|
106
109
|
self, endpoint: str, data: Optional[Dict[str, Any]] = None
|
@@ -141,6 +144,7 @@ class BaseNotionClient(LoggingMixin, ABC):
|
|
141
144
|
method: Union[HttpMethod, str],
|
142
145
|
endpoint: str,
|
143
146
|
data: Optional[Dict[str, Any]] = None,
|
147
|
+
params: Optional[Dict[str, Any]] = None,
|
144
148
|
) -> Optional[Dict[str, Any]]:
|
145
149
|
"""
|
146
150
|
Executes an HTTP request and returns the data or None on error.
|
@@ -149,6 +153,7 @@ class BaseNotionClient(LoggingMixin, ABC):
|
|
149
153
|
method: HTTP method to use
|
150
154
|
endpoint: API endpoint
|
151
155
|
data: Request body data (for POST/PATCH)
|
156
|
+
params: Query parameters (for GET requests)
|
152
157
|
"""
|
153
158
|
await self.ensure_initialized()
|
154
159
|
|
@@ -160,15 +165,21 @@ class BaseNotionClient(LoggingMixin, ABC):
|
|
160
165
|
try:
|
161
166
|
self.logger.debug("Sending %s request to %s", method_str.upper(), url)
|
162
167
|
|
168
|
+
request_kwargs = {}
|
169
|
+
|
170
|
+
# Add query parameters for GET requests
|
171
|
+
if params:
|
172
|
+
request_kwargs["params"] = params
|
173
|
+
|
163
174
|
if (
|
164
175
|
method_str in [HttpMethod.POST.value, HttpMethod.PATCH.value]
|
165
176
|
and data is not None
|
166
177
|
):
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
178
|
+
request_kwargs["json"] = data
|
179
|
+
|
180
|
+
response: httpx.Response = await getattr(self.client, method_str)(
|
181
|
+
url, **request_kwargs
|
182
|
+
)
|
172
183
|
|
173
184
|
response.raise_for_status()
|
174
185
|
result_data = response.json()
|
notionary/blocks/__init__.py
CHANGED
@@ -2,60 +2,91 @@
|
|
2
2
|
from .prompts.element_prompt_content import ElementPromptContent
|
3
3
|
from .prompts.element_prompt_builder import ElementPromptBuilder
|
4
4
|
|
5
|
-
from .notion_block_element import
|
5
|
+
from .shared.notion_block_element import (
|
6
|
+
NotionBlockElement,
|
7
|
+
NotionBlockResult,
|
8
|
+
NotionBlock,
|
9
|
+
)
|
6
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
|
7
34
|
|
8
|
-
from .
|
9
|
-
from .bulleted_list_element import BulletedListElement
|
10
|
-
from .callout_element import CalloutElement
|
11
|
-
from .code_block_element import CodeBlockElement
|
12
|
-
from .column_element import ColumnElement
|
13
|
-
from .divider_element import DividerElement
|
14
|
-
from .embed_element import EmbedElement
|
15
|
-
from .heading_element import HeadingElement
|
16
|
-
from .image_element import ImageElement
|
17
|
-
from .numbered_list_element import NumberedListElement
|
18
|
-
from .paragraph_element import ParagraphElement
|
19
|
-
from .table_element import TableElement
|
20
|
-
from .toggle_element import ToggleElement
|
21
|
-
from .todo_element import TodoElement
|
22
|
-
from .video_element import VideoElement
|
23
|
-
from .toggleable_heading_element import ToggleableHeadingElement
|
24
|
-
from .bookmark_element import BookmarkElement
|
25
|
-
from .divider_element import DividerElement
|
26
|
-
from .heading_element import HeadingElement
|
27
|
-
from .mention_element import MentionElement
|
28
|
-
from .qoute_element import QuoteElement
|
35
|
+
from .markdown_node import MarkdownNode
|
29
36
|
|
30
37
|
from .registry.block_registry import BlockRegistry
|
31
38
|
from .registry.block_registry_builder import BlockRegistryBuilder
|
32
|
-
from .notion_block_client import NotionBlockClient
|
33
39
|
|
40
|
+
from .shared.block_client import NotionBlockClient
|
34
41
|
|
35
42
|
__all__ = [
|
43
|
+
"MarkdownNode",
|
36
44
|
"ElementPromptContent",
|
37
45
|
"ElementPromptBuilder",
|
38
46
|
"NotionBlockElement",
|
39
47
|
"AudioElement",
|
48
|
+
"AudioMarkdownNode",
|
40
49
|
"BulletedListElement",
|
50
|
+
"BulletedListMarkdownNode",
|
41
51
|
"CalloutElement",
|
42
|
-
"
|
52
|
+
"CalloutMarkdownNode",
|
53
|
+
"CodeElement",
|
54
|
+
"CodeMarkdownNode",
|
43
55
|
"ColumnElement",
|
44
56
|
"DividerElement",
|
57
|
+
"DividerMarkdownNode",
|
45
58
|
"EmbedElement",
|
59
|
+
"EmbedMarkdownNode",
|
46
60
|
"HeadingElement",
|
61
|
+
"HeadingMarkdownNode",
|
47
62
|
"ImageElement",
|
63
|
+
"ImageMarkdownNode",
|
48
64
|
"NumberedListElement",
|
65
|
+
"NumberedListMarkdownNode",
|
49
66
|
"ParagraphElement",
|
67
|
+
"ParagraphMarkdownNode",
|
50
68
|
"TableElement",
|
69
|
+
"TableMarkdownNode",
|
51
70
|
"ToggleElement",
|
71
|
+
"ToggleMarkdownNode",
|
52
72
|
"TodoElement",
|
73
|
+
"TodoMarkdownNode",
|
53
74
|
"VideoElement",
|
75
|
+
"VideoMarkdownNode",
|
54
76
|
"ToggleableHeadingElement",
|
77
|
+
"ToggleableHeadingMarkdownNode",
|
55
78
|
"BookmarkElement",
|
79
|
+
"BookmarkMarkdownNode",
|
56
80
|
"MentionElement",
|
81
|
+
"MentionMarkdownNode",
|
57
82
|
"QuoteElement",
|
83
|
+
"QuoteMarkdownNode",
|
84
|
+
"DocumentElement",
|
85
|
+
"DocumentMarkdownNode",
|
58
86
|
"BlockRegistry",
|
59
87
|
"BlockRegistryBuilder",
|
88
|
+
"TextInlineFormatter",
|
89
|
+
"NotionBlockResult",
|
90
|
+
"NotionBlock",
|
60
91
|
"NotionBlockClient",
|
61
92
|
]
|
@@ -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('')
|
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
|
+
)
|
@@ -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
|
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
|
-
|
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) ->
|
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
|
-
#
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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) + ")"
|
@@ -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
|
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) ->
|
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]\])(.+)$"
|