notionary 0.1.14__py3-none-any.whl → 0.1.15__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/elements/block_element_registry_builder.py +1 -19
- notionary/elements/mention_element.py +163 -71
- notionary/notion_client.py +55 -4
- notionary/page/content/page_content_manager.py +94 -23
- notionary/page/notion_to_markdown_converter.py +59 -43
- {notionary-0.1.14.dist-info → notionary-0.1.15.dist-info}/METADATA +1 -1
- {notionary-0.1.14.dist-info → notionary-0.1.15.dist-info}/RECORD +10 -10
- {notionary-0.1.14.dist-info → notionary-0.1.15.dist-info}/WHEEL +0 -0
- {notionary-0.1.14.dist-info → notionary-0.1.15.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.1.14.dist-info → notionary-0.1.15.dist-info}/top_level.txt +0 -0
@@ -155,7 +155,7 @@ class BlockElementRegistryBuilder:
|
|
155
155
|
Returns:
|
156
156
|
Self for method chaining
|
157
157
|
"""
|
158
|
-
return self.add_element(element_class)
|
158
|
+
return self.add_element(element_class)
|
159
159
|
|
160
160
|
def _ensure_paragraph_at_end(self) -> None:
|
161
161
|
"""
|
@@ -309,15 +309,6 @@ class BlockElementRegistryBuilder:
|
|
309
309
|
"""
|
310
310
|
return self.add_element(AudioElement)
|
311
311
|
|
312
|
-
def with_rich_content(self) -> BlockElementRegistryBuilder:
|
313
|
-
"""
|
314
|
-
Add support for rich content elements (callouts, toggles, etc.).
|
315
|
-
|
316
|
-
Returns:
|
317
|
-
Self for method chaining
|
318
|
-
"""
|
319
|
-
return self.with_callouts().with_toggles().with_quotes()
|
320
|
-
|
321
312
|
def with_media_support(self) -> BlockElementRegistryBuilder:
|
322
313
|
"""
|
323
314
|
Add support for media elements (images, videos, audio).
|
@@ -327,15 +318,6 @@ class BlockElementRegistryBuilder:
|
|
327
318
|
"""
|
328
319
|
return self.with_images().with_videos().with_audio()
|
329
320
|
|
330
|
-
def with_task_support(self) -> BlockElementRegistryBuilder:
|
331
|
-
"""
|
332
|
-
Add support for task-related elements (todos).
|
333
|
-
|
334
|
-
Returns:
|
335
|
-
Self for method chaining
|
336
|
-
"""
|
337
|
-
return self.with_todos()
|
338
|
-
|
339
321
|
def with_mention(self) -> BlockElementRegistryBuilder:
|
340
322
|
return self.add_element(MentionElement)
|
341
323
|
|
@@ -1,48 +1,166 @@
|
|
1
|
+
import re
|
1
2
|
from typing import Dict, Any, Optional, List
|
2
3
|
from typing_extensions import override
|
3
4
|
|
4
5
|
from notionary.elements.notion_block_element import NotionBlockElement
|
5
6
|
|
7
|
+
|
6
8
|
class MentionElement(NotionBlockElement):
|
7
9
|
"""
|
8
10
|
Handles conversion between Markdown mentions and Notion mention elements.
|
9
|
-
|
11
|
+
|
10
12
|
Markdown mention syntax:
|
11
13
|
- @[page-id] - Mention a page by its ID
|
12
|
-
|
13
|
-
|
14
|
-
as page mentions in Markdown would typically require knowing internal page IDs.
|
14
|
+
- @date[YYYY-MM-DD] - Mention a date
|
15
|
+
- @db[database-id] - Mention a database by its ID
|
15
16
|
"""
|
16
|
-
|
17
|
+
|
18
|
+
MENTION_TYPES = {
|
19
|
+
"page": {
|
20
|
+
"pattern": r"@\[([0-9a-f-]+)\]",
|
21
|
+
"create_mention": lambda id_value: {
|
22
|
+
"type": "mention",
|
23
|
+
"mention": {"type": "page", "page": {"id": id_value}},
|
24
|
+
},
|
25
|
+
"get_plain_text": lambda mention: f"Page {mention['mention']['page']['id']}",
|
26
|
+
"to_markdown": lambda mention: f"@[{mention['mention']['page']['id']}]",
|
27
|
+
},
|
28
|
+
"date": {
|
29
|
+
"pattern": r"@date\[(\d{4}-\d{2}-\d{2})\]",
|
30
|
+
"create_mention": lambda date_value: {
|
31
|
+
"type": "mention",
|
32
|
+
"mention": {"type": "date", "date": {"start": date_value, "end": None}},
|
33
|
+
},
|
34
|
+
"get_plain_text": lambda mention: mention["mention"]["date"]["start"],
|
35
|
+
"to_markdown": lambda mention: f"@date[{mention['mention']['date']['start']}]",
|
36
|
+
},
|
37
|
+
"database": {
|
38
|
+
"pattern": r"@db\[([0-9a-f-]+)\]",
|
39
|
+
"create_mention": lambda db_id: {
|
40
|
+
"type": "mention",
|
41
|
+
"mention": {"type": "database", "database": {"id": db_id}},
|
42
|
+
},
|
43
|
+
"get_plain_text": lambda mention: f"Database {mention['mention']['database']['id']}",
|
44
|
+
"to_markdown": lambda mention: f"@db[{mention['mention']['database']['id']}]",
|
45
|
+
},
|
46
|
+
}
|
47
|
+
|
17
48
|
@override
|
18
49
|
@staticmethod
|
19
50
|
def match_markdown(text: str) -> bool:
|
20
|
-
"""Check if text
|
51
|
+
"""Check if text contains a markdown mention."""
|
52
|
+
for mention_type in MentionElement.MENTION_TYPES.values():
|
53
|
+
if re.search(mention_type["pattern"], text):
|
54
|
+
return True
|
21
55
|
return False
|
22
|
-
|
56
|
+
|
23
57
|
@override
|
24
58
|
@staticmethod
|
25
59
|
def match_notion(block: Dict[str, Any]) -> bool:
|
26
60
|
"""Check if block contains a mention."""
|
27
|
-
|
61
|
+
supported_block_types = [
|
62
|
+
"paragraph",
|
63
|
+
"heading_1",
|
64
|
+
"heading_2",
|
65
|
+
"heading_3",
|
66
|
+
"bulleted_list_item",
|
67
|
+
"numbered_list_item",
|
68
|
+
]
|
69
|
+
|
70
|
+
if block.get("type") not in supported_block_types:
|
28
71
|
return False
|
29
|
-
|
72
|
+
|
30
73
|
block_content = block.get(block.get("type"), {})
|
31
74
|
rich_text = block_content.get("rich_text", [])
|
32
|
-
|
33
|
-
for text_item in rich_text
|
34
|
-
|
35
|
-
return True
|
36
|
-
|
37
|
-
return False
|
38
|
-
|
75
|
+
|
76
|
+
return any(text_item.get("type") == "mention" for text_item in rich_text)
|
77
|
+
|
39
78
|
@override
|
40
79
|
@staticmethod
|
41
80
|
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
42
|
-
"""Convert markdown
|
43
|
-
|
44
|
-
|
45
|
-
|
81
|
+
"""Convert markdown text with mentions to a Notion paragraph block."""
|
82
|
+
if not MentionElement.match_markdown(text):
|
83
|
+
return None
|
84
|
+
|
85
|
+
rich_text = MentionElement._process_markdown_with_mentions(text)
|
86
|
+
|
87
|
+
return {
|
88
|
+
"type": "paragraph",
|
89
|
+
"paragraph": {"rich_text": rich_text, "color": "default"},
|
90
|
+
}
|
91
|
+
|
92
|
+
@staticmethod
|
93
|
+
def _process_markdown_with_mentions(text: str) -> List[Dict[str, Any]]:
|
94
|
+
"""Convert markdown mentions to Notion rich_text format."""
|
95
|
+
mentions = []
|
96
|
+
|
97
|
+
for mention_type, config in MentionElement.MENTION_TYPES.items():
|
98
|
+
for match in re.finditer(config["pattern"], text):
|
99
|
+
mentions.append(
|
100
|
+
{
|
101
|
+
"start": match.start(),
|
102
|
+
"end": match.end(),
|
103
|
+
"type": mention_type,
|
104
|
+
"value": match.group(1),
|
105
|
+
"original": match.group(0),
|
106
|
+
}
|
107
|
+
)
|
108
|
+
|
109
|
+
mentions.sort(key=lambda m: m["start"])
|
110
|
+
|
111
|
+
# Build rich_text list
|
112
|
+
rich_text = []
|
113
|
+
position = 0
|
114
|
+
|
115
|
+
for mention in mentions:
|
116
|
+
if mention["start"] > position:
|
117
|
+
rich_text.append(
|
118
|
+
MentionElement._create_text_item(text[position : mention["start"]])
|
119
|
+
)
|
120
|
+
|
121
|
+
# Add the mention
|
122
|
+
mention_obj = MentionElement.MENTION_TYPES[mention["type"]][
|
123
|
+
"create_mention"
|
124
|
+
](mention["value"])
|
125
|
+
|
126
|
+
# Add annotations and plain text
|
127
|
+
mention_obj["annotations"] = MentionElement._default_annotations()
|
128
|
+
mention_obj["plain_text"] = MentionElement.MENTION_TYPES[mention["type"]][
|
129
|
+
"get_plain_text"
|
130
|
+
](mention_obj)
|
131
|
+
|
132
|
+
rich_text.append(mention_obj)
|
133
|
+
position = mention["end"]
|
134
|
+
|
135
|
+
# Add remaining text if any
|
136
|
+
if position < len(text):
|
137
|
+
rich_text.append(MentionElement._create_text_item(text[position:]))
|
138
|
+
|
139
|
+
return rich_text
|
140
|
+
|
141
|
+
@staticmethod
|
142
|
+
def _create_text_item(content: str) -> Dict[str, Any]:
|
143
|
+
"""Create a text item with default annotations."""
|
144
|
+
text_item = {
|
145
|
+
"type": "text",
|
146
|
+
"text": {"content": content, "link": None},
|
147
|
+
"annotations": MentionElement._default_annotations(),
|
148
|
+
"plain_text": content,
|
149
|
+
}
|
150
|
+
return text_item
|
151
|
+
|
152
|
+
@staticmethod
|
153
|
+
def _default_annotations() -> Dict[str, Any]:
|
154
|
+
"""Return default annotations for rich text."""
|
155
|
+
return {
|
156
|
+
"bold": False,
|
157
|
+
"italic": False,
|
158
|
+
"strikethrough": False,
|
159
|
+
"underline": False,
|
160
|
+
"code": False,
|
161
|
+
"color": "default",
|
162
|
+
}
|
163
|
+
|
46
164
|
@override
|
47
165
|
@staticmethod
|
48
166
|
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
@@ -50,86 +168,60 @@ class MentionElement(NotionBlockElement):
|
|
50
168
|
block_type = block.get("type")
|
51
169
|
if not block_type or block_type not in block:
|
52
170
|
return None
|
53
|
-
|
171
|
+
|
54
172
|
block_content = block.get(block_type, {})
|
55
173
|
rich_text = block_content.get("rich_text", [])
|
56
|
-
|
174
|
+
|
57
175
|
processed_text = MentionElement._process_rich_text_with_mentions(rich_text)
|
58
|
-
|
59
|
-
if
|
176
|
+
|
177
|
+
if processed_text:
|
60
178
|
return processed_text
|
61
|
-
|
179
|
+
|
62
180
|
return None
|
63
|
-
|
181
|
+
|
64
182
|
@staticmethod
|
65
183
|
def _process_rich_text_with_mentions(rich_text: List[Dict[str, Any]]) -> str:
|
66
|
-
"""
|
184
|
+
"""Convert rich text with mentions to markdown string."""
|
67
185
|
result = []
|
68
|
-
|
186
|
+
|
69
187
|
for item in rich_text:
|
70
188
|
if item.get("type") == "mention":
|
71
189
|
mention = item.get("mention", {})
|
72
190
|
mention_type = mention.get("type")
|
73
|
-
|
74
|
-
if mention_type
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
user_id = mention.get("user", {}).get("id", "")
|
79
|
-
result.append(f"@user[{user_id}]")
|
80
|
-
elif mention_type == "date":
|
81
|
-
date_value = mention.get("date", {}).get("start", "")
|
82
|
-
result.append(f"@date[{date_value}]")
|
83
|
-
elif mention_type == "database":
|
84
|
-
db_id = mention.get("database", {}).get("id", "")
|
85
|
-
result.append(f"@db[{db_id}]")
|
191
|
+
|
192
|
+
if mention_type in MentionElement.MENTION_TYPES:
|
193
|
+
result.append(
|
194
|
+
MentionElement.MENTION_TYPES[mention_type]["to_markdown"](item)
|
195
|
+
)
|
86
196
|
else:
|
87
|
-
# Unknown mention type, fallback to plain text if available
|
88
197
|
result.append(item.get("plain_text", "@[unknown]"))
|
89
198
|
else:
|
90
|
-
# Regular text item
|
91
199
|
result.append(item.get("plain_text", ""))
|
92
|
-
|
200
|
+
|
93
201
|
return "".join(result)
|
94
|
-
|
95
|
-
@staticmethod
|
96
|
-
def _is_only_mentions(rich_text: List[Dict[str, Any]]) -> bool:
|
97
|
-
"""Check if rich_text array contains only mentions."""
|
98
|
-
if not rich_text:
|
99
|
-
return False
|
100
|
-
|
101
|
-
for item in rich_text:
|
102
|
-
if item.get("type") != "mention":
|
103
|
-
return False
|
104
|
-
|
105
|
-
return True
|
106
|
-
|
202
|
+
|
107
203
|
@override
|
108
204
|
@staticmethod
|
109
205
|
def is_multiline() -> bool:
|
110
206
|
return False
|
111
|
-
|
207
|
+
|
112
208
|
@classmethod
|
113
209
|
def get_llm_prompt_content(cls) -> dict:
|
114
|
-
"""
|
115
|
-
Returns a dictionary with all information needed for LLM prompts about this element.
|
116
|
-
"""
|
210
|
+
"""Information about this element for LLM-based processing."""
|
117
211
|
return {
|
118
|
-
"description": "References to Notion pages,
|
119
|
-
"when_to_use": "
|
212
|
+
"description": "References to Notion pages, databases, or dates within text content.",
|
213
|
+
"when_to_use": "When you want to link to other Notion content within your text.",
|
120
214
|
"syntax": [
|
121
215
|
"@[page-id] - Reference to a Notion page",
|
122
|
-
"@user[user-id] - Reference to a Notion user",
|
123
216
|
"@date[YYYY-MM-DD] - Reference to a date",
|
124
|
-
"@db[database-id] - Reference to a Notion database"
|
217
|
+
"@db[database-id] - Reference to a Notion database",
|
125
218
|
],
|
126
219
|
"examples": [
|
127
220
|
"Check the meeting notes at @[1a6389d5-7bd3-80c5-9a87-e90b034989d0]",
|
128
|
-
"
|
129
|
-
"
|
221
|
+
"Deadline is @date[2023-12-31]",
|
222
|
+
"Use the structure in @db[1a6389d5-7bd3-80e9-b199-000cfb3fa0b3]",
|
130
223
|
],
|
131
224
|
"limitations": [
|
132
|
-
"Mentions
|
133
|
-
|
134
|
-
|
135
|
-
}
|
225
|
+
"Mentions require knowing the internal IDs of the pages or databases you want to reference"
|
226
|
+
],
|
227
|
+
}
|
notionary/notion_client.py
CHANGED
@@ -9,8 +9,6 @@ from notionary.util.logging_mixin import LoggingMixin
|
|
9
9
|
|
10
10
|
|
11
11
|
class HttpMethod(Enum):
|
12
|
-
"""Enum für HTTP-Methoden."""
|
13
|
-
|
14
12
|
GET = "get"
|
15
13
|
POST = "post"
|
16
14
|
PATCH = "patch"
|
@@ -42,31 +40,84 @@ class NotionClient(LoggingMixin):
|
|
42
40
|
|
43
41
|
@classmethod
|
44
42
|
async def close_all(cls):
|
43
|
+
"""
|
44
|
+
Closes all active NotionClient instances and releases resources.
|
45
|
+
"""
|
45
46
|
for instance in list(cls._instances):
|
46
47
|
await instance.close()
|
47
48
|
|
48
|
-
async def close(self)
|
49
|
+
async def close(self):#
|
50
|
+
"""
|
51
|
+
Closes the HTTP client for this instance and releases resources.
|
52
|
+
"""
|
49
53
|
if hasattr(self, "client") and self.client:
|
50
54
|
await self.client.aclose()
|
51
55
|
self.client = None
|
52
56
|
|
53
57
|
async def get(self, endpoint: str) -> Optional[Dict[str, Any]]:
|
58
|
+
"""
|
59
|
+
Sends a GET request to the specified Notion API endpoint.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
endpoint: The relative API path (e.g., 'databases/<id>').
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
A dictionary with the response data, or None if the request failed.
|
66
|
+
"""
|
54
67
|
return await self._make_request(HttpMethod.GET, endpoint)
|
55
68
|
|
56
69
|
async def get_page(self, page_id: str) -> Optional[Dict[str, Any]]:
|
57
|
-
|
70
|
+
"""
|
71
|
+
Fetches metadata for a Notion page by its ID.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
page_id: The Notion page ID.
|
58
75
|
|
76
|
+
Returns:
|
77
|
+
A dictionary with the page data, or None if the request failed.
|
78
|
+
"""
|
79
|
+
return await self.get(f"pages/{page_id}")
|
80
|
+
|
59
81
|
async def post(
|
60
82
|
self, endpoint: str, data: Optional[Dict[str, Any]] = None
|
61
83
|
) -> Optional[Dict[str, Any]]:
|
84
|
+
"""
|
85
|
+
Sends a POST request to the specified Notion API endpoint.
|
86
|
+
|
87
|
+
Args:
|
88
|
+
endpoint: The relative API path.
|
89
|
+
data: Optional dictionary payload to send with the request.
|
90
|
+
|
91
|
+
Returns:
|
92
|
+
A dictionary with the response data, or None if the request failed.
|
93
|
+
"""
|
62
94
|
return await self._make_request(HttpMethod.POST, endpoint, data)
|
63
95
|
|
64
96
|
async def patch(
|
65
97
|
self, endpoint: str, data: Optional[Dict[str, Any]] = None
|
66
98
|
) -> Optional[Dict[str, Any]]:
|
99
|
+
"""
|
100
|
+
Sends a PATCH request to the specified Notion API endpoint.
|
101
|
+
|
102
|
+
Args:
|
103
|
+
endpoint: The relative API path.
|
104
|
+
data: Optional dictionary payload to send with the request.
|
105
|
+
|
106
|
+
Returns:
|
107
|
+
A dictionary with the response data, or None if the request failed.
|
108
|
+
"""
|
67
109
|
return await self._make_request(HttpMethod.PATCH, endpoint, data)
|
68
110
|
|
69
111
|
async def delete(self, endpoint: str) -> bool:
|
112
|
+
"""
|
113
|
+
Sends a DELETE request to the specified Notion API endpoint.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
endpoint: The relative API path.
|
117
|
+
|
118
|
+
Returns:
|
119
|
+
True if the request was successful, False otherwise.
|
120
|
+
"""
|
70
121
|
result = await self._make_request(HttpMethod.DELETE, endpoint)
|
71
122
|
return result is not None
|
72
123
|
|
@@ -39,8 +39,8 @@ class PageContentManager(LoggingMixin):
|
|
39
39
|
"""
|
40
40
|
try:
|
41
41
|
blocks = self._markdown_to_notion_converter.convert(markdown_text)
|
42
|
+
print(json.dumps(blocks, indent=4))
|
42
43
|
|
43
|
-
# Fix any blocks that exceed Notion's content length limits
|
44
44
|
fixed_blocks = self._chunker.fix_blocks_content_length(blocks)
|
45
45
|
|
46
46
|
result = await self._client.patch(
|
@@ -55,26 +55,81 @@ class PageContentManager(LoggingMixin):
|
|
55
55
|
self.logger.error("Error appending markdown: %s", str(e))
|
56
56
|
raise
|
57
57
|
|
58
|
-
async def
|
59
|
-
|
60
|
-
if
|
61
|
-
|
58
|
+
async def has_database_descendant(self, block_id: str) -> bool:
|
59
|
+
"""
|
60
|
+
Check if a block or any of its descendants is a database.
|
61
|
+
"""
|
62
|
+
resp = await self._client.get(f"blocks/{block_id}/children")
|
63
|
+
children = resp.get("results", []) if resp else []
|
64
|
+
|
65
|
+
for child in children:
|
66
|
+
block_type = child.get("type")
|
67
|
+
if block_type in ["child_database", "database", "linked_database"]:
|
68
|
+
return True
|
62
69
|
|
63
|
-
|
64
|
-
|
65
|
-
|
70
|
+
if child.get("has_children", False):
|
71
|
+
if await self.has_database_descendant(child["id"]):
|
72
|
+
return True
|
66
73
|
|
74
|
+
return False
|
75
|
+
|
76
|
+
async def delete_block_with_children(
|
77
|
+
self, block: Dict[str, Any], skip_databases: bool
|
78
|
+
) -> tuple[int, int]:
|
79
|
+
"""
|
80
|
+
Delete a block and all its children, optionally skipping databases.
|
81
|
+
Returns a tuple of (deleted_count, skipped_count).
|
82
|
+
"""
|
67
83
|
deleted = 0
|
68
84
|
skipped = 0
|
69
|
-
for block in results:
|
70
|
-
if block.get("type") in ["child_database", "database", "linked_database"]:
|
71
|
-
skipped += 1
|
72
|
-
continue
|
73
85
|
|
74
|
-
|
75
|
-
|
86
|
+
block_type = block.get("type")
|
87
|
+
if skip_databases and block_type in [
|
88
|
+
"child_database",
|
89
|
+
"database",
|
90
|
+
"linked_database",
|
91
|
+
]:
|
92
|
+
return 0, 1
|
93
|
+
|
94
|
+
if skip_databases and await self.has_database_descendant(block["id"]):
|
95
|
+
return 0, 1
|
96
|
+
|
97
|
+
# Process children first
|
98
|
+
if block.get("has_children", False):
|
99
|
+
children_resp = await self._client.get(f"blocks/{block['id']}/children")
|
100
|
+
for child in children_resp.get("results", []):
|
101
|
+
child_deleted, child_skipped = await self.delete_block_with_children(
|
102
|
+
child, skip_databases
|
103
|
+
)
|
104
|
+
deleted += child_deleted
|
105
|
+
skipped += child_skipped
|
106
|
+
|
107
|
+
# Then delete the block itself
|
108
|
+
if await self._client.delete(f"blocks/{block['id']}"):
|
109
|
+
deleted += 1
|
110
|
+
|
111
|
+
return deleted, skipped
|
112
|
+
|
113
|
+
async def clear(self, skip_databases: bool = True) -> str:
|
114
|
+
"""
|
115
|
+
Clear the content of the page, optionally preserving databases.
|
116
|
+
"""
|
117
|
+
blocks_resp = await self._client.get(f"blocks/{self.page_id}/children")
|
118
|
+
results = blocks_resp.get("results", []) if blocks_resp else []
|
76
119
|
|
77
|
-
|
120
|
+
total_deleted = 0
|
121
|
+
total_skipped = 0
|
122
|
+
|
123
|
+
for block in results:
|
124
|
+
deleted, skipped = await self.delete_block_with_children(
|
125
|
+
block, skip_databases
|
126
|
+
)
|
127
|
+
total_deleted += deleted
|
128
|
+
total_skipped += skipped
|
129
|
+
|
130
|
+
return (
|
131
|
+
f"Deleted {total_deleted} blocks. Skipped {total_skipped} database blocks."
|
132
|
+
)
|
78
133
|
|
79
134
|
async def get_blocks(self) -> List[Dict[str, Any]]:
|
80
135
|
result = await self._client.get(f"blocks/{self.page_id}/children")
|
@@ -90,17 +145,33 @@ class PageContentManager(LoggingMixin):
|
|
90
145
|
return []
|
91
146
|
return result.get("results", [])
|
92
147
|
|
93
|
-
async def get_page_blocks_with_children(
|
94
|
-
|
148
|
+
async def get_page_blocks_with_children(
|
149
|
+
self, parent_id: Optional[str] = None
|
150
|
+
) -> List[Dict[str, Any]]:
|
151
|
+
blocks = (
|
152
|
+
await self.get_blocks()
|
153
|
+
if parent_id is None
|
154
|
+
else await self.get_block_children(parent_id)
|
155
|
+
)
|
156
|
+
if not blocks:
|
157
|
+
return []
|
158
|
+
|
95
159
|
for block in blocks:
|
96
|
-
if block.get("has_children"):
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
160
|
+
if not block.get("has_children"):
|
161
|
+
continue
|
162
|
+
|
163
|
+
block_id = block.get("id")
|
164
|
+
if not block_id:
|
165
|
+
continue
|
166
|
+
|
167
|
+
# Recursive call for nested blocks
|
168
|
+
children = await self.get_page_blocks_with_children(block_id)
|
169
|
+
if children:
|
170
|
+
block["children"] = children
|
171
|
+
|
102
172
|
return blocks
|
103
173
|
|
104
174
|
async def get_text(self) -> str:
|
105
175
|
blocks = await self.get_page_blocks_with_children()
|
176
|
+
print(json.dumps(blocks, indent=4))
|
106
177
|
return self._notion_to_markdown_converter.convert(blocks)
|
@@ -58,75 +58,85 @@ class NotionToMarkdownConverter:
|
|
58
58
|
return ""
|
59
59
|
|
60
60
|
block_markdown = self._block_registry.notion_to_markdown(block)
|
61
|
-
|
61
|
+
|
62
62
|
if not self._has_children(block):
|
63
63
|
return block_markdown
|
64
|
-
|
64
|
+
|
65
65
|
children_markdown = self.convert(block["children"])
|
66
66
|
if not children_markdown:
|
67
67
|
return block_markdown
|
68
|
-
|
68
|
+
|
69
69
|
block_type = block.get("type", "")
|
70
|
-
|
70
|
+
|
71
71
|
if block_type == "toggle":
|
72
72
|
return self._format_toggle_with_children(block_markdown, children_markdown)
|
73
|
-
|
73
|
+
|
74
74
|
if block_type in ["numbered_list_item", "bulleted_list_item"]:
|
75
|
-
return self._format_list_item_with_children(
|
76
|
-
|
75
|
+
return self._format_list_item_with_children(
|
76
|
+
block_markdown, children_markdown
|
77
|
+
)
|
78
|
+
|
77
79
|
if block_type in ["column_list", "column"]:
|
78
80
|
return children_markdown
|
79
|
-
|
80
|
-
return self._format_standard_block_with_children(
|
81
|
+
|
82
|
+
return self._format_standard_block_with_children(
|
83
|
+
block_markdown, children_markdown
|
84
|
+
)
|
81
85
|
|
82
86
|
def _has_children(self, block: Dict[str, Any]) -> bool:
|
83
87
|
"""
|
84
88
|
Check if block has children that need processing.
|
85
|
-
|
89
|
+
|
86
90
|
Args:
|
87
91
|
block: Notion block to check
|
88
|
-
|
92
|
+
|
89
93
|
Returns:
|
90
94
|
True if block has children to process
|
91
95
|
"""
|
92
96
|
return block.get("has_children", False) and "children" in block
|
93
97
|
|
94
|
-
def _format_toggle_with_children(
|
98
|
+
def _format_toggle_with_children(
|
99
|
+
self, toggle_markdown: str, children_markdown: str
|
100
|
+
) -> str:
|
95
101
|
"""
|
96
102
|
Format toggle block with its children content.
|
97
|
-
|
103
|
+
|
98
104
|
Args:
|
99
105
|
toggle_markdown: Markdown for the toggle itself
|
100
106
|
children_markdown: Markdown for toggle's children
|
101
|
-
|
107
|
+
|
102
108
|
Returns:
|
103
109
|
Formatted markdown with indented children
|
104
110
|
"""
|
105
111
|
indented_children = self._indent_text(children_markdown)
|
106
112
|
return f"{toggle_markdown}\n{indented_children}"
|
107
113
|
|
108
|
-
def _format_list_item_with_children(
|
114
|
+
def _format_list_item_with_children(
|
115
|
+
self, item_markdown: str, children_markdown: str
|
116
|
+
) -> str:
|
109
117
|
"""
|
110
118
|
Format list item with its children content.
|
111
|
-
|
119
|
+
|
112
120
|
Args:
|
113
121
|
item_markdown: Markdown for the list item itself
|
114
122
|
children_markdown: Markdown for item's children
|
115
|
-
|
123
|
+
|
116
124
|
Returns:
|
117
125
|
Formatted markdown with indented children
|
118
126
|
"""
|
119
127
|
indented_children = self._indent_text(children_markdown)
|
120
128
|
return f"{item_markdown}\n{indented_children}"
|
121
129
|
|
122
|
-
def _format_standard_block_with_children(
|
130
|
+
def _format_standard_block_with_children(
|
131
|
+
self, block_markdown: str, children_markdown: str
|
132
|
+
) -> str:
|
123
133
|
"""
|
124
134
|
Format standard block with its children content.
|
125
|
-
|
135
|
+
|
126
136
|
Args:
|
127
137
|
block_markdown: Markdown for the block itself
|
128
138
|
children_markdown: Markdown for block's children
|
129
|
-
|
139
|
+
|
130
140
|
Returns:
|
131
141
|
Formatted markdown with children after block
|
132
142
|
"""
|
@@ -135,11 +145,11 @@ class NotionToMarkdownConverter:
|
|
135
145
|
def _indent_text(self, text: str, spaces: int = 4) -> str:
|
136
146
|
"""
|
137
147
|
Indent each line of text with specified number of spaces.
|
138
|
-
|
148
|
+
|
139
149
|
Args:
|
140
150
|
text: Text to indent
|
141
151
|
spaces: Number of spaces to use for indentation
|
142
|
-
|
152
|
+
|
143
153
|
Returns:
|
144
154
|
Indented text
|
145
155
|
"""
|
@@ -149,27 +159,29 @@ class NotionToMarkdownConverter:
|
|
149
159
|
def extract_toggle_content(self, blocks: List[Dict[str, Any]]) -> str:
|
150
160
|
"""
|
151
161
|
Extract only the content of toggles from blocks.
|
152
|
-
|
162
|
+
|
153
163
|
Args:
|
154
164
|
blocks: List of Notion blocks
|
155
|
-
|
165
|
+
|
156
166
|
Returns:
|
157
167
|
Markdown text with toggle contents
|
158
168
|
"""
|
159
169
|
if not blocks:
|
160
170
|
return ""
|
161
|
-
|
171
|
+
|
162
172
|
toggle_contents = []
|
163
|
-
|
173
|
+
|
164
174
|
for block in blocks:
|
165
175
|
self._extract_toggle_content_recursive(block, toggle_contents)
|
166
|
-
|
176
|
+
|
167
177
|
return "\n".join(toggle_contents)
|
168
178
|
|
169
|
-
def _extract_toggle_content_recursive(
|
179
|
+
def _extract_toggle_content_recursive(
|
180
|
+
self, block: Dict[str, Any], result: List[str]
|
181
|
+
) -> None:
|
170
182
|
"""
|
171
183
|
Recursively extract toggle content from a block and its children.
|
172
|
-
|
184
|
+
|
173
185
|
Args:
|
174
186
|
block: Block to process
|
175
187
|
result: List to collect toggle content
|
@@ -177,7 +189,7 @@ class NotionToMarkdownConverter:
|
|
177
189
|
if self._is_toggle_with_children(block):
|
178
190
|
self._add_toggle_header_to_result(block, result)
|
179
191
|
self._add_toggle_children_to_result(block, result)
|
180
|
-
|
192
|
+
|
181
193
|
if self._has_children(block):
|
182
194
|
for child in block["children"]:
|
183
195
|
self._extract_toggle_content_recursive(child, result)
|
@@ -185,19 +197,21 @@ class NotionToMarkdownConverter:
|
|
185
197
|
def _is_toggle_with_children(self, block: Dict[str, Any]) -> bool:
|
186
198
|
"""
|
187
199
|
Check if block is a toggle with children.
|
188
|
-
|
200
|
+
|
189
201
|
Args:
|
190
202
|
block: Block to check
|
191
|
-
|
203
|
+
|
192
204
|
Returns:
|
193
205
|
True if block is a toggle with children
|
194
206
|
"""
|
195
207
|
return block.get("type") == "toggle" and "children" in block
|
196
208
|
|
197
|
-
def _add_toggle_header_to_result(
|
209
|
+
def _add_toggle_header_to_result(
|
210
|
+
self, block: Dict[str, Any], result: List[str]
|
211
|
+
) -> None:
|
198
212
|
"""
|
199
213
|
Add toggle header text to result list.
|
200
|
-
|
214
|
+
|
201
215
|
Args:
|
202
216
|
block: Toggle block
|
203
217
|
result: List to add header to
|
@@ -205,14 +219,16 @@ class NotionToMarkdownConverter:
|
|
205
219
|
toggle_text = self._extract_text_from_rich_text(
|
206
220
|
block.get("toggle", {}).get("rich_text", [])
|
207
221
|
)
|
208
|
-
|
222
|
+
|
209
223
|
if toggle_text:
|
210
224
|
result.append(f"### {toggle_text}")
|
211
225
|
|
212
|
-
def _add_toggle_children_to_result(
|
226
|
+
def _add_toggle_children_to_result(
|
227
|
+
self, block: Dict[str, Any], result: List[str]
|
228
|
+
) -> None:
|
213
229
|
"""
|
214
230
|
Add formatted toggle children to result list.
|
215
|
-
|
231
|
+
|
216
232
|
Args:
|
217
233
|
block: Toggle block with children
|
218
234
|
result: List to add children content to
|
@@ -221,25 +237,25 @@ class NotionToMarkdownConverter:
|
|
221
237
|
child_type = child.get("type")
|
222
238
|
if not (child_type and child_type in child):
|
223
239
|
continue
|
224
|
-
|
240
|
+
|
225
241
|
child_text = self._extract_text_from_rich_text(
|
226
242
|
child.get(child_type, {}).get("rich_text", [])
|
227
243
|
)
|
228
|
-
|
244
|
+
|
229
245
|
if child_text:
|
230
246
|
result.append(f"- {child_text}")
|
231
247
|
|
232
248
|
def _extract_text_from_rich_text(self, rich_text: List[Dict[str, Any]]) -> str:
|
233
249
|
"""
|
234
250
|
Extract plain text from Notion's rich text array.
|
235
|
-
|
251
|
+
|
236
252
|
Args:
|
237
253
|
rich_text: List of rich text objects
|
238
|
-
|
254
|
+
|
239
255
|
Returns:
|
240
256
|
Concatenated plain text
|
241
257
|
"""
|
242
258
|
if not rich_text:
|
243
259
|
return ""
|
244
|
-
|
245
|
-
return "".join([rt.get("plain_text", "") for rt in rich_text])
|
260
|
+
|
261
|
+
return "".join([rt.get("plain_text", "") for rt in rich_text])
|
@@ -1,5 +1,5 @@
|
|
1
1
|
notionary/__init__.py,sha256=sVqdwzMQcc9jf6FKi1qflilsx8rnWzluVhWewVi5gyI,717
|
2
|
-
notionary/notion_client.py,sha256=
|
2
|
+
notionary/notion_client.py,sha256=NPPK7zId6EaC-hQeFJ7geaiROGtZfmc8cVP2nQez5DU,6040
|
3
3
|
notionary/database/database_discovery.py,sha256=qDGFhXG9s-_6CXdRg8tMiwX4dvX7jLjgAUFPSNlYtlI,4506
|
4
4
|
notionary/database/database_info_service.py,sha256=Ig6gx8jUSPYORJvfgEV5kV6t72pZQsWU8HPMqd43B-o,1336
|
5
5
|
notionary/database/notion_database.py,sha256=RY5MlXNE5DVNWLC_Derljsz87ZMHkE-05Vgm80kvLxg,7250
|
@@ -7,7 +7,7 @@ notionary/database/notion_database_factory.py,sha256=Af57yaUHidD8TKJ8uyXOc2nnqHm
|
|
7
7
|
notionary/database/models/page_result.py,sha256=Vmm5_oYpYAkIIJVoTd1ZZGloeC3cmFLMYP255mAmtaw,233
|
8
8
|
notionary/elements/audio_element.py,sha256=XLARz5zlPPW_Qof6uhcYXFmyYzyS1fLdxdfsvh6GMOs,5589
|
9
9
|
notionary/elements/block_element_registry.py,sha256=2mEbCEIPRY15qk6NYcq9vf7Bq_5vuu1xbiJatoLFf7w,8530
|
10
|
-
notionary/elements/block_element_registry_builder.py,sha256=
|
10
|
+
notionary/elements/block_element_registry_builder.py,sha256=xfZWJaamYCyw-aIU8TXTcAncVJaLpN7ioJRLYQxTbCo,11529
|
11
11
|
notionary/elements/bookmark_element.py,sha256=83ciz2THxjeq7ofn-Xz9sG1Ifzm_gkIDmOo0A-pNsSo,8577
|
12
12
|
notionary/elements/callout_element.py,sha256=K8yk7nE3WP8nskJKbMunkRDFlhSCXrONmYS6hELN6QE,5932
|
13
13
|
notionary/elements/code_block_element.py,sha256=wbW_PfH6QBUFVfeELESuLiI5-GmLm2YUFP4xwFHgNV4,5173
|
@@ -17,7 +17,7 @@ notionary/elements/embed_element.py,sha256=LZjbSfwq0v8NGzwfUXpnGwvJ34IjYDwZzqyxV
|
|
17
17
|
notionary/elements/heading_element.py,sha256=GsfEg5XtohmtO8PBP9giezIg6pRWQ_CdPXjh7jOiytw,2756
|
18
18
|
notionary/elements/image_element.py,sha256=663H_FzE_bsovps3uCV12trNTmMAWBu5Ko1tSBNu2V4,4845
|
19
19
|
notionary/elements/list_element.py,sha256=-f4mPRPesqFYYXfiiqGpnADeAY2ZAF1sTDtkLcejvLg,4846
|
20
|
-
notionary/elements/mention_element.py,sha256=
|
20
|
+
notionary/elements/mention_element.py,sha256=N1AcE8daMs6sDJGidQ8vvtHj30Do3K7x-4lW2aAYthU,8274
|
21
21
|
notionary/elements/notion_block_element.py,sha256=lLRBDXhBeRaRzkbvdpYpr-U9nbkd62oVtqdSe-svT4c,1746
|
22
22
|
notionary/elements/paragraph_element.py,sha256=ULSPcwy_JbnKdQkMy-xMs_KtYI8k5uxh6b4EGMNldTk,2734
|
23
23
|
notionary/elements/qoute_element.py,sha256=3I2a7KboPF5QF6afu99HSIa62YUNfDJ6oaSDgyc9NjA,9041
|
@@ -31,9 +31,9 @@ notionary/exceptions/page_creation_exception.py,sha256=4v7IuZD6GsQLrqhDLriGjuG3M
|
|
31
31
|
notionary/page/markdown_to_notion_converter.py,sha256=wTkH7o6367IWBtSqBrldpKx4rxHli176QfWtAenyysQ,15067
|
32
32
|
notionary/page/notion_page.py,sha256=KIjVeiMJGWWxR6ty1uuNvMoQf2IoRmSUxwMdDIyOu40,17635
|
33
33
|
notionary/page/notion_page_factory.py,sha256=UUEZ-cyEWL0OMVPrgjc4vJdcplEa1bO2yHCYooACYC8,8189
|
34
|
-
notionary/page/notion_to_markdown_converter.py,sha256=
|
34
|
+
notionary/page/notion_to_markdown_converter.py,sha256=WHrESgMZPqnp3kufi0YB7Cyy8U1rwhJ0d4HHdBRfRdU,8053
|
35
35
|
notionary/page/content/notion_page_content_chunker.py,sha256=xRks74Dqec-De6-AVTxMPnXs-MSJBzSm1HfJfaHiKr8,3330
|
36
|
-
notionary/page/content/page_content_manager.py,sha256=
|
36
|
+
notionary/page/content/page_content_manager.py,sha256=jXWscZyiFNLGMiuJ8Da9P26q_9Hit9_G1j0rVDlOc5M,6224
|
37
37
|
notionary/page/metadata/metadata_editor.py,sha256=61uiw8oB25O8ePhytoJvZDetuof5sjPoM6aoHZGo4wc,4949
|
38
38
|
notionary/page/metadata/notion_icon_manager.py,sha256=ixZrWsHGVpmF05Ncy9LCt8vZlKAQHYFZW-2yI5JZZDI,1426
|
39
39
|
notionary/page/metadata/notion_page_cover_manager.py,sha256=qgQxQE-bx4oWjLFUQvpXD5GzO1Mx7w7htz1xC2BOqUg,1717
|
@@ -49,8 +49,8 @@ notionary/page/relations/relation_operation_result.py,sha256=NDxBzGntOxc_89ti-HG
|
|
49
49
|
notionary/util/logging_mixin.py,sha256=fKsx9t90bwvL74ZX3dU-sXdC4TZCQyO6qU9I8txkw_U,1369
|
50
50
|
notionary/util/page_id_utils.py,sha256=EYNMxgf-7ghzL5K8lKZBZfW7g5CsdY0Xuj4IYmU8RPk,1381
|
51
51
|
notionary/util/singleton_decorator.py,sha256=GTNMfIlVNRUVMw_c88xqd12-DcqZJjmyidN54yqiNVw,472
|
52
|
-
notionary-0.1.
|
53
|
-
notionary-0.1.
|
54
|
-
notionary-0.1.
|
55
|
-
notionary-0.1.
|
56
|
-
notionary-0.1.
|
52
|
+
notionary-0.1.15.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
|
53
|
+
notionary-0.1.15.dist-info/METADATA,sha256=uXefq6BdsjQnMAf34az8oWfMb2_6L068evq_G8UGWlE,6154
|
54
|
+
notionary-0.1.15.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
55
|
+
notionary-0.1.15.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
|
56
|
+
notionary-0.1.15.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|