notionary 0.1.14__tar.gz → 0.1.15__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 (63) hide show
  1. {notionary-0.1.14 → notionary-0.1.15}/PKG-INFO +1 -1
  2. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/block_element_registry_builder.py +1 -19
  3. notionary-0.1.15/notionary/elements/mention_element.py +227 -0
  4. {notionary-0.1.14 → notionary-0.1.15}/notionary/notion_client.py +55 -4
  5. notionary-0.1.15/notionary/page/content/page_content_manager.py +177 -0
  6. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/notion_to_markdown_converter.py +59 -43
  7. {notionary-0.1.14 → notionary-0.1.15}/notionary.egg-info/PKG-INFO +1 -1
  8. {notionary-0.1.14 → notionary-0.1.15}/setup.py +1 -1
  9. notionary-0.1.14/notionary/elements/mention_element.py +0 -135
  10. notionary-0.1.14/notionary/page/content/page_content_manager.py +0 -106
  11. {notionary-0.1.14 → notionary-0.1.15}/LICENSE +0 -0
  12. {notionary-0.1.14 → notionary-0.1.15}/README.md +0 -0
  13. {notionary-0.1.14 → notionary-0.1.15}/notionary/__init__.py +0 -0
  14. {notionary-0.1.14 → notionary-0.1.15}/notionary/database/database_discovery.py +0 -0
  15. {notionary-0.1.14 → notionary-0.1.15}/notionary/database/database_info_service.py +0 -0
  16. {notionary-0.1.14 → notionary-0.1.15}/notionary/database/models/page_result.py +0 -0
  17. {notionary-0.1.14 → notionary-0.1.15}/notionary/database/notion_database.py +0 -0
  18. {notionary-0.1.14 → notionary-0.1.15}/notionary/database/notion_database_factory.py +0 -0
  19. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/audio_element.py +0 -0
  20. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/block_element_registry.py +0 -0
  21. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/bookmark_element.py +0 -0
  22. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/callout_element.py +0 -0
  23. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/code_block_element.py +0 -0
  24. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/column_element.py +0 -0
  25. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/divider_element.py +0 -0
  26. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/embed_element.py +0 -0
  27. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/heading_element.py +0 -0
  28. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/image_element.py +0 -0
  29. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/list_element.py +0 -0
  30. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/notion_block_element.py +0 -0
  31. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/paragraph_element.py +0 -0
  32. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/qoute_element.py +0 -0
  33. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/table_element.py +0 -0
  34. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/text_inline_formatter.py +0 -0
  35. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/todo_lists.py +0 -0
  36. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/toggle_element.py +0 -0
  37. {notionary-0.1.14 → notionary-0.1.15}/notionary/elements/video_element.py +0 -0
  38. {notionary-0.1.14 → notionary-0.1.15}/notionary/exceptions/database_exceptions.py +0 -0
  39. {notionary-0.1.14 → notionary-0.1.15}/notionary/exceptions/page_creation_exception.py +0 -0
  40. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/content/notion_page_content_chunker.py +0 -0
  41. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/markdown_to_notion_converter.py +0 -0
  42. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/metadata/metadata_editor.py +0 -0
  43. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/metadata/notion_icon_manager.py +0 -0
  44. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/metadata/notion_page_cover_manager.py +0 -0
  45. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/notion_page.py +0 -0
  46. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/notion_page_factory.py +0 -0
  47. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/properites/database_property_service.py +0 -0
  48. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/properites/page_property_manager.py +0 -0
  49. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/properites/property_formatter.py +0 -0
  50. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/properites/property_operation_result.py +0 -0
  51. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/properites/property_value_extractor.py +0 -0
  52. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/relations/notion_page_relation_manager.py +0 -0
  53. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/relations/notion_page_title_resolver.py +0 -0
  54. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/relations/page_database_relation.py +0 -0
  55. {notionary-0.1.14 → notionary-0.1.15}/notionary/page/relations/relation_operation_result.py +0 -0
  56. {notionary-0.1.14 → notionary-0.1.15}/notionary/util/logging_mixin.py +0 -0
  57. {notionary-0.1.14 → notionary-0.1.15}/notionary/util/page_id_utils.py +0 -0
  58. {notionary-0.1.14 → notionary-0.1.15}/notionary/util/singleton_decorator.py +0 -0
  59. {notionary-0.1.14 → notionary-0.1.15}/notionary.egg-info/SOURCES.txt +0 -0
  60. {notionary-0.1.14 → notionary-0.1.15}/notionary.egg-info/dependency_links.txt +0 -0
  61. {notionary-0.1.14 → notionary-0.1.15}/notionary.egg-info/requires.txt +0 -0
  62. {notionary-0.1.14 → notionary-0.1.15}/notionary.egg-info/top_level.txt +0 -0
  63. {notionary-0.1.14 → notionary-0.1.15}/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.15
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)
@@ -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(block_markdown, children_markdown)
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(block_markdown, children_markdown)
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(self, toggle_markdown: str, children_markdown: str) -> str:
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(self, item_markdown: str, children_markdown: str) -> str:
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(self, block_markdown: str, children_markdown: str) -> str:
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(self, block: Dict[str, Any], result: List[str]) -> None:
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(self, block: Dict[str, Any], result: List[str]) -> None:
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(self, block: Dict[str, Any], result: List[str]) -> None:
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: notionary
3
- Version: 0.1.14
3
+ Version: 0.1.15
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
@@ -2,7 +2,7 @@ from setuptools import setup, find_namespace_packages
2
2
 
