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.
@@ -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
 
@@ -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
- Note: This element primarily supports Notion-to-Markdown conversion,
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 is a markdown mention."""
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
- if block.get("type") not in ["paragraph", "heading_1", "heading_2", "heading_3", "bulleted_list_item", "numbered_list_item"]:
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
- if text_item.get("type") == "mention":
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 mention to Notion mention block."""
43
- # This would be handled within rich text processing rather than as a standalone block
44
- return None
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 MentionElement._is_only_mentions(rich_text):
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
- """Process rich text array and convert any mentions to markdown format."""
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 == "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}]")
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, 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.",
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
- "Please review this with @user[d3dbbbd7-ec00-4204-94d9-e4a46e4928db]",
129
- "Deadline is @date[2023-12-31]"
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 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
- }
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
 
@@ -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 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."
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
- results = blocks.get("results", [])
64
- if not results:
65
- return "No content to delete."
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
- if await self._client.delete(f"blocks/{block['id']}"):
75
- deleted += 1
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
- return f"Deleted {deleted}/{len(results)} blocks. Skipped {skipped} database blocks."
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(self) -> List[Dict[str, Any]]:
94
- blocks = await self.get_blocks()
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
- 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
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(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
@@ -1,5 +1,5 @@
1
1
  notionary/__init__.py,sha256=sVqdwzMQcc9jf6FKi1qflilsx8rnWzluVhWewVi5gyI,717
2
- notionary/notion_client.py,sha256=gRyTBVuCZCQod6L18VxzKl38Tn5oXdE0gPl7yGG8anQ,4446
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=qPIGCAdzeZ3Xubxp1tysH8DqJMPZ7q1VEl0AbCsS01s,12107
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=vltFnUJs83tIezqevSrMzXhICZFWyA28zflWd0Dmr-E,5497
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=RJn8JCcoAOGAhVtZGc4NL-puBnMAAm3udY9w-qIDMqo,8231
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=Z0zYWLzcoY0wijWhQT5KU8uxRnZZMJ7MgDKa4U5iBY4,3932
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.14.dist-info/licenses/LICENSE,sha256=zOm3cRT1qD49eg7vgw95MI79rpUAZa1kRBFwL2FkAr8,1120
53
- notionary-0.1.14.dist-info/METADATA,sha256=aAgXmYYWBruH6azteEcg-N38clmDqszmgNVp3RWF0G0,6154
54
- notionary-0.1.14.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
55
- notionary-0.1.14.dist-info/top_level.txt,sha256=fhONa6BMHQXqthx5PanWGbPL0b8rdFqhrJKVLf_adSs,10
56
- notionary-0.1.14.dist-info/RECORD,,
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,,