notionary 0.1.14__tar.gz → 0.1.16__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. {notionary-0.1.14 → notionary-0.1.16}/PKG-INFO +1 -1
  2. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/block_element_registry_builder.py +1 -19
  3. notionary-0.1.16/notionary/elements/mention_element.py +227 -0
  4. {notionary-0.1.14 → notionary-0.1.16}/notionary/notion_client.py +55 -4
  5. notionary-0.1.16/notionary/page/content/page_content_manager.py +177 -0
  6. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/notion_to_markdown_converter.py +59 -43
  7. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/relations/notion_page_relation_manager.py +29 -14
  8. notionary-0.1.16/notionary/page/relations/notion_page_title_resolver.py +87 -0
  9. {notionary-0.1.14 → notionary-0.1.16}/notionary.egg-info/PKG-INFO +1 -1
  10. {notionary-0.1.14 → notionary-0.1.16}/setup.py +1 -1
  11. notionary-0.1.14/notionary/elements/mention_element.py +0 -135
  12. notionary-0.1.14/notionary/page/content/page_content_manager.py +0 -106
  13. notionary-0.1.14/notionary/page/relations/notion_page_title_resolver.py +0 -43
  14. {notionary-0.1.14 → notionary-0.1.16}/LICENSE +0 -0
  15. {notionary-0.1.14 → notionary-0.1.16}/README.md +0 -0
  16. {notionary-0.1.14 → notionary-0.1.16}/notionary/__init__.py +0 -0
  17. {notionary-0.1.14 → notionary-0.1.16}/notionary/database/database_discovery.py +0 -0
  18. {notionary-0.1.14 → notionary-0.1.16}/notionary/database/database_info_service.py +0 -0
  19. {notionary-0.1.14 → notionary-0.1.16}/notionary/database/models/page_result.py +0 -0
  20. {notionary-0.1.14 → notionary-0.1.16}/notionary/database/notion_database.py +0 -0
  21. {notionary-0.1.14 → notionary-0.1.16}/notionary/database/notion_database_factory.py +0 -0
  22. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/audio_element.py +0 -0
  23. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/block_element_registry.py +0 -0
  24. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/bookmark_element.py +0 -0
  25. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/callout_element.py +0 -0
  26. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/code_block_element.py +0 -0
  27. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/column_element.py +0 -0
  28. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/divider_element.py +0 -0
  29. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/embed_element.py +0 -0
  30. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/heading_element.py +0 -0
  31. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/image_element.py +0 -0
  32. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/list_element.py +0 -0
  33. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/notion_block_element.py +0 -0
  34. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/paragraph_element.py +0 -0
  35. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/qoute_element.py +0 -0
  36. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/table_element.py +0 -0
  37. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/text_inline_formatter.py +0 -0
  38. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/todo_lists.py +0 -0
  39. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/toggle_element.py +0 -0
  40. {notionary-0.1.14 → notionary-0.1.16}/notionary/elements/video_element.py +0 -0
  41. {notionary-0.1.14 → notionary-0.1.16}/notionary/exceptions/database_exceptions.py +0 -0
  42. {notionary-0.1.14 → notionary-0.1.16}/notionary/exceptions/page_creation_exception.py +0 -0
  43. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/content/notion_page_content_chunker.py +0 -0
  44. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/markdown_to_notion_converter.py +0 -0
  45. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/metadata/metadata_editor.py +0 -0
  46. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/metadata/notion_icon_manager.py +0 -0
  47. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/metadata/notion_page_cover_manager.py +0 -0
  48. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/notion_page.py +0 -0
  49. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/notion_page_factory.py +0 -0
  50. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/properites/database_property_service.py +0 -0
  51. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/properites/page_property_manager.py +0 -0
  52. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/properites/property_formatter.py +0 -0
  53. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/properites/property_operation_result.py +0 -0
  54. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/properites/property_value_extractor.py +0 -0
  55. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/relations/page_database_relation.py +0 -0
  56. {notionary-0.1.14 → notionary-0.1.16}/notionary/page/relations/relation_operation_result.py +0 -0
  57. {notionary-0.1.14 → notionary-0.1.16}/notionary/util/logging_mixin.py +0 -0
  58. {notionary-0.1.14 → notionary-0.1.16}/notionary/util/page_id_utils.py +0 -0
  59. {notionary-0.1.14 → notionary-0.1.16}/notionary/util/singleton_decorator.py +0 -0
  60. {notionary-0.1.14 → notionary-0.1.16}/notionary.egg-info/SOURCES.txt +0 -0
  61. {notionary-0.1.14 → notionary-0.1.16}/notionary.egg-info/dependency_links.txt +0 -0
  62. {notionary-0.1.14 → notionary-0.1.16}/notionary.egg-info/requires.txt +0 -0
  63. {notionary-0.1.14 → notionary-0.1.16}/notionary.egg-info/top_level.txt +0 -0
  64. {notionary-0.1.14 → notionary-0.1.16}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionary
