notionary 0.1.13__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/__init__.py +2 -2
- notionary/{converters/elements → elements}/audio_element.py +1 -1
- notionary/{converters/registry → elements}/block_element_registry.py +2 -5
- notionary/elements/block_element_registry_builder.py +383 -0
- notionary/{converters/elements → elements}/bookmark_element.py +1 -1
- notionary/{converters/elements → elements}/callout_element.py +2 -2
- notionary/{converters/elements → elements}/code_block_element.py +1 -1
- notionary/{converters/elements → elements}/column_element.py +1 -1
- notionary/{converters/elements → elements}/divider_element.py +1 -1
- notionary/{converters/elements → elements}/embed_element.py +1 -1
- notionary/{converters/elements → elements}/heading_element.py +2 -2
- notionary/{converters/elements → elements}/image_element.py +1 -1
- notionary/{converters/elements → elements}/list_element.py +2 -2
- notionary/elements/mention_element.py +227 -0
- notionary/{converters/elements → elements}/paragraph_element.py +2 -2
- notionary/{converters/elements → elements}/qoute_element.py +1 -1
- notionary/{converters/elements → elements}/table_element.py +2 -2
- notionary/{converters/elements → elements}/todo_lists.py +2 -2
- notionary/{converters/elements → elements}/toggle_element.py +1 -1
- notionary/{converters/elements → elements}/video_element.py +1 -1
- notionary/notion_client.py +55 -5
- notionary/page/content/page_content_manager.py +98 -26
- notionary/{converters → page}/markdown_to_notion_converter.py +2 -4
- notionary/page/notion_page.py +23 -5
- notionary/page/notion_page_factory.py +1 -15
- notionary/page/notion_to_markdown_converter.py +261 -0
- {notionary-0.1.13.dist-info → notionary-0.1.15.dist-info}/METADATA +1 -1
- notionary-0.1.15.dist-info/RECORD +56 -0
- notionary/converters/__init__.py +0 -50
- notionary/converters/notion_to_markdown_converter.py +0 -45
- notionary/converters/registry/block_element_registry_builder.py +0 -284
- notionary-0.1.13.dist-info/RECORD +0 -56
- /notionary/{converters/elements → elements}/notion_block_element.py +0 -0
- /notionary/{converters/elements → elements}/text_inline_formatter.py +0 -0
- {notionary-0.1.13.dist-info → notionary-0.1.15.dist-info}/WHEEL +0 -0
- {notionary-0.1.13.dist-info → notionary-0.1.15.dist-info}/licenses/LICENSE +0 -0
- {notionary-0.1.13.dist-info → notionary-0.1.15.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,227 @@
|
|
1
|
+
import re
|
2
|
+
from typing import Dict, Any, Optional, List
|
3
|
+
from typing_extensions import override
|
4
|
+
|
5
|
+
from notionary.elements.notion_block_element import NotionBlockElement
|
6
|
+
|
7
|
+
|
8
|
+
class MentionElement(NotionBlockElement):
|
9
|
+
"""
|
10
|
+
Handles conversion between Markdown mentions and Notion mention elements.
|
11
|
+
|
12
|
+
Markdown mention syntax:
|
13
|
+
- @[page-id] - Mention a page by its ID
|
14
|
+
- @date[YYYY-MM-DD] - Mention a date
|
15
|
+
- @db[database-id] - Mention a database by its ID
|
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
|
+
|
48
|
+
@override
|
49
|
+
@staticmethod
|
50
|
+
def match_markdown(text: str) -> bool:
|
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
|
55
|
+
return False
|
56
|
+
|
57
|
+
@override
|
58
|
+
@staticmethod
|
59
|
+
def match_notion(block: Dict[str, Any]) -> bool:
|
60
|
+
"""Check if block contains a mention."""
|
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:
|
71
|
+
return False
|
72
|
+
|
73
|
+
block_content = block.get(block.get("type"), {})
|
74
|
+
rich_text = block_content.get("rich_text", [])
|
75
|
+
|
76
|
+
return any(text_item.get("type") == "mention" for text_item in rich_text)
|
77
|
+
|
78
|
+
@override
|
79
|
+
@staticmethod
|
80
|
+
def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
|
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
|
+
|
164
|
+
@override
|
165
|
+
@staticmethod
|
166
|
+
def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
|
167
|
+
"""Extract mentions from Notion block and convert to markdown format."""
|
168
|
+
block_type = block.get("type")
|
169
|
+
if not block_type or block_type not in block:
|
170
|
+
return None
|
171
|
+
|
172
|
+
block_content = block.get(block_type, {})
|
173
|
+
rich_text = block_content.get("rich_text", [])
|
174
|
+
|
175
|
+
processed_text = MentionElement._process_rich_text_with_mentions(rich_text)
|
176
|
+
|
177
|
+
if processed_text:
|
178
|
+
return processed_text
|
179
|
+
|
180
|
+
return None
|
181
|
+
|
182
|
+
@staticmethod
|
183
|
+
def _process_rich_text_with_mentions(rich_text: List[Dict[str, Any]]) -> str:
|
184
|
+
"""Convert rich text with mentions to markdown string."""
|
185
|
+
result = []
|
186
|
+
|
187
|
+
for item in rich_text:
|
188
|
+
if item.get("type") == "mention":
|
189
|
+
mention = item.get("mention", {})
|
190
|
+
mention_type = mention.get("type")
|
191
|
+
|
192
|
+
if mention_type in MentionElement.MENTION_TYPES:
|
193
|
+
result.append(
|
194
|
+
MentionElement.MENTION_TYPES[mention_type]["to_markdown"](item)
|
195
|
+
)
|
196
|
+
else:
|
197
|
+
result.append(item.get("plain_text", "@[unknown]"))
|
198
|
+
else:
|
199
|
+
result.append(item.get("plain_text", ""))
|
200
|
+
|
201
|
+
return "".join(result)
|
202
|
+
|
203
|
+
@override
|
204
|
+
@staticmethod
|
205
|
+
def is_multiline() -> bool:
|
206
|
+
return False
|
207
|
+
|
208
|
+
@classmethod
|
209
|
+
def get_llm_prompt_content(cls) -> dict:
|
210
|
+
"""Information about this element for LLM-based processing."""
|
211
|
+
return {
|
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.",
|
214
|
+
"syntax": [
|
215
|
+
"@[page-id] - Reference to a Notion page",
|
216
|
+
"@date[YYYY-MM-DD] - Reference to a date",
|
217
|
+
"@db[database-id] - Reference to a Notion database",
|
218
|
+
],
|
219
|
+
"examples": [
|
220
|
+
"Check the meeting notes at @[1a6389d5-7bd3-80c5-9a87-e90b034989d0]",
|
221
|
+
"Deadline is @date[2023-12-31]",
|
222
|
+
"Use the structure in @db[1a6389d5-7bd3-80e9-b199-000cfb3fa0b3]",
|
223
|
+
],
|
224
|
+
"limitations": [
|
225
|
+
"Mentions require knowing the internal IDs of the pages or databases you want to reference"
|
226
|
+
],
|
227
|
+
}
|
@@ -1,8 +1,8 @@
|
|
1
1
|
from typing import Dict, Any, Optional
|
2
2
|
from typing_extensions import override
|
3
3
|
|
4
|
-
from notionary.
|
5
|
-
from notionary.
|
4
|
+
from notionary.elements.notion_block_element import NotionBlockElement
|
5
|
+
from notionary.elements.text_inline_formatter import TextInlineFormatter
|
6
6
|
|
7
7
|
|
8
8
|
class ParagraphElement(NotionBlockElement):
|
@@ -2,7 +2,7 @@ import re
|
|
2
2
|
from typing import Dict, Any, Optional, List, Tuple
|
3
3
|
from typing_extensions import override
|
4
4
|
|
5
|
-
from notionary.
|
5
|
+
from notionary.elements.notion_block_element import NotionBlockElement
|
6
6
|
|
7
7
|
|
8
8
|
class QuoteElement(NotionBlockElement):
|
@@ -3,8 +3,8 @@
|
|
3
3
|
from typing import Dict, Any, Optional, List, Tuple
|
4
4
|
from typing_extensions import override
|
5
5
|
import re
|
6
|
-
from notionary.
|
7
|
-
from notionary.
|
6
|
+
from notionary.elements.notion_block_element import NotionBlockElement
|
7
|
+
from notionary.elements.text_inline_formatter import TextInlineFormatter
|
8
8
|
|
9
9
|
|
10
10
|
class TableElement(NotionBlockElement):
|
@@ -1,8 +1,8 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional
|
3
3
|
from typing_extensions import override
|
4
|
-
from notionary.
|
5
|
-
from notionary.
|
4
|
+
from notionary.elements.notion_block_element import NotionBlockElement
|
5
|
+
from notionary.elements.text_inline_formatter import TextInlineFormatter
|
6
6
|
|
7
7
|
|
8
8
|
class TodoElement(NotionBlockElement):
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Dict, Any, Optional, List, Tuple, Callable
|
3
3
|
|
4
|
-
from notionary.
|
4
|
+
from notionary.elements.notion_block_element import NotionBlockElement
|
5
5
|
|
6
6
|
|
7
7
|
class ToggleElement(NotionBlockElement):
|
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
|
|
@@ -124,7 +175,6 @@ class NotionClient(LoggingMixin):
|
|
124
175
|
)
|
125
176
|
return
|
126
177
|
|
127
|
-
# Versuche, Cleanup Task zu erstellen
|
128
178
|
loop.create_task(self.close())
|
129
179
|
self.logger.debug("Created cleanup task for NotionClient")
|
130
180
|
except RuntimeError:
|
@@ -1,12 +1,13 @@
|
|
1
|
+
import json
|
1
2
|
from typing import Any, Dict, List, Optional
|
2
3
|
|
4
|
+
from notionary.elements.block_element_registry import BlockElementRegistry
|
3
5
|
from notionary.notion_client import NotionClient
|
4
|
-
from notionary.converters.registry.block_element_registry import BlockElementRegistry
|
5
6
|
|
6
|
-
from notionary.
|
7
|
+
from notionary.page.markdown_to_notion_converter import (
|
7
8
|
MarkdownToNotionConverter,
|
8
9
|
)
|
9
|
-
from notionary.
|
10
|
+
from notionary.page.notion_to_markdown_converter import (
|
10
11
|
NotionToMarkdownConverter,
|
11
12
|
)
|
12
13
|
from notionary.page.content.notion_page_content_chunker import (
|
@@ -38,8 +39,8 @@ class PageContentManager(LoggingMixin):
|
|
38
39
|
"""
|
39
40
|
try:
|
40
41
|
blocks = self._markdown_to_notion_converter.convert(markdown_text)
|
42
|
+
print(json.dumps(blocks, indent=4))
|
41
43
|
|
42
|
-
# Fix any blocks that exceed Notion's content length limits
|
43
44
|
fixed_blocks = self._chunker.fix_blocks_content_length(blocks)
|
44
45
|
|
45
46
|
result = await self._client.patch(
|
@@ -54,26 +55,81 @@ class PageContentManager(LoggingMixin):
|
|
54
55
|
self.logger.error("Error appending markdown: %s", str(e))
|
55
56
|
raise
|
56
57
|
|
57
|
-
async def
|
58
|
-
|
59
|
-
if
|
60
|
-
|
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
|
61
69
|
|
62
|
-
|
63
|
-
|
64
|
-
|
70
|
+
if child.get("has_children", False):
|
71
|
+
if await self.has_database_descendant(child["id"]):
|
72
|
+
return True
|
65
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
|
+
"""
|
66
83
|
deleted = 0
|
67
84
|
skipped = 0
|
68
|
-
for block in results:
|
69
|
-
if block.get("type") in ["child_database", "database", "linked_database"]:
|
70
|
-
skipped += 1
|
71
|
-
continue
|
72
85
|
|
73
|
-
|
74
|
-
|
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 []
|
75
119
|
|
76
|
-
|
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
|
+
)
|
77
133
|
|
78
134
|
async def get_blocks(self) -> List[Dict[str, Any]]:
|
79
135
|
result = await self._client.get(f"blocks/{self.page_id}/children")
|
@@ -89,17 +145,33 @@ class PageContentManager(LoggingMixin):
|
|
89
145
|
return []
|
90
146
|
return result.get("results", [])
|
91
147
|
|
92
|
-
async def get_page_blocks_with_children(
|
93
|
-
|
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
|
+
|
94
159
|
for block in blocks:
|
95
|
-
if block.get("has_children"):
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
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
|
+
|
101
172
|
return blocks
|
102
173
|
|
103
174
|
async def get_text(self) -> str:
|
104
175
|
blocks = await self.get_page_blocks_with_children()
|
176
|
+
print(json.dumps(blocks, indent=4))
|
105
177
|
return self._notion_to_markdown_converter.convert(blocks)
|
@@ -1,9 +1,7 @@
|
|
1
1
|
from typing import Dict, Any, List, Optional, Tuple
|
2
2
|
|
3
|
-
from notionary.
|
4
|
-
|
5
|
-
)
|
6
|
-
from notionary.converters.registry.block_element_registry_builder import (
|
3
|
+
from notionary.elements.block_element_registry import BlockElementRegistry
|
4
|
+
from notionary.elements.block_element_registry_builder import (
|
7
5
|
BlockElementRegistryBuilder,
|
8
6
|
)
|
9
7
|
|
notionary/page/notion_page.py
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
import re
|
2
2
|
from typing import Any, Dict, List, Optional, Union
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
from notionary.converters.registry.block_element_registry_builder import (
|
3
|
+
|
4
|
+
from notionary.elements.block_element_registry import BlockElementRegistry
|
5
|
+
from notionary.elements.block_element_registry_builder import (
|
7
6
|
BlockElementRegistryBuilder,
|
8
7
|
)
|
9
8
|
from notionary.notion_client import NotionClient
|
@@ -412,6 +411,25 @@ class NotionPage(LoggingMixin):
|
|
412
411
|
"""
|
413
412
|
return await self._relation_manager.get_all_relations()
|
414
413
|
|
414
|
+
async def get_last_edited_time(self) -> str:
|
415
|
+
"""
|
416
|
+
Get the timestamp when the page was last edited.
|
417
|
+
|
418
|
+
Returns:
|
419
|
+
str: ISO 8601 formatted timestamp string of when the page was last edited.
|
420
|
+
"""
|
421
|
+
try:
|
422
|
+
page_data = await self._client.get_page(self._page_id)
|
423
|
+
if "last_edited_time" in page_data:
|
424
|
+
return page_data["last_edited_time"]
|
425
|
+
|
426
|
+
self.logger.warning("last_edited_time not found in page data")
|
427
|
+
return ""
|
428
|
+
|
429
|
+
except Exception as e:
|
430
|
+
self.logger.error("Error retrieving last edited time: %s", str(e))
|
431
|
+
return ""
|
432
|
+
|
415
433
|
async def _load_page_title(self) -> str:
|
416
434
|
"""
|
417
435
|
Load the page title from Notion API if not already loaded.
|
@@ -483,7 +501,7 @@ class NotionPage(LoggingMixin):
|
|
483
501
|
clean_id = self._page_id.replace("-", "")
|
484
502
|
|
485
503
|
return f"https://www.notion.so/{url_title}{clean_id}"
|
486
|
-
|
504
|
+
|
487
505
|
async def _get_db_property_service(self) -> Optional[DatabasePropertyService]:
|
488
506
|
"""
|
489
507
|
Gets the database property service, initializing it if necessary.
|
@@ -176,9 +176,7 @@ class NotionPageFactory(LoggingMixin):
|
|
176
176
|
best_score,
|
177
177
|
)
|
178
178
|
|
179
|
-
page = NotionPage(
|
180
|
-
page_id=page_id, title=matched_name, token=token
|
181
|
-
)
|
179
|
+
page = NotionPage(page_id=page_id, title=matched_name, token=token)
|
182
180
|
|
183
181
|
logger.info("Successfully created page instance for '%s'", matched_name)
|
184
182
|
await client.close()
|
@@ -242,15 +240,3 @@ class NotionPageFactory(LoggingMixin):
|
|
242
240
|
text_parts.append(text_obj["plain_text"])
|
243
241
|
|
244
242
|
return "".join(text_parts)
|
245
|
-
|
246
|
-
|
247
|
-
async def demo():
|
248
|
-
clipboard = await NotionPageFactory.from_page_name("Jarvis Clipboard")
|
249
|
-
icon = await clipboard.get_icon()
|
250
|
-
print(f"Icon: {icon}")
|
251
|
-
|
252
|
-
|
253
|
-
if __name__ == "__main__":
|
254
|
-
import asyncio
|
255
|
-
|
256
|
-
asyncio.run(demo())
|