notionary 0.2.17__py3-none-any.whl → 0.2.19__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notionary/__init__.py +3 -2
- notionary/blocks/__init__.py +54 -25
- notionary/blocks/audio/__init__.py +7 -0
- notionary/blocks/audio/audio_element.py +152 -0
- notionary/blocks/audio/audio_markdown_node.py +29 -0
- notionary/blocks/audio/audio_models.py +59 -0
- notionary/blocks/bookmark/__init__.py +7 -0
- notionary/blocks/{bookmark_element.py → bookmark/bookmark_element.py} +20 -65
- notionary/blocks/bookmark/bookmark_markdown_node.py +43 -0
- notionary/blocks/bookmark/bookmark_models.py +0 -0
- notionary/blocks/bulleted_list/__init__.py +7 -0
- notionary/blocks/{bulleted_list_element.py → bulleted_list/bulleted_list_element.py} +7 -3
- notionary/blocks/bulleted_list/bulleted_list_markdown_node.py +33 -0
- notionary/blocks/bulleted_list/bulleted_list_models.py +0 -0
- notionary/blocks/callout/__init__.py +7 -0
- notionary/blocks/callout/callout_element.py +132 -0
- notionary/blocks/callout/callout_markdown_node.py +31 -0
- notionary/blocks/callout/callout_models.py +0 -0
- notionary/blocks/code/__init__.py +7 -0
- notionary/blocks/{code_block_element.py → code/code_element.py} +72 -40
- notionary/blocks/code/code_markdown_node.py +43 -0
- notionary/blocks/code/code_models.py +0 -0
- notionary/blocks/column/__init__.py +5 -0
- notionary/blocks/{column_element.py → column/column_element.py} +24 -55
- notionary/blocks/column/column_models.py +0 -0
- notionary/blocks/divider/__init__.py +7 -0
- notionary/blocks/{divider_element.py → divider/divider_element.py} +11 -3
- notionary/blocks/divider/divider_markdown_node.py +24 -0
- notionary/blocks/divider/divider_models.py +0 -0
- notionary/blocks/document/__init__.py +7 -0
- notionary/blocks/document/document_element.py +102 -0
- notionary/blocks/document/document_markdown_node.py +31 -0
- notionary/blocks/document/document_models.py +0 -0
- notionary/blocks/embed/__init__.py +7 -0
- notionary/blocks/{embed_element.py → embed/embed_element.py} +50 -32
- notionary/blocks/embed/embed_markdown_node.py +30 -0
- notionary/blocks/embed/embed_models.py +0 -0
- notionary/blocks/heading/__init__.py +7 -0
- notionary/blocks/{heading_element.py → heading/heading_element.py} +25 -17
- notionary/blocks/heading/heading_markdown_node.py +29 -0
- notionary/blocks/heading/heading_models.py +0 -0
- notionary/blocks/image/__init__.py +7 -0
- notionary/blocks/{image_element.py → image/image_element.py} +62 -42
- notionary/blocks/image/image_markdown_node.py +33 -0
- notionary/blocks/image/image_models.py +0 -0
- notionary/blocks/markdown_builder.py +356 -0
- notionary/blocks/markdown_node.py +29 -0
- notionary/blocks/mention/__init__.py +7 -0
- notionary/blocks/{mention_element.py → mention/mention_element.py} +6 -2
- notionary/blocks/mention/mention_markdown_node.py +38 -0
- notionary/blocks/mention/mention_models.py +0 -0
- notionary/blocks/numbered_list/__init__.py +7 -0
- notionary/blocks/{numbered_list_element.py → numbered_list/numbered_list_element.py} +10 -6
- notionary/blocks/numbered_list/numbered_list_markdown_node.py +29 -0
- notionary/blocks/numbered_list/numbered_list_models.py +0 -0
- notionary/blocks/paragraph/__init__.py +7 -0
- notionary/blocks/{paragraph_element.py → paragraph/paragraph_element.py} +7 -3
- notionary/blocks/paragraph/paragraph_markdown_node.py +25 -0
- notionary/blocks/paragraph/paragraph_models.py +0 -0
- notionary/blocks/quote/__init__.py +7 -0
- notionary/blocks/quote/quote_element.py +92 -0
- notionary/blocks/quote/quote_markdown_node.py +23 -0
- notionary/blocks/quote/quote_models.py +0 -0
- notionary/blocks/registry/block_registry.py +17 -3
- notionary/blocks/registry/block_registry_builder.py +90 -178
- notionary/blocks/shared/__init__.py +0 -0
- notionary/blocks/shared/block_client.py +256 -0
- notionary/blocks/shared/models.py +713 -0
- notionary/blocks/{notion_block_element.py → shared/notion_block_element.py} +8 -5
- notionary/blocks/{text_inline_formatter.py → shared/text_inline_formatter.py} +14 -14
- notionary/blocks/shared/text_inline_formatter_new.py +139 -0
- notionary/blocks/table/__init__.py +7 -0
- notionary/blocks/{table_element.py → table/table_element.py} +23 -11
- notionary/blocks/table/table_markdown_node.py +40 -0
- notionary/blocks/table/table_models.py +0 -0
- notionary/blocks/todo/__init__.py +7 -0
- notionary/blocks/{todo_element.py → todo/todo_element.py} +8 -4
- notionary/blocks/todo/todo_markdown_node.py +31 -0
- notionary/blocks/todo/todo_models.py +0 -0
- notionary/blocks/toggle/__init__.py +4 -0
- notionary/blocks/{toggle_element.py → toggle/toggle_element.py} +7 -3
- notionary/blocks/toggle/toggle_markdown_node.py +35 -0
- notionary/blocks/toggle/toggle_models.py +0 -0
- notionary/blocks/toggleable_heading/__init__.py +9 -0
- notionary/blocks/{toggleable_heading_element.py → toggleable_heading/toggleable_heading_element.py} +8 -4
- notionary/blocks/toggleable_heading/toggleable_heading_markdown_node.py +43 -0
- notionary/blocks/toggleable_heading/toggleable_heading_models.py +0 -0
- notionary/blocks/video/__init__.py +7 -0
- notionary/blocks/{video_element.py → video/video_element.py} +82 -57
- notionary/blocks/video/video_markdown_node.py +30 -0
- notionary/file_upload/notion_file_upload.py +1 -1
- notionary/page/content/markdown_whitespace_processor.py +80 -0
- notionary/page/content/notion_text_length_utils.py +87 -0
- notionary/page/content/page_content_retriever.py +18 -10
- notionary/page/content/page_content_writer.py +97 -148
- notionary/page/formatting/line_processor.py +153 -0
- notionary/page/formatting/markdown_to_notion_converter.py +104 -425
- notionary/page/notion_page.py +9 -11
- notionary/page/notion_to_markdown_converter.py +9 -13
- notionary/util/factory_decorator.py +0 -0
- notionary/workspace.py +0 -1
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/METADATA +1 -1
- notionary-0.2.19.dist-info/RECORD +150 -0
- notionary/blocks/audio_element.py +0 -144
- notionary/blocks/callout_element.py +0 -122
- notionary/blocks/document_element.py +0 -194
- notionary/blocks/notion_block_client.py +0 -26
- notionary/blocks/qoute_element.py +0 -169
- notionary/page/content/notion_page_content_chunker.py +0 -84
- notionary/page/formatting/spacer_rules.py +0 -483
- notionary-0.2.17.dist-info/RECORD +0 -85
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/LICENSE +0 -0
- {notionary-0.2.17.dist-info → notionary-0.2.19.dist-info}/WHEEL +0 -0
@@ -0,0 +1,256 @@
|
|
1
|
+
from typing import Optional, Dict, Any
|
2
|
+
from notionary.base_notion_client import BaseNotionClient
|
3
|
+
from notionary.util import singleton
|
4
|
+
from notionary.blocks.shared.models import Block, BlockChildrenResponse
|
5
|
+
|
6
|
+
|
7
|
+
@singleton
|
8
|
+
class NotionBlockClient(BaseNotionClient):
|
9
|
+
"""
|
10
|
+
Client for Notion Block API operations.
|
11
|
+
"""
|
12
|
+
|
13
|
+
async def get_block(self, block_id: str) -> Optional[Block]:
|
14
|
+
"""
|
15
|
+
Retrieves a single block by its ID.
|
16
|
+
"""
|
17
|
+
self.logger.debug("Retrieving block: %s", block_id)
|
18
|
+
|
19
|
+
response = await self.get(f"blocks/{block_id}")
|
20
|
+
if response:
|
21
|
+
try:
|
22
|
+
return Block.model_validate(response)
|
23
|
+
except Exception as e:
|
24
|
+
self.logger.error("Failed to parse block response: %s", str(e))
|
25
|
+
return None
|
26
|
+
return None
|
27
|
+
|
28
|
+
async def get_block_children(
|
29
|
+
self, block_id: str, start_cursor: Optional[str] = None, page_size: int = 100
|
30
|
+
) -> Optional[BlockChildrenResponse]:
|
31
|
+
"""
|
32
|
+
Retrieves the children of a block with pagination support.
|
33
|
+
"""
|
34
|
+
self.logger.debug("Retrieving children of block: %s", block_id)
|
35
|
+
|
36
|
+
params = {"page_size": min(page_size, 100)}
|
37
|
+
if start_cursor:
|
38
|
+
params["start_cursor"] = start_cursor
|
39
|
+
|
40
|
+
response = await self.get(f"blocks/{block_id}/children", params=params)
|
41
|
+
if response:
|
42
|
+
try:
|
43
|
+
return BlockChildrenResponse.model_validate(response)
|
44
|
+
except Exception as e:
|
45
|
+
self.logger.error("Failed to parse block children response: %s", str(e))
|
46
|
+
return None
|
47
|
+
return None
|
48
|
+
|
49
|
+
async def get_all_block_children(self, block_id: str) -> list[Block]:
|
50
|
+
"""
|
51
|
+
Retrieves ALL children of a block, handling pagination automatically.
|
52
|
+
"""
|
53
|
+
all_blocks = []
|
54
|
+
cursor = None
|
55
|
+
|
56
|
+
while True:
|
57
|
+
response = await self.get_block_children(
|
58
|
+
block_id=block_id, start_cursor=cursor, page_size=100
|
59
|
+
)
|
60
|
+
|
61
|
+
if not response:
|
62
|
+
break
|
63
|
+
|
64
|
+
all_blocks.extend(response.results)
|
65
|
+
|
66
|
+
if not response.has_more:
|
67
|
+
break
|
68
|
+
|
69
|
+
cursor = response.next_cursor
|
70
|
+
|
71
|
+
self.logger.debug(
|
72
|
+
"Retrieved %d total children for block %s", len(all_blocks), block_id
|
73
|
+
)
|
74
|
+
return all_blocks
|
75
|
+
|
76
|
+
async def append_block_children(
|
77
|
+
self, block_id: str, children: list[Dict[str, Any]], after: Optional[str] = None
|
78
|
+
) -> Optional[BlockChildrenResponse]:
|
79
|
+
"""
|
80
|
+
Appends new child blocks to a parent block.
|
81
|
+
Automatically handles batching for more than 100 blocks.
|
82
|
+
"""
|
83
|
+
if not children:
|
84
|
+
self.logger.warning("No children provided to append")
|
85
|
+
return None
|
86
|
+
|
87
|
+
self.logger.debug("Appending %d children to block: %s", len(children), block_id)
|
88
|
+
|
89
|
+
# If 100 or fewer blocks, use single request
|
90
|
+
if len(children) <= 100:
|
91
|
+
return await self._append_single_batch(block_id, children, after)
|
92
|
+
|
93
|
+
# For more than 100 blocks, use batch processing
|
94
|
+
return await self._append_multiple_batches(block_id, children, after)
|
95
|
+
|
96
|
+
async def _append_single_batch(
|
97
|
+
self, block_id: str, children: list[Dict[str, Any]], after: Optional[str] = None
|
98
|
+
) -> Optional[BlockChildrenResponse]:
|
99
|
+
"""
|
100
|
+
Appends a single batch of blocks (≤100).
|
101
|
+
"""
|
102
|
+
data = {"children": children}
|
103
|
+
if after:
|
104
|
+
data["after"] = after
|
105
|
+
|
106
|
+
response = await self.patch(f"blocks/{block_id}/children", data)
|
107
|
+
if response:
|
108
|
+
try:
|
109
|
+
return BlockChildrenResponse.model_validate(response)
|
110
|
+
except Exception as e:
|
111
|
+
self.logger.error("Failed to parse append response: %s", str(e))
|
112
|
+
return None
|
113
|
+
return None
|
114
|
+
|
115
|
+
async def _append_multiple_batches(
|
116
|
+
self, block_id: str, children: list[Dict[str, Any]], after: Optional[str] = None
|
117
|
+
) -> Optional[BlockChildrenResponse]:
|
118
|
+
"""
|
119
|
+
Appends multiple batches of blocks, handling pagination.
|
120
|
+
"""
|
121
|
+
all_results = []
|
122
|
+
current_after = after
|
123
|
+
batch_size = 100
|
124
|
+
|
125
|
+
self.logger.info(
|
126
|
+
"Processing %d blocks in batches of %d", len(children), batch_size
|
127
|
+
)
|
128
|
+
|
129
|
+
# Process blocks in chunks of 100
|
130
|
+
for i in range(0, len(children), batch_size):
|
131
|
+
batch = children[i : i + batch_size]
|
132
|
+
batch_num = (i // batch_size) + 1
|
133
|
+
total_batches = (len(children) + batch_size - 1) // batch_size
|
134
|
+
|
135
|
+
self.logger.debug(
|
136
|
+
"Processing batch %d/%d (%d blocks)",
|
137
|
+
batch_num,
|
138
|
+
total_batches,
|
139
|
+
len(batch),
|
140
|
+
)
|
141
|
+
|
142
|
+
# Append current batch
|
143
|
+
response = await self._append_single_batch(block_id, batch, current_after)
|
144
|
+
|
145
|
+
if not response:
|
146
|
+
self.logger.error(
|
147
|
+
"Failed to append batch %d/%d", batch_num, total_batches
|
148
|
+
)
|
149
|
+
# Return partial results if we have any
|
150
|
+
if all_results:
|
151
|
+
return self._combine_batch_responses(all_results)
|
152
|
+
return None
|
153
|
+
|
154
|
+
all_results.append(response)
|
155
|
+
|
156
|
+
# Update 'after' to the last block ID from this batch for next iteration
|
157
|
+
if response.results:
|
158
|
+
current_after = response.results[-1].id
|
159
|
+
|
160
|
+
self.logger.debug(
|
161
|
+
"Successfully appended batch %d/%d", batch_num, total_batches
|
162
|
+
)
|
163
|
+
|
164
|
+
self.logger.info(
|
165
|
+
"Successfully appended all %d blocks in %d batches",
|
166
|
+
len(children),
|
167
|
+
len(all_results),
|
168
|
+
)
|
169
|
+
|
170
|
+
# Combine all batch responses into a single response
|
171
|
+
return self._combine_batch_responses(all_results)
|
172
|
+
|
173
|
+
def _combine_batch_responses(
|
174
|
+
self, responses: list[BlockChildrenResponse]
|
175
|
+
) -> BlockChildrenResponse:
|
176
|
+
"""
|
177
|
+
Combines multiple batch responses into a single response.
|
178
|
+
"""
|
179
|
+
if not responses:
|
180
|
+
# Return empty response structure
|
181
|
+
return BlockChildrenResponse(
|
182
|
+
object="list",
|
183
|
+
results=[],
|
184
|
+
next_cursor=None,
|
185
|
+
has_more=False,
|
186
|
+
type="block",
|
187
|
+
block={},
|
188
|
+
request_id="",
|
189
|
+
)
|
190
|
+
|
191
|
+
# Use the first response as template and combine all results
|
192
|
+
combined = responses[0]
|
193
|
+
all_blocks = []
|
194
|
+
|
195
|
+
for response in responses:
|
196
|
+
all_blocks.extend(response.results)
|
197
|
+
|
198
|
+
# Create new combined response
|
199
|
+
return BlockChildrenResponse(
|
200
|
+
object=combined.object,
|
201
|
+
results=all_blocks,
|
202
|
+
next_cursor=None, # No pagination in combined result
|
203
|
+
has_more=False, # All blocks are included
|
204
|
+
type=combined.type,
|
205
|
+
block=combined.block,
|
206
|
+
request_id=responses[-1].request_id, # Use last request ID
|
207
|
+
)
|
208
|
+
|
209
|
+
async def update_block(
|
210
|
+
self, block_id: str, block_data: Dict[str, Any], archived: Optional[bool] = None
|
211
|
+
) -> Optional[Block]:
|
212
|
+
"""
|
213
|
+
Updates an existing block.
|
214
|
+
"""
|
215
|
+
self.logger.debug("Updating block: %s", block_id)
|
216
|
+
|
217
|
+
data = block_data.copy()
|
218
|
+
if archived is not None:
|
219
|
+
data["archived"] = archived
|
220
|
+
|
221
|
+
response = await self.patch(f"blocks/{block_id}", data)
|
222
|
+
if response:
|
223
|
+
try:
|
224
|
+
return Block.model_validate(response)
|
225
|
+
except Exception as e:
|
226
|
+
self.logger.error("Failed to parse update response: %s", str(e))
|
227
|
+
return None
|
228
|
+
return None
|
229
|
+
|
230
|
+
async def delete_block(self, block_id: str) -> Optional[Block]:
|
231
|
+
"""
|
232
|
+
Deletes (archives) a block.
|
233
|
+
"""
|
234
|
+
self.logger.debug("Deleting block: %s", block_id)
|
235
|
+
|
236
|
+
success = await self.delete(f"blocks/{block_id}")
|
237
|
+
if success:
|
238
|
+
# After deletion, retrieve the block to return the updated state
|
239
|
+
return await self.get_block(block_id)
|
240
|
+
return None
|
241
|
+
|
242
|
+
async def archive_block(self, block_id: str) -> Optional[Block]:
|
243
|
+
"""
|
244
|
+
Archives a block by setting archived=True.
|
245
|
+
"""
|
246
|
+
self.logger.debug("Archiving block: %s", block_id)
|
247
|
+
|
248
|
+
return await self.update_block(block_id=block_id, block_data={}, archived=True)
|
249
|
+
|
250
|
+
async def unarchive_block(self, block_id: str) -> Optional[Block]:
|
251
|
+
"""
|
252
|
+
Unarchives a block by setting archived=False.
|
253
|
+
"""
|
254
|
+
self.logger.debug("Unarchiving block: %s", block_id)
|
255
|
+
|
256
|
+
return await self.update_block(block_id=block_id, block_data={}, archived=False)
|