3
- Version: 0.1.14
3
+ Version: 0.1.16
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
@@ -155,7 +155,7 @@ class BlockElementRegistryBuilder:
155
155
  Returns:
156
156
  Self for method chaining
157
157
  """
158
- return self.add_element(element_class) # add_element already handles this logic
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
 
@@ -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
+ }
@@ -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
- return await self.get(f"pages/{page_id}")
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
 
@@ -0,0 +1,177 @@
1
+ import json
2
+ from typing import Any, Dict, List, Optional
3
+
4
+ from notionary.elements.block_element_registry import BlockElementRegistry
5
+ from notionary.notion_client import NotionClient
6
+
7
+ from notionary.page.markdown_to_notion_converter import (
8
+ MarkdownToNotionConverter,
9
+ )
10
+ from notionary.page.notion_to_markdown_converter import (
11
+ NotionToMarkdownConverter,
12
+ )
13
+ from notionary.page.content.notion_page_content_chunker import (
14
+ NotionPageContentChunker,
15
+ )
16
+ from notionary.util.logging_mixin import LoggingMixin
17
+
18
+
19
+ class PageContentManager(LoggingMixin):
20
+ def __init__(
21
+ self,
22
+ page_id: str,
23
+ client: NotionClient,
24
+ block_registry: Optional[BlockElementRegistry] = None,
25
+ ):
26
+ self.page_id = page_id
27
+ self._client = client
28
+ self._markdown_to_notion_converter = MarkdownToNotionConverter(
29
+ block_registry=block_registry
30
+ )
31
+ self._notion_to_markdown_converter = NotionToMarkdownConverter(
32
+ block_registry=block_registry
33
+ )
34
+ self._chunker = NotionPageContentChunker()
35
+
36
+ async def append_markdown(self, markdown_text: str) -> str:
37
+ """
38
+ Append markdown text to a Notion page, automatically handling content length limits.
39
+ """
40
+ try:
41
+ blocks = self._markdown_to_notion_converter.convert(markdown_text)
42
+ print(json.dumps(blocks, indent=4))
43
+
44
+ fixed_blocks = self._chunker.fix_blocks_content_length(blocks)
45
+
46
+ result = await self._client.patch(
47
+ f"blocks/{self.page_id}/children", {"children": fixed_blocks}
48
+ )
49
+ return (
50
+ "Successfully added text to the page."
51
+ if result
52
+ else "Failed to add text."
53
+ )
54
+ except Exception as e:
55
+ self.logger.error("Error appending markdown: %s", str(e))
56
+ raise
57
+
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
69
+
70
+ if child.get("has_children", False):
71
+ if await self.has_database_descendant(child["id"]):
72
+ return True
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
+ """
83
+ deleted = 0
84
+ skipped = 0
85
+
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 []
119
+
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
+ )
133
+
134
+ async def get_blocks(self) -> List[Dict[str, Any]]:
135
+ result = await self._client.get(f"blocks/{self.page_id}/children")
136
+ if not result:
137
+ self.logger.error("Error retrieving page content: %s", result.error)
138
+ return []
139
+ return result.get("results", [])
140
+
141
+ async def get_block_children(self, block_id: str) -> List[Dict[str, Any]]:
142
+ result = await self._client.get(f"blocks/{block_id}/children")
143
+ if not result:
144
+ self.logger.error("Error retrieving block children: %s", result.error)
145
+ return []
146
+ return result.get("results", [])
147
+
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
+
159
+ for block in blocks:
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
+
172
+ return blocks
173
+
174
+ async def get_text(self) -> str:
175
+ blocks = await self.get_page_blocks_with_children()
176
+ print(json.dumps(blocks, indent=4))
177
+ return self._notion_to_markdown_converter.convert(blocks)