3
3
  setup(
4
4
  name="notionary",
5
- version="0.1.14",
5
+ version="0.1.15",
6
6
  packages=find_namespace_packages(include=["notionary*"]),
7
7
  install_requires=[
8
8
  "notion-client>=2.0.0",
@@ -1,135 +0,0 @@
1
- from typing import Dict, Any, Optional, List
2
- from typing_extensions import override
3
-
4
- from notionary.elements.notion_block_element import NotionBlockElement
5
-
6
- class MentionElement(NotionBlockElement):
7
- """
8
- Handles conversion between Markdown mentions and Notion mention elements.
9
-
10
- Markdown mention syntax:
11
- - @[page-id] - Mention a page by its ID
12
-
13
- Note: This element primarily supports Notion-to-Markdown conversion,
14
- as page mentions in Markdown would typically require knowing internal page IDs.
15
- """
16
-
17
- @override
18
- @staticmethod
19
- def match_markdown(text: str) -> bool:
20
- """Check if text is a markdown mention."""
21
- return False
22
-
23
- @override
24
- @staticmethod
25
- def match_notion(block: Dict[str, Any]) -> bool:
26
- """Check if block contains a mention."""
27
- if block.get("type") not in ["paragraph", "heading_1", "heading_2", "heading_3", "bulleted_list_item", "numbered_list_item"]:
28
- return False
29
-
30
- block_content = block.get(block.get("type"), {})
31
- rich_text = block_content.get("rich_text", [])
32
-
33
- for text_item in rich_text:
34
- if text_item.get("type") == "mention":
35
- return True
36
-
37
- return False
38
-
39
- @override
40
- @staticmethod
41
- def markdown_to_notion(text: str) -> Optional[Dict[str, Any]]:
42
- """Convert markdown mention to Notion mention block."""
43
- # This would be handled within rich text processing rather than as a standalone block
44
- return None
45
-
46
- @override
47
- @staticmethod
48
- def notion_to_markdown(block: Dict[str, Any]) -> Optional[str]:
49
- """Extract mentions from Notion block and convert to markdown format."""
50
- block_type = block.get("type")
51
- if not block_type or block_type not in block:
52
- return None
53
-
54
- block_content = block.get(block_type, {})
55
- rich_text = block_content.get("rich_text", [])
56
-
57
- processed_text = MentionElement._process_rich_text_with_mentions(rich_text)
58
-
59
- if MentionElement._is_only_mentions(rich_text):
60
- return processed_text
61
-
62
- return None
63
-
64
- @staticmethod
65
- def _process_rich_text_with_mentions(rich_text: List[Dict[str, Any]]) -> str:
66
- """Process rich text array and convert any mentions to markdown format."""
67
- result = []
68
-
69
- for item in rich_text:
70
- if item.get("type") == "mention":
71
- mention = item.get("mention", {})
72
- mention_type = mention.get("type")
73
-
74
- if mention_type == "page":
75
- page_id = mention.get("page", {}).get("id", "")
76
- result.append(f"@[{page_id}]")
77
- elif mention_type == "user":
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}]")
86
- else:
87
- # Unknown mention type, fallback to plain text if available
88
- result.append(item.get("plain_text", "@[unknown]"))
89
- else:
90
- # Regular text item
91
- result.append(item.get("plain_text", ""))
92
-
93
- 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
-
107
- @override
108
- @staticmethod
109
- def is_multiline() -> bool:
110
- return False
111
-
112
- @classmethod
113
- def get_llm_prompt_content(cls) -> dict:
114
- """
115
- Returns a dictionary with all information needed for LLM prompts about this element.
116
- """
117
- return {
118
- "description": "References to Notion pages, users, databases, or dates within text content.",
119
- "when_to_use": "Mentions are typically part of rich text content rather than standalone elements. They're used to link to other Notion content or users.",
120
- "syntax": [
121
- "@[page-id] - Reference to a Notion page",
122
- "@user[user-id] - Reference to a Notion user",
123
- "@date[YYYY-MM-DD] - Reference to a date",
124
- "@db[database-id] - Reference to a Notion database"
125
- ],
126
- "examples": [
127
- "Check the meeting notes at @[1a6389d5-7bd3-80c5-9a87-e90b034989d0]",
128
- "Please review this with @user[d3dbbbd7-ec00-4204-94d9-e4a46e4928db]",
129
- "Deadline is @date[2023-12-31]"
130
- ],
131
- "limitations": [
132
- "Mentions are typically created through Notion's UI rather than direct markdown input",
133
- "When converting Notion content to markdown, mentions are represented with their internal IDs"
134
- ]
135
- }
@@ -1,106 +0,0 @@
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
-
43
- # Fix any blocks that exceed Notion's content length limits
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 clear(self) -> str:
59
- blocks = await self._client.get(f"blocks/{self.page_id}/children")
60
- if not blocks:
61
- return "No content to delete."
62
-
63
- results = blocks.get("results", [])
64
- if not results:
65
- return "No content to delete."
66
-
67
- deleted = 0
68
- skipped = 0
69
- for block in results:
70
- if block.get("type") in ["child_database", "database", "linked_database"]:
71
- skipped += 1
72
- continue
73
-
74
- if await self._client.delete(f"blocks/{block['id']}"):
75
- deleted += 1
76
-
77
- return f"Deleted {deleted}/{len(results)} blocks. Skipped {skipped} database blocks."
78
-
79
- async def get_blocks(self) -> List[Dict[str, Any]]:
80
- result = await self._client.get(f"blocks/{self.page_id}/children")
81
- if not result:
82
- self.logger.error("Error retrieving page content: %s", result.error)
83
- return []
84
- return result.get("results", [])
85
-
86
- async def get_block_children(self, block_id: str) -> List[Dict[str, Any]]:
87
- result = await self._client.get(f"blocks/{block_id}/children")
88
- if not result:
89
- self.logger.error("Error retrieving block children: %s", result.error)
90
- return []
91
- return result.get("results", [])
92
-
93
- async def get_page_blocks_with_children(self) -> List[Dict[str, Any]]:
94
- blocks = await self.get_blocks()
95
- for block in blocks:
96
- if block.get("has_children"):
97
- block_id = block.get("id")
98
- if block_id:
99
- children = await self.get_block_children(block_id)
100
- if children:
101
- block["children"] = children
102
- return blocks
103
-
104
- async def get_text(self) -> str:
105
- blocks = await self.get_page_blocks_with_children()
106
- return self._notion_to_markdown_converter.convert(blocks)
File without changes
File without changes
File without changes